|
| 1 | +#' Get path relative to project root (Quarto-aware) |
| 2 | +#' |
| 3 | +#' @description |
| 4 | +#' `r lifecycle::badge("experimental")` |
| 5 | +#' |
| 6 | +#' This function constructs file paths relative to the project root when |
| 7 | +#' running in a Quarto context (using `QUARTO_PROJECT_ROOT` or `QUARTO_PROJECT_DIR` |
| 8 | +#' environment variables), or falls back to intelligent project root detection |
| 9 | +#' when not in a Quarto context. |
| 10 | +#' |
| 11 | +#' It is experimental and subject to change in future releases. The automatic |
| 12 | +#' project root detection may not work reliably in all contexts, especially when |
| 13 | +#' projects have complex directory structures or when running in non-standard |
| 14 | +#' environments. For a more explicit and potentially more robust approach, |
| 15 | +#' consider using [here::i_am()] to declare your project structure, |
| 16 | +#' followed by [here::here()] for path construction. See examples for comparison. |
| 17 | +#' |
| 18 | +#' @details |
| 19 | +#' The function uses the following fallback hierarchy to determine the project root: |
| 20 | +#' |
| 21 | +#' - Quarto environment variables set during Quarto commands (e.g., `quarto render`): |
| 22 | +#' - `QUARTO_PROJECT_ROOT` environment variable (set by Quarto commands) |
| 23 | +#' - `QUARTO_PROJECT_DIR` environment variable (alternative Quarto variable) |
| 24 | +#' |
| 25 | +#' - Fallback to intelligent project root detection using [xfun::proj_root()] for interactive sessions: |
| 26 | +#' - `_quarto.yml` or `_quarto.yaml` (Quarto project files) |
| 27 | +#' - `DESCRIPTION` file with `Package:` field (R package or Project) |
| 28 | +#' - `.Rproj` files with `Version:` field (RStudio projects) |
| 29 | +#' |
| 30 | +#' Last fallback is the current working directory if no project root can be determined. |
| 31 | +#' A warning is issued to alert users that behavior may differ between interactive use and Quarto rendering, |
| 32 | +#' as in this case the computed path may be wrong. |
| 33 | +#' |
| 34 | +#' @section Use in Quarto document cells: |
| 35 | +#' |
| 36 | +#' This function is particularly useful in Quarto document cells where you want to |
| 37 | +#' use a path relative to the project root dynamically during rendering. |
| 38 | +#' |
| 39 | +#' ````markdown |
| 40 | +#' ```{r}`r ''` |
| 41 | +#' # Get a csv path from data directory in the Quarto project root |
| 42 | +#' data <- project_path("data", "my_data.csv") |
| 43 | +#' ``` |
| 44 | +#' ```` |
| 45 | +#' |
| 46 | +#' @param ... Character vectors of path components to be joined |
| 47 | +#' @param root Project root directory. If `NULL` (default), automatic detection |
| 48 | +#' is used following the hierarchy described above |
| 49 | +#' @return A character vector of the normalized file path relative to the project root. |
| 50 | +#' |
| 51 | +#' @examples |
| 52 | +#' \dontrun{ |
| 53 | +#' # Create a dummy Quarto project structure for example |
| 54 | +#' tmpdir <- tempfile("quarto_project") |
| 55 | +#' dir.create(tmpdir) |
| 56 | +#' quarto::quarto_create_project( |
| 57 | +#' 'test project', type = 'blog', |
| 58 | +#' dir = tmpdir, no_prompt = TRUE, quiet = TRUE |
| 59 | +#' ) |
| 60 | +#' project_dir <- file.path(tmpdir, "test project") |
| 61 | +#' |
| 62 | +#' # Simulate working within a blog post |
| 63 | +#' xfun::in_dir( |
| 64 | +#' dir = file.path(project_dir, "posts", "welcome"), { |
| 65 | +#' |
| 66 | +#' # Reference a data file from project root |
| 67 | +#' # ../../data/my_data.csv |
| 68 | +#' quarto::project_path("data", "my_data.csv") |
| 69 | +#' |
| 70 | +#' # Reference a script from project root |
| 71 | +#' # ../../R/analysis.R |
| 72 | +#' quarto::project_path("R", "analysis.R") |
| 73 | +#' |
| 74 | +#' # Explicitly specify root (overrides automatic detection) |
| 75 | +#' # ../../data/file.csv |
| 76 | +#' quarto::project_path("data", "file.csv", root = "../..") |
| 77 | +#' |
| 78 | +#' # Alternative approach using here::i_am() (potentially more robust) |
| 79 | +#' # This approach requires you to declare where you are in the project: |
| 80 | +#' if (requireNamespace("here", quietly = TRUE)) { |
| 81 | +#' # Declare that this document is in the project root or subdirectory |
| 82 | +#' here::i_am("posts/welcome/index.qmd") |
| 83 | +#' |
| 84 | +#' # Now here::here() will work reliably from the project root |
| 85 | +#' here::here("data", "my_data.csv") |
| 86 | +#' here::here("R", "analysis.R") |
| 87 | +#' } |
| 88 | +#' }) |
| 89 | +#' |
| 90 | +#' } |
| 91 | +#' |
| 92 | +#' @seealso |
| 93 | +#' * [here::here()] and [here::i_am()] for a similar function that works with R projects |
| 94 | +#' * [find_project_root()] to search for Quarto Project configuration in parents directories |
| 95 | +#' * [get_running_project_root()] for detecting the project root in Quarto commands |
| 96 | +#' * [xfun::from_root()] for the underlying path construction |
| 97 | +#' * [xfun::proj_root()] for project root detection logic |
| 98 | +#' |
| 99 | +#' @export |
| 100 | +project_path <- function(..., root = NULL) { |
| 101 | + if (is.null(root)) { |
| 102 | + # Try Quarto project environment variables first |
| 103 | + quarto_root <- get_running_project_root() |
| 104 | + |
| 105 | + root <- if (!is.null(quarto_root) && nzchar(quarto_root)) { |
| 106 | + quarto_root |
| 107 | + } else { |
| 108 | + # Try to find project root using xfun::proj_root() with extended rules |
| 109 | + tryCatch( |
| 110 | + { |
| 111 | + # Create extended rules that include Quarto and VS Code project files |
| 112 | + extended_rules <- rbind( |
| 113 | + # this should be the same as Quarto environment variables |
| 114 | + # which are only set when running Quarto commands |
| 115 | + c("_quarto.yml", ""), # Quarto project config |
| 116 | + c("_quarto.yaml", ""), # Alternative Quarto config |
| 117 | + xfun::root_rules # Default rules (DESCRIPTION, .Rproj) |
| 118 | + ) |
| 119 | + |
| 120 | + proj_root <- xfun::proj_root(rules = extended_rules) |
| 121 | + if (!is.null(proj_root)) { |
| 122 | + proj_root |
| 123 | + } else { |
| 124 | + cli::cli_warn(c( |
| 125 | + "Failed to determine project root using {.fun xfun::proj_root}. Using current working directory.", |
| 126 | + ">" = "This may lead to different behavior interactively vs running Quarto commands." |
| 127 | + )) |
| 128 | + getwd() |
| 129 | + } |
| 130 | + }, |
| 131 | + error = function(e) { |
| 132 | + # Fall back to working directory if proj_root() fails |
| 133 | + cli::cli_warn(c( |
| 134 | + "Failed to determine project root: {e$message}. Using current working directory as a fallback.", |
| 135 | + ">" = "This may lead to different behavior interactively vs running Quarto commands." |
| 136 | + )) |
| 137 | + getwd() # Return the working directory |
| 138 | + } |
| 139 | + ) |
| 140 | + } |
| 141 | + } |
| 142 | + |
| 143 | + # Normalize the root path |
| 144 | + root <- xfun::normalize_path(root) |
| 145 | + # Use xfun::from_root for better path handling |
| 146 | + path <- rlang::try_fetch( |
| 147 | + xfun::from_root(..., root = root, error = TRUE), |
| 148 | + error = function(e) { |
| 149 | + rlang::abort( |
| 150 | + c( |
| 151 | + "Failed to construct project path", |
| 152 | + ">" = "Ensure you are using valid path components." |
| 153 | + ), |
| 154 | + parent = e, |
| 155 | + call = rlang::caller_env() |
| 156 | + ) |
| 157 | + } |
| 158 | + ) |
| 159 | + path |
| 160 | +} |
| 161 | + |
| 162 | +#' Get the root of the currently running Quarto project |
| 163 | +#' |
| 164 | +#' @description |
| 165 | +#' This function is to be used inside cells and will return the project root |
| 166 | +#' when doing [quarto_render()] by detecting Quarto project environment variables. |
| 167 | +#' |
| 168 | +#' @details |
| 169 | +#' Quarto sets `QUARTO_PROJECT_ROOT` and `QUARTO_PROJECT_DIR` environment |
| 170 | +#' variables when executing commands within a Quarto project context (e.g., |
| 171 | +#' `quarto render`, `quarto preview`). This function detects their presence. |
| 172 | +#' |
| 173 | +#' Note that this function will return `NULL` when running code interactively |
| 174 | +#' in an IDE (even within a Quarto project directory), as these specific |
| 175 | +#' environment variables are only set during Quarto command execution. |
| 176 | +#' |
| 177 | +#' @section Use in Quarto document cells: |
| 178 | +#' |
| 179 | +#' This function is particularly useful in Quarto document cells where you want to |
| 180 | +#' get the project root path dynamically during rendering. Cell example: |
| 181 | +#' |
| 182 | +#' ````markdown |
| 183 | +#' ```{r}`r ''` |
| 184 | +#' # Get the project root path |
| 185 | +#' project_root <- get_running_project_root() |
| 186 | +#' ``` |
| 187 | +#' ```` |
| 188 | +#' |
| 189 | +#' @return Character Quarto project root path from set environment variables. |
| 190 | +#' |
| 191 | +#' @seealso |
| 192 | +#' * [find_project_root()] for finding the Quarto project root directory |
| 193 | +#' * [project_path()] for constructing paths relative to the project root |
| 194 | +#' @examples |
| 195 | +#' \dontrun{ |
| 196 | +#' get_running_project_root() |
| 197 | +#' } |
| 198 | +#' @export |
| 199 | +get_running_project_root <- function() { |
| 200 | + root <- Sys.getenv("QUARTO_PROJECT_ROOT", Sys.getenv("QUARTO_PROJECT_DIR")) |
| 201 | + if (!nzchar(root)) { |
| 202 | + return() |
| 203 | + } |
| 204 | + root |
| 205 | +} |
| 206 | + |
| 207 | +#' Find the root of a Quarto project |
| 208 | +#' |
| 209 | +#' @description |
| 210 | +#' This function checks if the current working directory is within a Quarto |
| 211 | +#' project by looking for Quarto project files (`_quarto.yml` or `_quarto.yaml`). |
| 212 | +#' Unlike [get_running_project_root()], this works both during rendering and |
| 213 | +#' interactive sessions. |
| 214 | +#' |
| 215 | +#' @param path Character. Path to check for Quarto project files. Defaults to |
| 216 | +#' current working directory. |
| 217 | +#' |
| 218 | +#' @return Character Path of the project root directory if found, or `NULL` |
| 219 | +#' |
| 220 | +#' @examplesIf quarto_available() |
| 221 | +#' tmpdir <- tempfile() |
| 222 | +#' dir.create(tmpdir) |
| 223 | +#' find_project_root(tmpdir) |
| 224 | +#' quarto_create_project("test-proj", type = "blog", dir = tmpdir, no_prompt = TRUE, quiet = TRUE) |
| 225 | +#' blog_post_dir <- file.path(tmpdir, "test-proj", "posts", "welcome") |
| 226 | +#' find_project_root(blog_post_dir) |
| 227 | +#' |
| 228 | +#' xfun::in_dir(blog_post_dir, { |
| 229 | +#' # Check if current directory is a Quarto project or in one |
| 230 | +#' !is.null(find_project_root()) |
| 231 | +#' }) |
| 232 | +#' |
| 233 | +#' # clean up |
| 234 | +#' unlink(tmpdir, recursive = TRUE) |
| 235 | +#' |
| 236 | +#' |
| 237 | +#' @seealso [get_running_project_root()] for detecting active Quarto rendering |
| 238 | +#' @export |
| 239 | +find_project_root <- function(path = ".") { |
| 240 | + quarto_rules <- rbind( |
| 241 | + c("_quarto.yml", ""), |
| 242 | + c("_quarto.yaml", "") |
| 243 | + ) |
| 244 | + xfun::proj_root(path = path, rules = quarto_rules) |
| 245 | +} |
0 commit comments