From 09b36f2dd65ad20d9b3b9f8c4933e6549a6cc801 Mon Sep 17 00:00:00 2001 From: larnsce Date: Tue, 25 Aug 2026 10:19:13 +0200 Subject: [PATCH 1/2] feat: use_brand() installs and refreshes the openwashdata brand use_brand() copies _brand.yml and the logo files it references from the central openwashdata/brand repository (raw GitHub content at a configurable ref, or a local/alternative source for tests and offline work) into the active package. Re-running refreshes idempotently and reports written and updated files by md5 comparison; unchanged files stay untouched. With pkgdown = TRUE an existing _pkgdown.yml is wired to the brand through template.bslib.brand so the next site build renders with the brand fonts and colors; without a config the wiring is skipped with a setup_website() hint. Brand values are never edited locally, keeping the change-here-first discipline of the brand repo. Five tests cover install, idempotent refresh with source change, pkgdown wiring and re-run stability, the missing-config skip, and the missing-source error. Full suite passes at 63. Refs #109 Assisted-by: Claude claude-fable-5 --- DESCRIPTION | 3 +- NAMESPACE | 1 + NEWS.md | 6 ++ R/use_brand.R | 162 ++++++++++++++++++++++++++++++++ man/use_brand.Rd | 53 +++++++++++ tests/testthat/test_use_brand.R | 75 +++++++++++++++ 6 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 R/use_brand.R create mode 100644 man/use_brand.Rd create mode 100644 tests/testthat/test_use_brand.R diff --git a/DESCRIPTION b/DESCRIPTION index 6889163..721730d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -35,7 +35,8 @@ Imports: tools, usethis (>= 2.2.3), utils (>= 4.3.3), - stringr + stringr, + yaml Suggests: knitr, remotes, diff --git a/NAMESPACE b/NAMESPACE index d679928..98dc78e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -17,4 +17,5 @@ export(update_citation) export(update_description) export(update_gsheet_metadata) export(update_metadata) +export(use_brand) importFrom(utils,head) diff --git a/NEWS.md b/NEWS.md index 3c25ecf..817ba36 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # washr (development version) +- New `use_brand()` installs the openwashdata brand (`_brand.yml` and the + logo files it references) from the central openwashdata/brand repository + into the active package, refreshes an existing copy idempotently, and + wires an existing `_pkgdown.yml` to the brand through bslib so the + package site renders with the brand fonts and colors (#109). + # washr 1.0.2 Patch release: bug fixes only, no new API. New maintainer: Lars Schöbitz. diff --git a/R/use_brand.R b/R/use_brand.R new file mode 100644 index 0000000..d282769 --- /dev/null +++ b/R/use_brand.R @@ -0,0 +1,162 @@ +#' Install or refresh the openwashdata brand in the active package +#' +#' @description +#' `use_brand()` copies the openwashdata brand definition (`_brand.yml`) +#' and the logo files it references from the central +#' [openwashdata/brand](https://github.com/openwashdata/brand) repository +#' into the package root. Re-running the function refreshes an existing +#' copy and reports which files changed, so consuming packages stay in +#' sync with the central definition. +#' +#' Brand values are never edited locally: change them in +#' openwashdata/brand first, then refresh consumers with `use_brand()`. +#' +#' @details +#' With `pkgdown = TRUE` (the default), an existing `_pkgdown.yml` is +#' pointed at the brand through bslib (`template.bslib.brand`), so the +#' next [pkgdown::build_site()] renders the site with the brand fonts +#' and colors. The wiring rewrites `_pkgdown.yml` through the yaml +#' package, which does not preserve comments in that file. When no +#' `_pkgdown.yml` exists, the wiring is skipped with a hint to run +#' [setup_website()] first. +#' +#' @param ref Character. Git reference (branch or tag) of +#' openwashdata/brand to copy from. Defaults to `"main"`. +#' @param pkgdown Logical. Should `_pkgdown.yml` be wired to use the +#' brand via bslib? Defaults to `TRUE`. +#' @param source Character. Advanced: an alternative source for the +#' brand files, either a local directory or a URL prefix. When `NULL` +#' (the default), the raw GitHub content of openwashdata/brand at +#' `ref` is used. Mainly useful for tests and offline work. +#' +#' @returns Invisibly, a character vector of the files written or +#' updated (empty when everything was already current). +#' +#' @export +#' +#' @examples +#' \dontrun{ +#' # Install the brand and wire the pkgdown site +#' use_brand() +#' +#' # Refresh later, without touching _pkgdown.yml +#' use_brand(pkgdown = FALSE) +#' } +use_brand <- function(ref = "main", pkgdown = TRUE, source = NULL) { + if (is.null(source)) { + source <- paste0( + "https://raw.githubusercontent.com/openwashdata/brand/", ref + ) + } + + changed <- character(0) + + # The brand definition itself. + brand_tmp <- fetch_brand_file(source, "_brand.yml") + changed <- c(changed, place_brand_file(brand_tmp, "_brand.yml")) + + # The logo files the brand definition references. + brand <- yaml::read_yaml("_brand.yml") + for (path in brand_logo_paths(brand)) { + fetched <- fetch_brand_file(source, path) + changed <- c(changed, place_brand_file(fetched, path)) + } + + if (isTRUE(pkgdown)) { + changed <- c(changed, wire_pkgdown_brand()) + } + + if (length(changed) == 0) { + usethis::ui_done("Brand is up to date; nothing to change.") + } + invisible(changed) +} + +# Download or copy one brand file into a tempfile. +fetch_brand_file <- function(base, path) { + tmp <- tempfile() + if (dir.exists(base)) { + src <- file.path(base, path) + if (!file.exists(src)) { + usethis::ui_stop("Brand source file not found: {src}") + } + file.copy(src, tmp) + } else { + url <- paste(base, path, sep = "/") + ok <- tryCatch( + { + utils::download.file(url, tmp, quiet = TRUE, mode = "wb") + TRUE + }, + error = function(e) FALSE, + warning = function(w) FALSE + ) + if (!ok) { + usethis::ui_stop( + "Could not download {url}. Check the network connection and that openwashdata/brand carries the file on this ref." + ) + } + } + tmp +} + +# Write a fetched file to its destination when new or changed; report and +# return the destination path, or an empty vector when unchanged. +place_brand_file <- function(tmp, dest) { + destdir <- dirname(dest) + if (destdir != "." && !dir.exists(destdir)) { + dir.create(destdir, recursive = TRUE) + } + status <- if (!file.exists(dest)) { + "written" + } else if (identical( + unname(tools::md5sum(tmp)), unname(tools::md5sum(dest)) + )) { + "unchanged" + } else { + "updated" + } + if (status == "unchanged") { + return(character(0)) + } + file.copy(tmp, dest, overwrite = TRUE) + usethis::ui_done("{usethis::ui_path(dest)} {status}.") + dest +} + +# The logo paths a brand definition references: the named images plus any +# size entries that are direct paths rather than image names. +brand_logo_paths <- function(brand) { + logo <- brand$logo + if (is.null(logo)) { + return(character(0)) + } + images <- unlist(logo$images, use.names = FALSE) + sizes <- unlist(logo[setdiff(names(logo), "images")], use.names = FALSE) + direct <- setdiff(sizes, names(logo$images)) + unique(c(images, direct)) +} + +# Point an existing _pkgdown.yml at the brand through bslib. Returns the +# config path when it changed, or an empty vector. +wire_pkgdown_brand <- function() { + configpath <- "_pkgdown.yml" + if (!file.exists(configpath)) { + usethis::ui_info( + "No _pkgdown.yml found; skipping the pkgdown wiring. Run washr::setup_website() first, then use_brand() again." + ) + return(character(0)) + } + config <- yaml::read_yaml(configpath) + if (identical(config$template$bslib$brand, "_brand.yml")) { + return(character(0)) + } + config$template$bslib$brand <- "_brand.yml" + if (is.null(config$template$bootstrap)) { + config$template$bootstrap <- 5 + } + yaml::write_yaml(config, configpath) + usethis::ui_done("{usethis::ui_path(configpath)} wired to the brand via bslib.") + usethis::ui_info("Rebuild the site with pkgdown::build_site() to apply the brand.") + configpath +} diff --git a/man/use_brand.Rd b/man/use_brand.Rd new file mode 100644 index 0000000..c4f145e --- /dev/null +++ b/man/use_brand.Rd @@ -0,0 +1,53 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/use_brand.R +\name{use_brand} +\alias{use_brand} +\title{Install or refresh the openwashdata brand in the active package} +\usage{ +use_brand(ref = "main", pkgdown = TRUE, source = NULL) +} +\arguments{ +\item{ref}{Character. Git reference (branch or tag) of +openwashdata/brand to copy from. Defaults to \code{"main"}.} + +\item{pkgdown}{Logical. Should \verb{_pkgdown.yml} be wired to use the +brand via bslib? Defaults to \code{TRUE}.} + +\item{source}{Character. Advanced: an alternative source for the +brand files, either a local directory or a URL prefix. When \code{NULL} +(the default), the raw GitHub content of openwashdata/brand at +\code{ref} is used. Mainly useful for tests and offline work.} +} +\value{ +Invisibly, a character vector of the files written or +updated (empty when everything was already current). +} +\description{ +\code{use_brand()} copies the openwashdata brand definition (\verb{_brand.yml}) +and the logo files it references from the central +\href{https://github.com/openwashdata/brand}{openwashdata/brand} repository +into the package root. Re-running the function refreshes an existing +copy and reports which files changed, so consuming packages stay in +sync with the central definition. + +Brand values are never edited locally: change them in +openwashdata/brand first, then refresh consumers with \code{use_brand()}. +} +\details{ +With \code{pkgdown = TRUE} (the default), an existing \verb{_pkgdown.yml} is +pointed at the brand through bslib (\code{template.bslib.brand}), so the +next \code{\link[pkgdown:build_site]{pkgdown::build_site()}} renders the site with the brand fonts +and colors. The wiring rewrites \verb{_pkgdown.yml} through the yaml +package, which does not preserve comments in that file. When no +\verb{_pkgdown.yml} exists, the wiring is skipped with a hint to run +\code{\link[=setup_website]{setup_website()}} first. +} +\examples{ +\dontrun{ +# Install the brand and wire the pkgdown site +use_brand() + +# Refresh later, without touching _pkgdown.yml +use_brand(pkgdown = FALSE) +} +} diff --git a/tests/testthat/test_use_brand.R b/tests/testthat/test_use_brand.R new file mode 100644 index 0000000..94c440a --- /dev/null +++ b/tests/testthat/test_use_brand.R @@ -0,0 +1,75 @@ +options(usethis.quiet = TRUE) +# TEST use_brand --------------------------------------------------------------- + +make_brand_source <- function(dir = tempfile("brandsrc")) { + dir.create(file.path(dir, "logos"), recursive = TRUE) + writeLines( + c( + "meta:", + " name: openwashdata", + "color:", + " palette:", + " owd-purple: \"#5b195b\"", + " primary: owd-purple", + "logo:", + " images:", + " icon: logos/icon.png", + " small: icon" + ), + file.path(dir, "_brand.yml") + ) + writeBin(as.raw(1:8), file.path(dir, "logos", "icon.png")) + dir +} + +test_that("use_brand installs the brand and referenced logos", { + create_local_package() + rlang::local_interactive(FALSE) + src <- make_brand_source() + written <- use_brand(source = src, pkgdown = FALSE) + expect_true(file.exists("_brand.yml")) + expect_true(file.exists("logos/icon.png")) + expect_setequal(written, c("_brand.yml", "logos/icon.png")) +}) + +test_that("use_brand is idempotent and reports refreshed files", { + create_local_package() + rlang::local_interactive(FALSE) + src <- make_brand_source() + use_brand(source = src, pkgdown = FALSE) + second <- use_brand(source = src, pkgdown = FALSE) + expect_length(second, 0) + # A change in the central source must reach the consumer on refresh. + writeBin(as.raw(9:16), file.path(src, "logos", "icon.png")) + third <- use_brand(source = src, pkgdown = FALSE) + expect_identical(third, "logos/icon.png") +}) + +test_that("use_brand wires an existing _pkgdown.yml to the brand", { + create_local_package() + rlang::local_interactive(FALSE) + src <- make_brand_source() + writeLines(c("template:", " bootstrap: 5"), "_pkgdown.yml") + written <- use_brand(source = src) + config <- yaml::read_yaml("_pkgdown.yml") + expect_identical(config$template$bslib$brand, "_brand.yml") + expect_true("_pkgdown.yml" %in% written) + # A second run leaves the wiring untouched. + expect_false("_pkgdown.yml" %in% use_brand(source = src)) +}) + +test_that("use_brand skips the pkgdown wiring when no _pkgdown.yml exists", { + create_local_package() + rlang::local_interactive(FALSE) + src <- make_brand_source() + expect_no_error(use_brand(source = src)) + expect_false(file.exists("_pkgdown.yml")) +}) + +test_that("use_brand errors clearly on a missing source file", { + create_local_package() + rlang::local_interactive(FALSE) + src <- tempfile("emptysrc") + dir.create(src) + expect_error(use_brand(source = src, pkgdown = FALSE), "not found") +}) From 262dcf4c1f510458fffda5dcbb036dfe05228044 Mon Sep 17 00:00:00 2001 From: larnsce Date: Tue, 25 Aug 2026 10:25:03 +0200 Subject: [PATCH 2/2] docs: declare brand.yml in Suggests, found in the fslogisticskampala trial bslib resolves the brand through the brand.yml package at site build time; without it pkgdown::build_site() stops. Surfaced by the first end-to-end run on a real data package. Refs #109 Assisted-by: Claude claude-fable-5 --- DESCRIPTION | 3 ++- R/use_brand.R | 4 +++- man/use_brand.Rd | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 721730d..d1f2c09 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -37,7 +37,8 @@ Imports: utils (>= 4.3.3), stringr, yaml -Suggests: +Suggests: + brand.yml, knitr, remotes, rmarkdown, diff --git a/R/use_brand.R b/R/use_brand.R index d282769..75c12be 100644 --- a/R/use_brand.R +++ b/R/use_brand.R @@ -18,7 +18,9 @@ #' and colors. The wiring rewrites `_pkgdown.yml` through the yaml #' package, which does not preserve comments in that file. When no #' `_pkgdown.yml` exists, the wiring is skipped with a hint to run -#' [setup_website()] first. +#' [setup_website()] first. Building the wired site requires the +#' brand.yml package (bslib asks for it at build time); it is listed in +#' Suggests and installed on demand. #' #' @param ref Character. Git reference (branch or tag) of #' openwashdata/brand to copy from. Defaults to `"main"`. diff --git a/man/use_brand.Rd b/man/use_brand.Rd index c4f145e..b71a827 100644 --- a/man/use_brand.Rd +++ b/man/use_brand.Rd @@ -40,7 +40,9 @@ next \code{\link[pkgdown:build_site]{pkgdown::build_site()}} renders the site wi and colors. The wiring rewrites \verb{_pkgdown.yml} through the yaml package, which does not preserve comments in that file. When no \verb{_pkgdown.yml} exists, the wiring is skipped with a hint to run -\code{\link[=setup_website]{setup_website()}} first. +\code{\link[=setup_website]{setup_website()}} first. Building the wired site requires the +brand.yml package (bslib asks for it at build time); it is listed in +Suggests and installed on demand. } \examples{ \dontrun{