Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ Imports:
tools,
usethis (>= 2.2.3),
utils (>= 4.3.3),
stringr
Suggests:
stringr,
yaml
Suggests:
brand.yml,
knitr,
remotes,
rmarkdown,
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export(update_citation)
export(update_description)
export(update_gsheet_metadata)
export(update_metadata)
export(use_brand)
importFrom(utils,head)
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
164 changes: 164 additions & 0 deletions R/use_brand.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#' 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. 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"`.
#' @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
}
55 changes: 55 additions & 0 deletions man/use_brand.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 75 additions & 0 deletions tests/testthat/test_use_brand.R
Original file line number Diff line number Diff line change
@@ -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")
})
Loading