diff --git a/DESCRIPTION b/DESCRIPTION index 8525c74a..7cadd730 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: quarto Title: R Interface to 'Quarto' Markdown Publishing System -Version: 1.4.4.9027 +Version: 1.4.4.9028 Authors@R: c( person("JJ", "Allaire", , "jj@posit.co", role = "aut", comment = c(ORCID = "0000-0003-0174-9868")), diff --git a/NAMESPACE b/NAMESPACE index ee02003b..7f249acc 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -5,6 +5,7 @@ export(check_newer_version) export(detect_bookdown_crossrefs) export(find_project_root) export(get_running_project_root) +export(has_parameters) export(is_using_quarto) export(new_blog_post) export(project_path) diff --git a/NEWS.md b/NEWS.md index f4ce3e1f..25f2be97 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,8 @@ - Package is now licenced MIT like Quarto CLI. +- Added `has_parameters()` function to detect whether Quarto documents use parameters. The function works with both knitr and Jupyter engines: for knitr documents (.qmd), it checks for a "params" field in the document metadata; for Jupyter notebooks (.ipynb), it detects cells tagged with "parameters" using papermill convention. This enables programmatic identification of parameterized documents for automated workflows and document processing (#245). + - Added `detect_bookdown_crossrefs()` function to help users migrate from bookdown to Quarto by identifying cross-references that need manual conversion. The function scans R Markdown or Quarto files to detect bookdown-specific cross-reference syntax (like `\@ref(fig:label)` and `(\#eq:label)`) and provides detailed guidance on converting them to Quarto syntax (like `@fig-label` and `{#eq-label}`). It offers both compact and verbose reporting modes, with context-aware warnings that only show syntax patterns actually found in your files. - Added `project_path()`, `get_running_project_root()`, and `find_project_root()` functions for Quarto-aware project path construction. These functions provide a consistent way to reference files relative to the project root, working both during Quarto rendering (using `QUARTO_PROJECT_ROOT` environment variables) and in interactive sessions (using intelligent project detection). The `project_path()` function is particularly useful in Quarto document cells where you need to reference data files or scripts from the project root regardless of the document's location in subdirectories (#180).0). diff --git a/R/parameters.R b/R/parameters.R new file mode 100644 index 00000000..c154b628 --- /dev/null +++ b/R/parameters.R @@ -0,0 +1,107 @@ +#' Check if a Quarto document uses parameters +#' +#' Determines whether a Quarto document uses parameters by examining the document +#' structure and metadata. This function works with both knitr and Jupyter engines, +#' using different detection methods for each: +#' +#' - **Knitr engine (.qmd files)**: Checks for a "params" field in the document's +#' YAML metadata using `quarto_inspect()` +#' - **Jupyter engine (.ipynb files)**: Looks for code cells tagged with "parameters" +#' following the papermill convention. For .ipynb files, the function parses the +#' notebook JSON directly due to limitations in `quarto inspect`. +#' +#' @param input Path to the Quarto document (.qmd or .ipynb file) to inspect. +#' +#' @return Logical. `TRUE` if the document uses parameters, `FALSE` otherwise. +#' +#' @details +#' Parameters in Quarto enable creating dynamic, reusable documents. This function +#' helps identify parameterized documents programmatically, which is useful for: +#' +#' - Document processing workflows +#' - Automated report generation +#' - Parameter validation before rendering +#' - Project analysis and organization +#' +#' For more information about using parameters in Quarto, see +#' +#' +#' @examples +#' \dontrun{ +#' # Check if a document uses parameters +#' has_parameters("my-document.qmd") +#' +#' # Check a parameterized report +#' has_parameters("parameterized-report.qmd") +#' +#' # Check a Jupyter notebook +#' has_parameters("analysis.ipynb") +#' +#' # Use in a workflow +#' if (has_parameters("report.qmd")) { +#' message("This document accepts parameters") +#' } +#' } +#' +#' @export +has_parameters <- function(input) { + if (!file.exists(input)) { + cli::cli_abort( + c( + "File {.file {input}} does not exist.", + ">" = "Please provide a valid Quarto document." + ), + call = rlang::caller_env() + ) + } + + # Check for Jupyter engine: look for cells with "parameters" tag + # Note: quarto_inspect() has limitations with Jupyter notebooks and may not + # detect code cells properly, so we fall back to direct JSON parsing for .ipynb files + if (identical(fs::path_ext(input), "ipynb")) { + return(has_parameters_jupyter_direct(input)) + } + + inspect <- quarto::quarto_inspect(input) + + if (identical(inspect$engines, "jupyter")) { + return( + "parameters" %in% inspect$fileInformation[[input]]$codeCells$metadata$tags + ) + } else if (identical(inspect$engines, "knitr")) { + return( + "params" %in% names(inspect$fileInformation[[input]]$metadata) + ) + } else { + return(FALSE) + } +} + +# Helper function to directly parse Jupyter notebook JSON for parameters +# This is needed because quarto_inspect() has limitations with detecting +# code cells in Jupyter notebooks +has_parameters_jupyter_direct <- function(notebook_path) { + tryCatch( + { + # Read and parse the notebook JSON with simplifyDataFrame = FALSE + # to preserve the original structure + notebook_json <- jsonlite::fromJSON( + notebook_path, + simplifyDataFrame = FALSE + ) + + # Check if there are cells + if (length(notebook_json$cells) == 0) { + return(FALSE) + } + # Look through cells for parameters tag + any(sapply(notebook_json$cells, function(cell) { + "parameters" %in% cell$metadata$tags + })) + }, + error = function(e) { + # If JSON parsing fails, return FALSE + return(FALSE) + } + ) +} diff --git a/_pkgdown.yml b/_pkgdown.yml index 0eea97f6..a89bb9f1 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -45,6 +45,7 @@ reference: for Quarto documents and projects: contents: - quarto_inspect + - has_parameters - quarto_path - quarto_version - quarto_available diff --git a/man/has_parameters.Rd b/man/has_parameters.Rd new file mode 100644 index 00000000..d3c728fa --- /dev/null +++ b/man/has_parameters.Rd @@ -0,0 +1,58 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/parameters.R +\name{has_parameters} +\alias{has_parameters} +\title{Check if a Quarto document uses parameters} +\usage{ +has_parameters(input) +} +\arguments{ +\item{input}{Path to the Quarto document (.qmd or .ipynb file) to inspect.} +} +\value{ +Logical. \code{TRUE} if the document uses parameters, \code{FALSE} otherwise. +} +\description{ +Determines whether a Quarto document uses parameters by examining the document +structure and metadata. This function works with both knitr and Jupyter engines, +using different detection methods for each: +} +\details{ +\itemize{ +\item \strong{Knitr engine (.qmd files)}: Checks for a "params" field in the document's +YAML metadata using \code{quarto_inspect()} +\item \strong{Jupyter engine (.ipynb files)}: Looks for code cells tagged with "parameters" +following the papermill convention. For .ipynb files, the function parses the +notebook JSON directly due to limitations in \verb{quarto inspect}. +} + +Parameters in Quarto enable creating dynamic, reusable documents. This function +helps identify parameterized documents programmatically, which is useful for: +\itemize{ +\item Document processing workflows +\item Automated report generation +\item Parameter validation before rendering +\item Project analysis and organization +} + +For more information about using parameters in Quarto, see +\url{https://quarto.org/docs/computations/parameters.html} +} +\examples{ +\dontrun{ +# Check if a document uses parameters +has_parameters("my-document.qmd") + +# Check a parameterized report +has_parameters("parameterized-report.qmd") + +# Check a Jupyter notebook +has_parameters("analysis.ipynb") + +# Use in a workflow +if (has_parameters("report.qmd")) { + message("This document accepts parameters") +} +} + +} diff --git a/tests/testthat/resources/test-no-parameters.ipynb b/tests/testthat/resources/test-no-parameters.ipynb new file mode 100644 index 00000000..5ede4a6a --- /dev/null +++ b/tests/testthat/resources/test-no-parameters.ipynb @@ -0,0 +1,47 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "cell-0", + "metadata": {}, + "source": [ + "---\n", + "title: \"Test Notebook without Parameters\"\n", + "format: html\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "cell-1", + "metadata": {}, + "source": [ + "## Test Notebook\n", + "\n", + "This notebook does not use parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-2", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Hello World!\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tests/testthat/resources/test-with-parameters.ipynb b/tests/testthat/resources/test-with-parameters.ipynb new file mode 100644 index 00000000..2a348e03 --- /dev/null +++ b/tests/testthat/resources/test-with-parameters.ipynb @@ -0,0 +1,63 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "cell-0", + "metadata": {}, + "source": [ + "---\n", + "title: \"Test Notebook with Parameters\"\n", + "format: html\n", + "---" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-1", + "metadata": { + "tags": [ + "parameters" + ] + }, + "outputs": [], + "source": [ + "# Parameters cell\n", + "name = \"John\"\n", + "age = 30" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "## Test Notebook\n", + "\n", + "This notebook uses parameters defined in the cell above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"Hello {name}, you are {age} years old!\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tests/testthat/test-parameters.R b/tests/testthat/test-parameters.R new file mode 100644 index 00000000..ee84cd70 --- /dev/null +++ b/tests/testthat/test-parameters.R @@ -0,0 +1,58 @@ +test_that("has_parameters() works with knitr engine", { + skip_if_no_quarto() + + # Create a .qmd file with params in metadata + qmd_with_params <- local_qmd_file( + "---", + "title: Test Document", + "params:", + " name: John", + "---", + "", + "# Test Document", + "", + "This is a test document with parameters.", + "```{r}", + "params$name", + "```" + ) + + # Create a .qmd file without params + qmd_no_params <- local_qmd_file( + "---", + "title: Test Document", + "---", + "", + "# Test Document", + "", + "This is a test document without parameters." + ) + + expect_true(has_parameters(qmd_with_params)) + expect_false(has_parameters(qmd_no_params)) +}) + +test_that("has_parameters() works with Jupyter engine", { + skip_if_no_quarto() + + # Test with notebook that has parameters cell + expect_true(has_parameters(resources_path("test-with-parameters.ipynb"))) + + # Test with notebook that doesn't have parameters + expect_false(has_parameters(resources_path("test-no-parameters.ipynb"))) +}) + +test_that("has_parameters() handles non-existent files", { + expect_error( + has_parameters("non-existent-file.qmd"), + class = "rlang_error" + ) +}) + +test_that("has_parameters() works with existing test files", { + skip_if_no_quarto() + + # Test with existing test files that don't have parameters + expect_false(has_parameters(test_path("test.qmd"))) + expect_false(has_parameters(test_path("test.ipynb"))) +})