diff --git a/news/changelog-1.9.md b/news/changelog-1.9.md index cd707849124..fc729be0e8d 100644 --- a/news/changelog-1.9.md +++ b/news/changelog-1.9.md @@ -11,6 +11,7 @@ All changes included in 1.9: - ([#13633](https://github.com/quarto-dev/quarto-cli/issues/13633)): Fix detection and auto-installation of babel language packages from newer error format that doesn't explicitly mention `.ldf` filename. - ([#13694](https://github.com/quarto-dev/quarto-cli/issues/13694)): Fix `notebook-view.url` being ignored - external notebook links now properly use specified URLs instead of local preview files. - ([#13732](https://github.com/quarto-dev/quarto-cli/issues/13732)): Fix automatic font package installation for fonts with spaces in their names (e.g., "Noto Emoji", "DejaVu Sans"). Font file search patterns now match both with and without spaces. +- ([#13798](https://github.com/quarto-dev/quarto-cli/pull/13798)): Directories specified in `ExecutionEngineDiscovery.ignoreDirs` were not getting ignored. ## Dependencies @@ -18,6 +19,12 @@ All changes included in 1.9: - Update `deno` to 2.4.5 - ([#13601](https://github.com/quarto-dev/quarto-cli/pull/13601)): Update `mermaid` to 11.12.0 (author: @multimeric) +## Extensions + +- Metadata and brand extensions now work without a `_quarto.yml` project. (Engine extensions do too.) A temporary default project is created in memory. + +- New **Engine Extensions**, to allow other execution engines than knitr, jupyter, julia. Julia is now a bundled extension. See [the prerelease notes](https://prerelease.quarto.org/docs/prerelease/1.9/) and [engine extension documentation](https://prerelease.quarto.org/docs/extensions/engine.html). + ## Formats ### `gfm` diff --git a/src/project/project-context.ts b/src/project/project-context.ts index 119a5e69989..a863a0d839a 100644 --- a/src/project/project-context.ts +++ b/src/project/project-context.ts @@ -63,6 +63,7 @@ import { fileExecutionEngine, fileExecutionEngineAndTarget, projectIgnoreGlobs, + resolveEngines, } from "../execute/engine.ts"; import { ExecutionEngineInstance, kMarkdownEngine } from "../execute/types.ts"; @@ -884,6 +885,9 @@ async function projectInputFilesInternal( project: ProjectContext, metadata?: ProjectConfig, ): Promise<{ files: string[]; engines: string[] }> { + // Resolve engines so engineIgnoreDirs() uses all engines (including external) + await resolveEngines(project); + const { dir } = project; const outputDir = metadata?.project[kProjectOutputDir]; diff --git a/tests/docs/books/book-404-detection/.gitignore b/tests/docs/books/book-404-detection/.gitignore new file mode 100644 index 00000000000..ad293093b07 --- /dev/null +++ b/tests/docs/books/book-404-detection/.gitignore @@ -0,0 +1,2 @@ +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/books/book-404-detection/404.ipynb b/tests/docs/books/book-404-detection/404.ipynb new file mode 100644 index 00000000000..7fcb42c90cb --- /dev/null +++ b/tests/docs/books/book-404-detection/404.ipynb @@ -0,0 +1,27 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "404-page-content", + "metadata": {}, + "source": [ + "---\n", + "title: \"Page Not Found\"\n", + "---\n", + "\n", + "# 404 - Page Not Found\n", + "\n", + "Sorry, the page you're looking for doesn't exist." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/docs/books/book-404-detection/_quarto.yml b/tests/docs/books/book-404-detection/_quarto.yml new file mode 100644 index 00000000000..a2f43cce5da --- /dev/null +++ b/tests/docs/books/book-404-detection/_quarto.yml @@ -0,0 +1,13 @@ +project: + type: book + +book: + title: "Test Book with 404" + author: "Test Author" + chapters: + - index.qmd + - chapter1.qmd + +format: + html: + theme: default diff --git a/tests/docs/books/book-404-detection/chapter1.qmd b/tests/docs/books/book-404-detection/chapter1.qmd new file mode 100644 index 00000000000..d42fb0a9311 --- /dev/null +++ b/tests/docs/books/book-404-detection/chapter1.qmd @@ -0,0 +1,3 @@ +# Chapter 1 + +Content here. diff --git a/tests/docs/books/book-404-detection/index.qmd b/tests/docs/books/book-404-detection/index.qmd new file mode 100644 index 00000000000..f443585c09b --- /dev/null +++ b/tests/docs/books/book-404-detection/index.qmd @@ -0,0 +1,3 @@ +# Welcome + +This is the index page. diff --git a/tests/docs/books/book-404-rmd/.gitignore b/tests/docs/books/book-404-rmd/.gitignore new file mode 100644 index 00000000000..ad293093b07 --- /dev/null +++ b/tests/docs/books/book-404-rmd/.gitignore @@ -0,0 +1,2 @@ +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/books/book-404-rmd/404.rmd b/tests/docs/books/book-404-rmd/404.rmd new file mode 100644 index 00000000000..a38facfd7b1 --- /dev/null +++ b/tests/docs/books/book-404-rmd/404.rmd @@ -0,0 +1,7 @@ +--- +title: "Page Not Found" +--- + +# 404 - Page Not Found + +Sorry, the page you're looking for doesn't exist. diff --git a/tests/docs/books/book-404-rmd/_quarto.yml b/tests/docs/books/book-404-rmd/_quarto.yml new file mode 100644 index 00000000000..923c6857acc --- /dev/null +++ b/tests/docs/books/book-404-rmd/_quarto.yml @@ -0,0 +1,13 @@ +project: + type: book + +book: + title: "Test Book with 404 RMD" + author: "Test Author" + chapters: + - index.qmd + - chapter1.qmd + +format: + html: + theme: default diff --git a/tests/docs/books/book-404-rmd/chapter1.qmd b/tests/docs/books/book-404-rmd/chapter1.qmd new file mode 100644 index 00000000000..d42fb0a9311 --- /dev/null +++ b/tests/docs/books/book-404-rmd/chapter1.qmd @@ -0,0 +1,3 @@ +# Chapter 1 + +Content here. diff --git a/tests/docs/books/book-404-rmd/index.qmd b/tests/docs/books/book-404-rmd/index.qmd new file mode 100644 index 00000000000..f443585c09b --- /dev/null +++ b/tests/docs/books/book-404-rmd/index.qmd @@ -0,0 +1,3 @@ +# Welcome + +This is the index page. diff --git a/tests/docs/project/ignore-dirs/.gitignore b/tests/docs/project/ignore-dirs/.gitignore new file mode 100644 index 00000000000..425b124155d --- /dev/null +++ b/tests/docs/project/ignore-dirs/.gitignore @@ -0,0 +1,3 @@ +/.quarto/ +**/*.quarto_ipynb +*_files/ diff --git a/tests/docs/project/ignore-dirs/_quarto.yml b/tests/docs/project/ignore-dirs/_quarto.yml new file mode 100644 index 00000000000..b8bae5830fa --- /dev/null +++ b/tests/docs/project/ignore-dirs/_quarto.yml @@ -0,0 +1,2 @@ +project: + type: default diff --git a/tests/docs/project/ignore-dirs/index.qmd b/tests/docs/project/ignore-dirs/index.qmd new file mode 100644 index 00000000000..d6fa531f06a --- /dev/null +++ b/tests/docs/project/ignore-dirs/index.qmd @@ -0,0 +1,5 @@ +--- +title: "Test Project" +--- + +This file should be rendered. diff --git a/tests/docs/project/ignore-dirs/renv/test.qmd b/tests/docs/project/ignore-dirs/renv/test.qmd new file mode 100644 index 00000000000..cae7b70a970 --- /dev/null +++ b/tests/docs/project/ignore-dirs/renv/test.qmd @@ -0,0 +1,5 @@ +--- +title: "Renv Test" +--- + +This file should NOT be rendered (renv is a Knitr ignore directory). diff --git a/tests/docs/project/ignore-dirs/venv/test.qmd b/tests/docs/project/ignore-dirs/venv/test.qmd new file mode 100644 index 00000000000..07e37657c74 --- /dev/null +++ b/tests/docs/project/ignore-dirs/venv/test.qmd @@ -0,0 +1,5 @@ +--- +title: "Venv Test" +--- + +This file should NOT be rendered (venv is a Jupyter ignore directory). diff --git a/tests/docs/websites/website-ignore-dirs/.gitignore b/tests/docs/websites/website-ignore-dirs/.gitignore new file mode 100644 index 00000000000..b5d6aa19d53 --- /dev/null +++ b/tests/docs/websites/website-ignore-dirs/.gitignore @@ -0,0 +1,4 @@ +/.quarto/ +**/*.quarto_ipynb +*_files/ +_site/ diff --git a/tests/docs/websites/website-ignore-dirs/_quarto.yml b/tests/docs/websites/website-ignore-dirs/_quarto.yml new file mode 100644 index 00000000000..3795ae0692a --- /dev/null +++ b/tests/docs/websites/website-ignore-dirs/_quarto.yml @@ -0,0 +1,13 @@ +project: + type: website + +website: + title: "Test Website" + navbar: + left: + - text: Home + file: index.qmd + +format: + html: + theme: default diff --git a/tests/docs/websites/website-ignore-dirs/index.qmd b/tests/docs/websites/website-ignore-dirs/index.qmd new file mode 100644 index 00000000000..e8da0d1c419 --- /dev/null +++ b/tests/docs/websites/website-ignore-dirs/index.qmd @@ -0,0 +1,5 @@ +--- +title: "Test Website" +--- + +This file should be rendered. diff --git a/tests/docs/websites/website-ignore-dirs/renv/test.qmd b/tests/docs/websites/website-ignore-dirs/renv/test.qmd new file mode 100644 index 00000000000..cae7b70a970 --- /dev/null +++ b/tests/docs/websites/website-ignore-dirs/renv/test.qmd @@ -0,0 +1,5 @@ +--- +title: "Renv Test" +--- + +This file should NOT be rendered (renv is a Knitr ignore directory). diff --git a/tests/docs/websites/website-ignore-dirs/venv/test.qmd b/tests/docs/websites/website-ignore-dirs/venv/test.qmd new file mode 100644 index 00000000000..07e37657c74 --- /dev/null +++ b/tests/docs/websites/website-ignore-dirs/venv/test.qmd @@ -0,0 +1,5 @@ +--- +title: "Venv Test" +--- + +This file should NOT be rendered (venv is a Jupyter ignore directory). diff --git a/tests/docs/websites/website-sidebar-section-index/.gitignore b/tests/docs/websites/website-sidebar-section-index/.gitignore new file mode 100644 index 00000000000..b5d6aa19d53 --- /dev/null +++ b/tests/docs/websites/website-sidebar-section-index/.gitignore @@ -0,0 +1,4 @@ +/.quarto/ +**/*.quarto_ipynb +*_files/ +_site/ diff --git a/tests/docs/websites/website-sidebar-section-index/_quarto.yml b/tests/docs/websites/website-sidebar-section-index/_quarto.yml new file mode 100644 index 00000000000..ce583919ccc --- /dev/null +++ b/tests/docs/websites/website-sidebar-section-index/_quarto.yml @@ -0,0 +1,14 @@ +project: + type: website + +website: + title: "Sidebar Auto Test" + sidebar: + contents: + - index.qmd + - section: "Subdir Section" + contents: subdir/* + +format: + html: + theme: default diff --git a/tests/docs/websites/website-sidebar-section-index/index.qmd b/tests/docs/websites/website-sidebar-section-index/index.qmd new file mode 100644 index 00000000000..50187e5080c --- /dev/null +++ b/tests/docs/websites/website-sidebar-section-index/index.qmd @@ -0,0 +1,5 @@ +--- +title: "Home" +--- + +This is the home page. diff --git a/tests/docs/websites/website-sidebar-section-index/subdir/index.ipynb b/tests/docs/websites/website-sidebar-section-index/subdir/index.ipynb new file mode 100644 index 00000000000..b03257b03b3 --- /dev/null +++ b/tests/docs/websites/website-sidebar-section-index/subdir/index.ipynb @@ -0,0 +1,33 @@ +{ + "cells": [ + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "---\n", + "title: \"Subdir Index\"\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is the index page for the subdir." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tests/docs/websites/website-sidebar-section-index/subdir/other.qmd b/tests/docs/websites/website-sidebar-section-index/subdir/other.qmd new file mode 100644 index 00000000000..6eaa73d3cbc --- /dev/null +++ b/tests/docs/websites/website-sidebar-section-index/subdir/other.qmd @@ -0,0 +1,5 @@ +--- +title: "Other Page" +--- + +This is another page in the subdir. diff --git a/tests/smoke/book/book-404-detection.test.ts b/tests/smoke/book/book-404-detection.test.ts new file mode 100644 index 00000000000..ffd60d75823 --- /dev/null +++ b/tests/smoke/book/book-404-detection.test.ts @@ -0,0 +1,51 @@ +import { testQuartoCmd } from "../../test.ts"; +import { fileExists, noErrorsOrWarnings } from "../../verify.ts"; +import { existsSync } from "../../../src/deno_ral/fs.ts"; +import { join } from "../../../src/deno_ral/path.ts"; +import { docs } from "../../utils.ts"; + +// Test that book 404 page with .ipynb extension is detected +const inputIpynb = docs("books/book-404-detection"); +const outputDirIpynb = join(inputIpynb, "_book"); + +testQuartoCmd( + "render", + [inputIpynb], + [ + noErrorsOrWarnings, + fileExists(join(outputDirIpynb, "index.html")), + fileExists(join(outputDirIpynb, "chapter1.html")), + fileExists(join(outputDirIpynb, "404.html")), + fileExists(join(outputDirIpynb, "search.json")), + ], + { + teardown: async () => { + if (existsSync(outputDirIpynb)) { + await Deno.remove(outputDirIpynb, { recursive: true }); + } + }, + }, +); + +// Test that book 404 page with .rmd extension is detected +const inputRmd = docs("books/book-404-rmd"); +const outputDirRmd = join(inputRmd, "_book"); + +testQuartoCmd( + "render", + [inputRmd], + [ + noErrorsOrWarnings, + fileExists(join(outputDirRmd, "index.html")), + fileExists(join(outputDirRmd, "chapter1.html")), + fileExists(join(outputDirRmd, "404.html")), + fileExists(join(outputDirRmd, "search.json")), + ], + { + teardown: async () => { + if (existsSync(outputDirRmd)) { + await Deno.remove(outputDirRmd, { recursive: true }); + } + }, + }, +); diff --git a/tests/smoke/project/project-ignore-dirs.test.ts b/tests/smoke/project/project-ignore-dirs.test.ts new file mode 100644 index 00000000000..f70e46578a0 --- /dev/null +++ b/tests/smoke/project/project-ignore-dirs.test.ts @@ -0,0 +1,46 @@ +/* + * project-ignore-dirs.test.ts + * + * Verifies that engine-specific ignore directories (venv, renv, env, packrat, etc.) + * are properly excluded from project file discovery and rendering. + * + * Copyright (C) 2020-2025 Posit Software, PBC + */ + +import { docs } from "../../utils.ts"; +import { join } from "../../../src/deno_ral/path.ts"; +import { existsSync } from "../../../src/deno_ral/fs.ts"; +import { testQuartoCmd } from "../../test.ts"; +import { fileExists, noErrors, pathDoNotExists } from "../../verify.ts"; + +const renderDir = docs("project/ignore-dirs"); +const outDir = join(Deno.cwd(), renderDir); + +// Test that engine ignore directories are properly excluded +testQuartoCmd( + "render", + [renderDir], + [ + noErrors, + fileExists(join(outDir, "index.html")), // Control: regular file should be rendered + pathDoNotExists(join(outDir, "venv", "test.html")), // venv (Jupyter) should be ignored + pathDoNotExists(join(outDir, "renv", "test.html")), // renv (Knitr) should be ignored + pathDoNotExists(join(outDir, "env", "test.html")), // env (Jupyter) should be ignored + ], + { + teardown: async () => { + // Clean up rendered HTML files + const htmlFiles = [ + join(outDir, "index.html"), + join(outDir, "venv", "test.html"), + join(outDir, "renv", "test.html"), + join(outDir, "env", "test.html"), + ]; + for (const file of htmlFiles) { + if (existsSync(file)) { + await Deno.remove(file); + } + } + }, + }, +); diff --git a/tests/smoke/website/website-ignore-dirs.test.ts b/tests/smoke/website/website-ignore-dirs.test.ts new file mode 100644 index 00000000000..a7e70885b52 --- /dev/null +++ b/tests/smoke/website/website-ignore-dirs.test.ts @@ -0,0 +1,37 @@ +/* + * website-ignore-dirs.test.ts + * + * Verifies that engine-specific ignore directories (venv, renv, env, packrat, etc.) + * are properly excluded from website file discovery and rendering. + * + * Copyright (C) 2020-2025 Posit Software, PBC + */ + +import { docs } from "../../utils.ts"; +import { join } from "../../../src/deno_ral/path.ts"; +import { existsSync } from "../../../src/deno_ral/fs.ts"; +import { testQuartoCmd } from "../../test.ts"; +import { fileExists, noErrors, pathDoNotExists } from "../../verify.ts"; + +const renderDir = docs("websites/website-ignore-dirs"); +const outDir = join(Deno.cwd(), renderDir, "_site"); + +// Test that engine ignore directories are properly excluded +testQuartoCmd( + "render", + [renderDir], + [ + noErrors, + fileExists(join(outDir, "index.html")), // Control: regular file should be rendered + pathDoNotExists(join(outDir, "venv", "test.html")), // venv (Jupyter) should be ignored + pathDoNotExists(join(outDir, "renv", "test.html")), // renv (Knitr) should be ignored + pathDoNotExists(join(outDir, "env", "test.html")), // env (Jupyter) should be ignored + ], + { + teardown: async () => { + if (existsSync(outDir)) { + await Deno.remove(outDir, { recursive: true }); + } + }, + }, +); diff --git a/tests/smoke/website/website-sidebar-section-index.test.ts b/tests/smoke/website/website-sidebar-section-index.test.ts new file mode 100644 index 00000000000..236d4ff49b6 --- /dev/null +++ b/tests/smoke/website/website-sidebar-section-index.test.ts @@ -0,0 +1,55 @@ +/* + * website-sidebar-section-index.test.ts + * + * Tests that sidebar section headers correctly link to index files with + * non-qmd extensions (like .ipynb) in subdirectories. This exercises the + * indexFileHrefForDir() -> engineValidExtensions() code path in + * website-sidebar-auto.ts. + * + * The test uses a section with glob pattern (contents: subdir/*) which + * triggers indexFileHrefForDir() to scan for index files. The subdir + * contains both index.ipynb and other.qmd - the section header should + * link to index.ipynb while other.qmd appears as a child item. + * + * Note: A second file (other.qmd) is required because sidebaritem.ejs + * only renders section hrefs when contents is non-empty. This may be + * a bug - sections with href but empty contents render as plain text + * instead of links. See sidebaritem.ejs lines 19 and 35-36. + * + * Copyright (C) 2020-2025 Posit Software, PBC + */ + +import { docs } from "../../utils.ts"; +import { join } from "../../../src/deno_ral/path.ts"; +import { existsSync } from "../../../src/deno_ral/fs.ts"; +import { testQuartoCmd } from "../../test.ts"; +import { ensureHtmlElements, fileExists, noErrorsOrWarnings } from "../../verify.ts"; + +const renderDir = docs("websites/website-sidebar-section-index"); +const outDir = join(Deno.cwd(), renderDir, "_site"); + +// Test that sidebar section headers link to index.ipynb in subdirectory +// (not just that the file renders - we verify the sidebar href) +testQuartoCmd( + "render", + [renderDir], + [ + noErrorsOrWarnings, + fileExists(join(outDir, "index.html")), // Main index page + fileExists(join(outDir, "subdir", "index.html")), // Subdir index from .ipynb should be rendered + fileExists(join(outDir, "subdir", "other.html")), // Other page should also render + // Verify the sidebar section header links to subdir/index.html + // This tests that indexFileHrefForDir() found the index.ipynb file + ensureHtmlElements( + join(outDir, "index.html"), + ['a.sidebar-link[href="./subdir/index.html"]'], // Sidebar should link to subdir index + ), + ], + { + teardown: async () => { + if (existsSync(outDir)) { + await Deno.remove(outDir, { recursive: true }); + } + }, + }, +);