diff --git a/DESCRIPTION b/DESCRIPTION index 341657a8..ee3009cb 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: officer Title: Manipulation of Microsoft Word and PowerPoint Documents -Version: 0.6.7.013 +Version: 0.6.7.014 Authors@R: c( person("David", "Gohel", , "david.gohel@ardata.fr", role = c("aut", "cre")), person("Stefan", "Moog", , "moogs@gmx.de", role = "aut"), diff --git a/NAMESPACE b/NAMESPACE index ceda3b5b..89f0a329 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -65,6 +65,7 @@ S3method(print,fp_cell) S3method(print,fp_par) S3method(print,fp_text) S3method(print,ftext) +S3method(print,layout_info) S3method(print,rdocx) S3method(print,rpptx) S3method(print,rtf) @@ -160,6 +161,7 @@ S3method(update,fp_text) S3method(update,fpar) S3method(update,sp_line) S3method(update,sp_lineend) +export("layout_rename_ph_labels<-") export(add_sheet) export(add_slide) export(annotate_base) @@ -241,6 +243,7 @@ export(hyperlink_ftext) export(image_to_base64) export(layout_dedupe_ph_labels) export(layout_properties) +export(layout_rename_ph_labels) export(layout_summary) export(media_extract) export(move_slide) diff --git a/NEWS.md b/NEWS.md index 0102a41d..f2ef4677 100644 --- a/NEWS.md +++ b/NEWS.md @@ -25,10 +25,11 @@ informative error message if the type is not present in layout (#601). ## Features -- add `layout_dedupe_ph_labels()` to handle duplicate placholder labels (#589). +- `layout_rename_ph_labels()` to rename ph labels (#610). +- add `layout_dedupe_ph_labels()` to handle duplicate placeholder labels (#589). By default, it will only detect duplicate labels, but apply no changes. With `action = "rename"`, it auto-renames duplicate labels and `action = "delete"` -deletes duplicates, only keeping their first occurence. +deletes duplicates, only keeping their first occurrence. - new convenience functions `body_replace_gg_at_bkm()` and `body_replace_plot_at_bkm()` to replace text content enclosed in a bookmark with a ggplot or a base plot. - add `unit` (in, cm, mm) argument in function `page_size()`. diff --git a/R/ph_location.R b/R/ph_location.R index 8e7ca423..8e789a31 100644 --- a/R/ph_location.R +++ b/R/ph_location.R @@ -15,7 +15,7 @@ get_ph_loc <- function(x, layout, master, type, type_idx = NULL, position_right, props <- layout_properties(x, layout = layout, master = master) if (!is.null(ph_id)) { - ids <- sort(na.omit(as.numeric(props$id))) + ids <- sort(stats::na.omit(as.numeric(props$id))) if (length(ids) <= 20) { .all_ids_switch <- c("x" = "Available ids: {.val {ids}}.") # only if few ids } else { diff --git a/R/ppt_ph_rename_layout.R b/R/ppt_ph_rename_layout.R new file mode 100644 index 00000000..35338023 --- /dev/null +++ b/R/ppt_ph_rename_layout.R @@ -0,0 +1,197 @@ +#' Change ph labels in a layout +#' +#' There are two versions of the function. The first takes a set of key-value pairs to rename the +#' ph labels. The second uses a right hand side (rhs) assignment to specify the new ph labels. +#' See section *Details*. \cr\cr +#' _NB:_ You can also rename ph labels directly in PowerPoint. Open the master template view +#' (`Alt` + `F10`) and go to `Home` > `Arrange` > `Selection Pane`. +#' +#' @details +#' * Note the difference between the terms `id` and `index`. Both can be found in the output of +#' [layout_properties()]. The unique ph `id` is found in column `id`. The `index` refers to the +#' index of the data frame row. +#' * In a right hand side (rhs) label assignment (`<- new_labels`), there are two ways to +#' optionally specify a subset of phs to rename. In both cases, the length of the rhs vector +#' (the new labels) must match the length of the id or index: +#' 1. use the `id` argument to specify ph ids to rename: `layout_rename_ph_labels(..., id = 2:3) <- new_labels` +#' 2. use an `index` in squared brackets: `layout_rename_ph_labels(...)[1:2] <- new_labels` +#' +#' @export +#' @rdname layout_rename_ph_labels +#' @param x An `rpptx` object. +#' @param layout Layout name or index. Index is the row index of [layout_summary()]. +#' @param master Name of master. Only required if the layout name is not unique across masters. +#' @param ... Comma separated list of key-value pairs to rename phs. Either reference a ph via its label +#' (`"old label"` = `"new label"`) or its unique id (`"id"` = `"new label"`). +#' @param .dots Provide a named list or vector of key-value pairs to rename phs +#' (`list("old label"` = `"new label"`). +#' @param id Unique placeholder id (see column `id` in [layout_properties()] or [plot_layout_properties()]). +#' @param value Not relevant for user. A pure technical necessity for rhs assignments. +#' @return Vector of renamed ph labels. +#' @example inst/examples/example_layout_rename_ph_labels.R +#' +layout_rename_ph_labels <- function(x, layout, master = NULL, ..., .dots = NULL) { + stop_if_not_rpptx(x, "x") + dots <- list(...) + dots <- c(dots, .dots) + if (length(dots) > 0 && !is_named(dots)) { + cli::cli_abort( + c("Unnamed arguments are not allowed.", + "x" = "Arguments {.arg ...} and {.arg .dots} both require key value pairs." + ), + call = NULL + ) + } + + l <- get_layout(x, layout, master) + lp <- layout_properties(x, l$layout_name, l$master_name) + if (length(dots) == 0) { + return(lp$ph_label) + } + df_renames <- .rename_df_from_dots(lp, dots) + .set_ph_labels(l, df_renames) + reload_slidelayouts(x) + + lp <- layout_properties(x, l$layout_name, l$master_name) + invisible(lp$ph_label) +} + + +#' @export +#' @rdname layout_rename_ph_labels +`layout_rename_ph_labels<-` <- function(x, layout, master = NULL, id = NULL, value) { + l <- get_layout(x, layout, master) + lp <- layout_properties(x, l$layout_name, l$master_name) + + if (!is.null(id)) { + if (length(id) != length(value)) { + cli::cli_abort( + c("{.arg id} and rhs vector must have the same length", + "x" = "Number of ids ({.val {length(id)}}) and assigned values ({.val {length(value)}}) differ" + ) + ) + } + wrong_ids <- setdiff(id, lp$id) + n_wrong <- length(wrong_ids) + if (n_wrong > 0) { + cli::cli_abort(c( + "{cli::qty(n_wrong)} {?This/These} id{?s} {?does/do} not exist: {.val {wrong_ids}}", + "x" = "Choose one of: {.val {lp$id}}", + "i" = cli::col_grey("Also see {.code plot_layout_properties(..., '{l$layout_name}', '{l$master_nam}')}") + )) + } + .idx <- match(id, lp$id) # user might enter ids in arbitrary order + lp$ph_label[.idx] <- value + value <- lp$ph_label + } + names(value) <- lp$id + df_renames <- .rename_df_from_dots(lp, value) + .set_ph_labels(l, df_renames) + reload_slidelayouts(x) +} + + +# heuristic: if a number, then treat as ph_id +.detect_ph_id <- function(x) { + suppressWarnings({ # avoid character to NA warning + !is.na(as.numeric(x)) # nchar(x) == 1 & + }) +} + + +# create data frame with: ph_id, ph_label, ph_label_new as a +# basis for subsequent renaming +# +# CAVEAT: the ph order in layout_properties() (i.e. get_xfrm_data()) is reference for the user. +# Using the 'slide_layout' object's xfrm() method does not yield the same ph order! +# We need to guarantee a proper match here. +# +.rename_df_from_dots <- function(lp, dots) { + lp <- lp[, c("id", "ph_label")] + label_old <- names(dots) + label_new <- as.character(dots) + is_id <- .detect_ph_id(label_old) + is_label <- !is_id + + # warn if renaming a duplicate label + ii <- duplicated(lp$ph_label) + dupes <- lp$ph_label[ii] + dupes_used <- intersect(label_old, dupes) + n_dupes_used <- length(dupes_used) + if (n_dupes_used > 0) { + cli::cli_warn(c( + "When renaming a label with duplicates, only the first occurrence is renamed.", + "x" = "Renaming {n_dupes_used} ph label{?s} with duplicates: {.val {dupes_used}}" + ), call = NULL) + } + + # check for duplicate renames + is_dupe <- duplicated(label_old) + if (any(is_dupe)) { + dupes <- unique(label_old[is_dupe]) + n_dupes <- length(dupes) + cli::cli_abort(c( + "Each id or label must only have one rename entry only.", + "x" = "Found {n_dupes} duplicate id{?s}/label{?s} to rename: {.val {dupes}}" + ), call = NULL) + } + + # match by label and check for unknown labels + label_old_ <- label_old[is_label] + row_idx_label <- match(label_old_, table = lp$ph_label) + i_wrong <- is.na(row_idx_label) + n_wrong <- sum(i_wrong) + if (n_wrong > 0) { + cli::cli_abort(c( + "Can't rename labels that don't exist.", + "x" = "{cli::qty(n_wrong)}{?This label does/These labels do} not exist: {.val {label_old_[i_wrong]}}" + ), call = NULL) + } + + # match by id and check for unknown ids + id_old_ <- label_old[is_id] + row_idx_id <- match(id_old_, table = lp$id) + i_wrong <- is.na(row_idx_id) + n_wrong <- sum(i_wrong) + if (n_wrong > 0) { + cli::cli_abort(c( + "Can't rename ids that don't exist.", + "x" = "{cli::qty(n_wrong)}{?This id does/These ids do} not exist: {.val {id_old_[i_wrong]}}" + ), call = NULL) + } + + # check for collision between label and id + idx_collision <- intersect(row_idx_label, row_idx_id) + n_collision <- length(idx_collision) + if (n_collision > 0) { + df <- lp[idx_collision, c("id", "ph_label")] + pairs <- paste(df$ph_label, "<-->", df$id) + cli::cli_abort(c( + "Either specify the label {.emph OR} the id of the ph to rename, not both.", + "x" = "These labels and ids collide: {.val {pairs}}" + ), call = NULL) + } + + lp$ph_label_new <- NA + lp$ph_label_new[row_idx_label] <- label_new[is_label] + lp$ph_label_new[row_idx_id] <- label_new[is_id] + lp[!is.na(lp$ph_label_new), , drop = FALSE] +} + + +.set_ph_labels <- function(l, df_renames) { + if (!inherits(l, "layout_info")) { + cli::cli_abort( + c("{.arg l} must a a {.cls layout_info} object", + "x" = "Got {.cls {class(l)[1]}} instead" + ), + call = NULL + ) + } + layout_xml <- l$slide_layout$get() + for (i in seq_len(nrow(df_renames))) { + cnvpr_node <- xml2::xml_find_first(layout_xml, sprintf("p:cSld/p:spTree/*/p:nvSpPr/p:cNvPr[@id='%s']", df_renames$id[i])) + xml2::xml_set_attr(cnvpr_node, "name", df_renames$ph_label_new[i]) + } + l$slide_layout$save() # persist changes in slide layout xml file +} diff --git a/R/pptx_informations.R b/R/pptx_informations.R index a5f47997..d4b21a34 100644 --- a/R/pptx_informations.R +++ b/R/pptx_informations.R @@ -95,6 +95,7 @@ layout_properties <- function(x, layout = NULL, master = NULL) { data[["cy"]] <- data[["cy"]] / 914400 data[["rotation"]] <- data[["rotation"]] / 60000 + rownames(data) <- NULL data } diff --git a/R/pptx_layout_helper.R b/R/pptx_layout_helper.R new file mode 100644 index 00000000..679109a5 --- /dev/null +++ b/R/pptx_layout_helper.R @@ -0,0 +1,116 @@ +#' Layout selection helper +#' +#' Select a layout by name or index. The master name is inferred and only required +#' for disambiguation in case the layout name is not unique across masters. +#' +#' @param x An `rpptx` object. +#' @param layout Layout name or index. Index refers to the row index of the [layout_summary()] +#' output. +#' @param master Name of master. Only required if layout name is not unique across masters. +#' @return A `` object, i.e. a list with the entries `index`, `layout_name`, +#' `layout_file`, `master_name`, `master_file`, and `slide_layout`. +#' @keywords internal +get_layout <- function(x, layout, master = NULL) { + stop_if_not_rpptx(x, "x") + if (!(is.numeric(layout) || is.character(layout))) { + cli::cli_abort( + c("{.arg layout} must be {.cls numeric} or {.cls character}", + "x" = "Got class {.cls {class(layout)[1]}} instead" + ) + ) + } + if (length(layout) != 1) { + cli::cli_abort( + c("{.arg layout} is not length 1", + "x" = "{.arg layout} must be {.emph one} layout name or index." + ) + ) + } + df <- x$slideLayouts$get_metadata() + names(df)[2:3] <- c("layout_name", "layout_file") # consistent naming + n_layouts <- nrow(df) + df$index <- seq_len(n_layouts) + + if (n_layouts == 0) { + cli::cli_alert_danger("No layouts available.") + return(NULL) + } + + if (is.numeric(layout)) { + res <- get_row_by_index(df, layout) + } else { + res <- get_row_by_name(df, layout, master) + } + l <- as.list(res) + slide_layout <- x$slideLayouts$collection_get(l$layout_file) + l <- c(l, slide_layout = slide_layout) + l <- l[c("index", "layout_name", "layout_file", "master_name", "master_file", "slide_layout")] # nice order + class(l) <- c("layout_info", "list") + l +} + + + +# else { +# # multiple layouts +# layout_exists(x, layout, must_exist = TRUE) +# layout_is_unique(x, layout, require_unique = TRUE) +# index <- which(df$layout_name == layout) +# } +# index <- which(df$layout_name == layout) +# l <- df[index, ] |> as.list() +# # l <- c(index = index, l, slide_layout = slide_layout) + +#' @export +print.layout_info <- function(x, ...) { + cli::cli_h3("{.cls layout_info} object") + str(utils::head(x, -1), give.attr = FALSE, no.list = TRUE) + cat(" $ slide_layout: 'R6' ") +} + + +get_row_by_index <- function(df, layout) { + index <- layout + if (!index %in% df$index) { + cli::cli_abort( + c("Layout index out of bounds.", + "x" = "Index must be between {.val {1}} and {.val {nrow(df)}}.", + "i" = cli::col_grey("See row indexes in {.fn layout_summary}") + ), + call = NULL + ) + } + df[index, ] +} + + +# select layout by name +get_row_by_name <- function(df, layout, master) { + if (!is.null(master)) { + masters <- unique(df$master_name) + if (!master %in% masters) { + cli::cli_abort(c( + "master {.val {master}} does not exist.", + "i" = "See {.fn layout_summary} for available masters." + ), call = NULL) + } + df <- df[df$master_name == master, ] + } + + df <- df[df$layout_name == layout, ] + if (nrow(df) == 0) { + msg <- ifelse(is.null(master), + "Layout {.val {layout}} does not exist", + "Layout {.val {layout}} does not exist in master {.val {master}}" + ) + cli::cli_abort(c(msg, "i" = "See {.fn layout_summary} for available layouts."), call = NULL) + return(NULL) + } + if (nrow(df) > 1) { + cli::cli_abort(c( + "Layout exists in more than one master", + "x" = "Please specify the master name in arg {.arg master}" + ), call = NULL) + } + df +} diff --git a/R/utils.R b/R/utils.R index 43f9550d..a97ae660 100644 --- a/R/utils.R +++ b/R/utils.R @@ -125,6 +125,8 @@ xfrmize <- function(slide_xfrm, master_xfrm) { x$type_idx <- stats::ave(x$type, x$master_file, x$file, x$type, FUN = seq_along) x$type_idx <- as.numeric(x$type_idx) # NB: ave returns character + x$id <- as.integer(x$id) + rownames(x) <- NULL # prevent meaningless rownames x } @@ -314,6 +316,24 @@ df_rename <- function(df, old, new) { } +# replacement for stopifnot() with nicer user feedback +stop_if_not_class <- function(x, class, arg = NULL) { + check <- inherits(x, what = class) + if (!check) { + msg_arg <- ifelse(is.null(arg), "Incorrect input.", "Incorrect input for {.arg {arg}}") + cli::cli_abort(c( + msg_arg, + "x" = "Expected {.cls {class}} but got {.cls {class(x)[1]}}" + ), call = NULL) + } +} + + +stop_if_not_rpptx <- function(x, arg = NULL) { + stop_if_not_class(x, "rpptx", arg) +} + + # htmlEscapeCopy ---- htmlEscapeCopy <- local({ @@ -354,13 +374,16 @@ htmlEscapeCopy <- local({ }) -# metric units ----- +# metric units ----------------------------------------------- +# cm_to_inches <- function(x) { x / 2.54 } + mm_to_inches <- function(x) { x / 25.4 } + convin <- function(unit, x) { unit <- match.arg(unit, choices = c("in", "cm", "mm"), several.ok = FALSE) if (!identical("in", unit)) { @@ -368,3 +391,22 @@ convin <- function(unit, x) { } x } + + +# from rlang pkg ------------------------------------------ + +is_named <- function(x) { + nms <- names(x) + if (is.null(nms)) { + return(FALSE) + } + if (any(detect_void_name(nms))) { + return(FALSE) + } + TRUE +} + + +detect_void_name <- function(x) { + x == "" | is.na(x) +} diff --git a/inst/examples/example_layout_rename_ph_labels.R b/inst/examples/example_layout_rename_ph_labels.R new file mode 100644 index 00000000..8422ce23 --- /dev/null +++ b/inst/examples/example_layout_rename_ph_labels.R @@ -0,0 +1,50 @@ +x <- read_pptx() + +# INFO ------------- + +# Returns layout's ph_labels by default in same order as layout_properties() +layout_rename_ph_labels(x, "Comparison") +layout_properties(x, "Comparison")$ph_label + + +# BASICS ----------- +# +# HINT: run `plot_layout_properties(x, "Comparison")` to see how labels change + +# rename using key-value pairs: 'old label' = 'new label' or 'id' = 'new label' +layout_rename_ph_labels(x, "Comparison", "Title 1" = "LABEL MATCHED") # label matching +layout_rename_ph_labels(x, "Comparison", "3" = "ID MATCHED") # id matching +layout_rename_ph_labels(x, "Comparison", "Date Placeholder 6" = "DATE", "8" = "FOOTER") # label, id + +# rename using a named list and the .dots arg +renames <- list("Content Placeholder 3" = "CONTENT_1", "6" = "CONTENT_2") +layout_rename_ph_labels(x, "Comparison", .dots = renames) + +# rename via rhs assignment and optional index (not id!) +layout_rename_ph_labels(x, "Comparison") <- LETTERS[1:8] +layout_rename_ph_labels(x, "Comparison")[1:3] <- paste("CHANGED", 1:3) + +# rename via rhs assignment and ph id (not index) +layout_rename_ph_labels(x, "Comparison", id = c(2, 4)) <- paste("ID =", c(2, 4)) + + +# MORE ------------ + +# make all labels lower case +labels <- layout_rename_ph_labels(x, "Comparison") +layout_rename_ph_labels(x, "Comparison") <- tolower(labels) + +# rename all labels to type [type_idx] +lp <- layout_properties(x, "Comparison") +layout_rename_ph_labels(x, "Comparison") <- paste0(lp$type, " [", lp$type_idx, "]") + +# rename duplicated placeholders (see also `layout_dedupe_ph_labels()`) +file <- system.file("doc_examples", "ph_dupes.pptx", package = "officer") +x <- read_pptx(file) +lp <- layout_properties(x, "2-dupes") +idx <- which(lp$ph_label == "Content 7") # exists twice +layout_rename_ph_labels(x, "2-dupes")[idx] <- paste("DUPLICATE", seq_along(idx)) + +# warning: in case of duped labels only the first occurrence is renamed +x <- read_pptx(file) +layout_rename_ph_labels(x, "2-dupes", "Content 7" = "new label") diff --git a/man/get_layout.Rd b/man/get_layout.Rd new file mode 100644 index 00000000..d754f347 --- /dev/null +++ b/man/get_layout.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/pptx_layout_helper.R +\name{get_layout} +\alias{get_layout} +\title{Layout selection helper} +\usage{ +get_layout(x, layout, master = NULL) +} +\arguments{ +\item{x}{An \code{rpptx} object.} + +\item{layout}{Layout name or index. Index refers to the row index of the \code{\link[=layout_summary]{layout_summary()}} +output.} + +\item{master}{Name of master. Only required if layout name is not unique across masters.} +} +\value{ +A \verb{} object, i.e. a list with the entries \code{index}, \code{layout_name}, +\code{layout_file}, \code{master_name}, \code{master_file}, and \code{slide_layout}. +} +\description{ +Select a layout by name or index. The master name is inferred and only required +for disambiguation in case the layout name is not unique across masters. +} +\keyword{internal} diff --git a/man/layout_rename_ph_labels.Rd b/man/layout_rename_ph_labels.Rd new file mode 100644 index 00000000..59f4b8cd --- /dev/null +++ b/man/layout_rename_ph_labels.Rd @@ -0,0 +1,104 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ppt_ph_rename_layout.R +\name{layout_rename_ph_labels} +\alias{layout_rename_ph_labels} +\alias{layout_rename_ph_labels<-} +\title{Change ph labels in a layout} +\usage{ +layout_rename_ph_labels(x, layout, master = NULL, ..., .dots = NULL) + +layout_rename_ph_labels(x, layout, master = NULL, id = NULL) <- value +} +\arguments{ +\item{x}{An \code{rpptx} object.} + +\item{layout}{Layout name or index. Index is the row index of \code{\link[=layout_summary]{layout_summary()}}.} + +\item{master}{Name of master. Only required if the layout name is not unique across masters.} + +\item{...}{Comma separated list of key-value pairs to rename phs. Either reference a ph via its label +(\code{"old label"} = \code{"new label"}) or its unique id (\code{"id"} = \code{"new label"}).} + +\item{.dots}{Provide a named list or vector of key-value pairs to rename phs +(\verb{list("old label"} = \code{"new label"}).} + +\item{id}{Unique placeholder id (see column \code{id} in \code{\link[=layout_properties]{layout_properties()}} or \code{\link[=plot_layout_properties]{plot_layout_properties()}}).} + +\item{value}{Not relevant for user. A pure technical necessity for rhs assignments.} +} +\value{ +Vector of renamed ph labels. +} +\description{ +There are two versions of the function. The first takes a set of key-value pairs to rename the +ph labels. The second uses a right hand side (rhs) assignment to specify the new ph labels. +See section \emph{Details}. \cr\cr +\emph{NB:} You can also rename ph labels directly in PowerPoint. Open the master template view +(\code{Alt} + \code{F10}) and go to \code{Home} > \code{Arrange} > \verb{Selection Pane}. +} +\details{ +\itemize{ +\item Note the difference between the terms \code{id} and \code{index}. Both can be found in the output of +\code{\link[=layout_properties]{layout_properties()}}. The unique ph \code{id} is found in column \code{id}. The \code{index} refers to the +index of the data frame row. +\item In a right hand side (rhs) label assignment (\verb{<- new_labels}), there are two ways to +optionally specify a subset of phs to rename. In both cases, the length of the rhs vector +(the new labels) must match the length of the id or index: +\enumerate{ +\item use the \code{id} argument to specify ph ids to rename: \code{layout_rename_ph_labels(..., id = 2:3) <- new_labels} +\item use an \code{index} in squared brackets: \code{layout_rename_ph_labels(...)[1:2] <- new_labels} +} +} +} +\examples{ +x <- read_pptx() + +# INFO ------------- + +# Returns layout's ph_labels by default in same order as layout_properties() +layout_rename_ph_labels(x, "Comparison") +layout_properties(x, "Comparison")$ph_label + + +# BASICS ----------- +# +# HINT: run `plot_layout_properties(x, "Comparison")` to see how labels change + +# rename using key-value pairs: 'old label' = 'new label' or 'id' = 'new label' +layout_rename_ph_labels(x, "Comparison", "Title 1" = "LABEL MATCHED") # label matching +layout_rename_ph_labels(x, "Comparison", "3" = "ID MATCHED") # id matching +layout_rename_ph_labels(x, "Comparison", "Date Placeholder 6" = "DATE", "8" = "FOOTER") # label, id + +# rename using a named list and the .dots arg +renames <- list("Content Placeholder 3" = "CONTENT_1", "6" = "CONTENT_2") +layout_rename_ph_labels(x, "Comparison", .dots = renames) + +# rename via rhs assignment and optional index (not id!) +layout_rename_ph_labels(x, "Comparison") <- LETTERS[1:8] +layout_rename_ph_labels(x, "Comparison")[1:3] <- paste("CHANGED", 1:3) + +# rename via rhs assignment and ph id (not index) +layout_rename_ph_labels(x, "Comparison", id = c(2, 4)) <- paste("ID =", c(2, 4)) + + +# MORE ------------ + +# make all labels lower case +labels <- layout_rename_ph_labels(x, "Comparison") +layout_rename_ph_labels(x, "Comparison") <- tolower(labels) + +# rename all labels to type [type_idx] +lp <- layout_properties(x, "Comparison") +layout_rename_ph_labels(x, "Comparison") <- paste0(lp$type, " [", lp$type_idx, "]") + +# rename duplicated placeholders (see also `layout_dedupe_ph_labels()`) +file <- system.file("doc_examples", "ph_dupes.pptx", package = "officer") +x <- read_pptx(file) +lp <- layout_properties(x, "2-dupes") +idx <- which(lp$ph_label == "Content 7") # exists twice +layout_rename_ph_labels(x, "2-dupes")[idx] <- paste("DUPLICATE", seq_along(idx)) + +# warning: in case of duped labels only the first occurrence is renamed +x <- read_pptx(file) +layout_rename_ph_labels(x, "2-dupes", "Content 7" = "new label") +} diff --git a/tests/testthat/docs_dir/test-no-layouts.pptx b/tests/testthat/docs_dir/test-no-layouts.pptx new file mode 100644 index 00000000..a5ea3dee Binary files /dev/null and b/tests/testthat/docs_dir/test-no-layouts.pptx differ diff --git a/tests/testthat/test-get-layout-helper.R b/tests/testthat/test-get-layout-helper.R new file mode 100644 index 00000000..853ad65c --- /dev/null +++ b/tests/testthat/test-get-layout-helper.R @@ -0,0 +1,86 @@ +test_that("get_layout works as expected", { + opts <- options(cli.num_colors = 1) # suppress colors for error message check + on.exit(options(opts)) + + # all layouts unique + x <- read_pptx() + + expect_error(get_layout(x, "Title Slide", "xxx"), 'master "xxx" does not exist') + expect_error(get_layout(x, "xxx", "Office Theme"), 'Layout "xxx" does not exist in master "Office Theme"') + + expect_error(get_layout(x, 0), "Layout index out of bounds.") + expect_error(get_layout(x, 10), "Layout index out of bounds.") + expect_error(get_layout(x, Inf), "Layout index out of bounds.") + + expect_no_error(l1 <- get_layout(x, "Title Slide")) + expect_s3_class(l1, "layout_info") + expect_no_error(l2 <- get_layout(x, "Title Slide", "Office Theme")) + expect_s3_class(l2, "layout_info") + + expect_no_error(la <- get_layout(x, 1)) + expect_equal(la$index, 1) + + # same layout in several masters + file <- test_path("docs_dir", "test-three-identical-masters.pptx") + x <- read_pptx(file) + + expect_error(get_layout(x, "xxx", NULL), 'Layout "xxx" does not exist') + expect_error(get_layout(x, "xxx", "Master_1"), 'Layout "xxx" does not exist in master "Master_1"') + expect_error(get_layout(x, "Title Slide"), "Layout exists in more than one master") + expect_error(get_layout(x, "Title Slide", "xxx"), 'master "xxx" does not exist') + + expect_no_error(la <- get_layout(x, "Title Slide", "Master_1")) + expect_s3_class(la, "layout_info") + + expect_no_error(la <- get_layout(x, 1)) + expect_equal(la$index, 1) +}) + + +test_that("incorrect inputs are detected", { + opts <- options(cli.num_colors = 1) # suppress colors for error message check + on.exit(options(opts)) + + x <- read_pptx() + layout <- "Comparison" + + expect_error(get_layout("no rpptx object", layout), "Incorrect input for `x`") + + # incorrect layout arg input + error_msg <- "`layout` must be or " + expect_error(get_layout(x, NA), error_msg) + expect_error(get_layout(x, NULL), error_msg) + expect_error(get_layout(x, mtcars), error_msg) + + # out of bound index + error_msg <- "Layout index out of bounds" + expect_error(get_layout(x, 0), error_msg) + expect_error(get_layout(x, -1), error_msg) + expect_error(get_layout(x, 8), error_msg) + expect_error(get_layout(x, Inf), error_msg) + + # non existing layout + expect_error(get_layout(x, "xxx"), 'Layout "xxx" does not exist.') + expect_error(get_layout(x, ""), 'Layout "" does not exist.') + + # not exactly one layout + error_msg <- "`layout` is not length 1" + expect_error(get_layout(x, c("a", "b")), error_msg) + expect_error(get_layout(x, 1:2), error_msg) + expect_error(get_layout(x, integer()), error_msg) + expect_error(get_layout(x, character()), error_msg) + + # layout not unique + file <- test_path("docs_dir", "test-three-identical-masters.pptx") + x <- read_pptx(file) + expect_error(get_layout(x, "Title Slide"), "Layout exists in more than one master") +}) + + +test_that(" prints correctly", { + x <- read_pptx() + layout <- "Comparison" + l <- get_layout(x, layout) + out <- capture.output(print(l)) + expect_equal(length(out), length(l)) +}) diff --git a/tests/testthat/test-pptx-rename-ph-labels.R b/tests/testthat/test-pptx-rename-ph-labels.R new file mode 100644 index 00000000..6f201ea5 --- /dev/null +++ b/tests/testthat/test-pptx-rename-ph-labels.R @@ -0,0 +1,139 @@ + +test_that("ph_labels with same order as in layout_properties()", { + x <- read_pptx() + l1 <- layout_rename_ph_labels(x, "Comparison") + l2 <- layout_properties(x, "Comparison")$ph_label + expect_equal(l1, l2) +}) + + +test_that("incorrect inputs are detected", { + opts <- options(cli.num_colors = 1) # suppress colors for error message check + on.exit(options(opts)) + + x <- read_pptx() + layout <- "Comparison" + + # unnamed args in renaming (dots) + error_msg <- "Unnamed arguments are not allowed." + expect_error(layout_rename_ph_labels(x, layout, NULL, "xxxx"), error_msg) + expect_error(layout_rename_ph_labels(x, layout, .dots = list("xxxx")), error_msg) + expect_error(layout_rename_ph_labels(x, layout, NULL, "xxx", .dots = list(a = "xxxx")), error_msg) + expect_error(layout_rename_ph_labels(x, layout, "xxx" = "a", .dots = list("xxxx")), error_msg) + + # unknown labels + error_msg <- "Can't rename labels that don't exist." + expect_error(layout_rename_ph_labels(x, layout, "xxxx" = "..."), error_msg) + expect_error(layout_rename_ph_labels(x, layout, "xxxx" = "...", "yyy" = "..."), error_msg) + expect_error(layout_rename_ph_labels(x, layout, .dots = list("xxxx" = "...")), error_msg) + + # unknown ids + error_msg <- "Can't rename ids that don't exist." + expect_error(layout_rename_ph_labels(x, layout, "1" = "..."), error_msg) + expect_error(layout_rename_ph_labels(x, layout, "1" = "...", "0" = "..."), error_msg) + expect_error(layout_rename_ph_labels(x, layout, .dots = list("1" = "...")), error_msg) + + # duplicate rename entries + error_msg <- "Each id or label must only have one rename entry only." + expect_error(layout_rename_ph_labels(x, layout, "Title 1" = "a", "Title 1" = "b"), error_msg) + expect_error(layout_rename_ph_labels(x, layout, "2" = "a", "2" = "b"), error_msg) + expect_error(layout_rename_ph_labels(x, layout, .dots = list("Title 1" = "a", "Title 1" = "b")), error_msg) + expect_error(layout_rename_ph_labels(x, layout, "2" = "a", .dots = list("2" = "b")), error_msg) + + # label and id collision + error_msg <- "Either specify the label OR the id of the ph to rename, not both." + expect_error(layout_rename_ph_labels(x, layout, "Title 1" = "a", "2" = "b"), error_msg) + expect_error(layout_rename_ph_labels(x, layout, "Title 1" = "a", "2" = "b"), error_msg) + expect_error(layout_rename_ph_labels(x, layout, .dots = list("Title 1" = "a", "2" = "b")), error_msg) + expect_error(layout_rename_ph_labels(x, layout, "Date Placeholder 6" = "a", "7" = "b", .dots = list("Title 1" = "a", "2" = "b")), error_msg) +}) + + +test_that("ph renaming works as expected", { + opts <- options(cli.num_colors = 1) # suppress colors for error message check + on.exit(options(opts)) + + x <- read_pptx() + layout <- "Comparison" + + # rename using key-value pairs: 'old label' = 'new label' or 'id' = 'new label' + layout_rename_ph_labels(x, layout, "Title 1" = "LABEL MATCHED") # label matching + layout_rename_ph_labels(x, layout, "3" = "ID MATCHED") # id matching + layout_rename_ph_labels(x, layout, "Date Placeholder 6" = "DATE", "8" = "FOOTER") # label and id + layout_properties(x, layout)$ph_label + + x <- read_pptx() + idx <- c(1, 2, 6, 7) + l <- list("Date Placeholder 6" = "idx_6", "8" = "idx_7", "Title 1" = "idx_1", "3" = "idx_2") # as list + layout_rename_ph_labels(x, layout, .dots = l) + expect_equal(layout_properties(x, layout)$ph_label[idx], paste0("idx_", idx)) + + x <- read_pptx() + l <- c("Date Placeholder 6" = "idx_6", "8" = "idx_7", "Title 1" = "idx_1", "3" = "idx_2") # as vector + layout_rename_ph_labels(x, layout, .dots = l) + expect_equal(layout_properties(x, layout)$ph_label[idx], paste0("idx_", idx)) + + x <- read_pptx() + l <- list("Date Placeholder 6" = "idx_6", "3" = "idx_2") + layout_rename_ph_labels(x, layout, "8" = "idx_7", "Title 1" = "idx_1", .dots = l) # mix ... and .dots + expect_equal(layout_properties(x, layout)$ph_label[idx], paste0("idx_", idx)) + + # rename via rhs assignment and ph index (not id!) + x <- read_pptx() + rhs <- LETTERS[1:8] + layout_rename_ph_labels(x, layout) <- rhs + expect_equal(layout_properties(x, layout)$ph_label, rhs) + + rhs <- paste("CHANGED", 1:3) + ph_label_check <- layout_properties(x, layout)$ph_label + ph_label_check[1:3] <- rhs + layout_rename_ph_labels(x, layout)[1:3] <- rhs + expect_equal(layout_properties(x, layout)$ph_label, ph_label_check) + + # rename via rhs assignment and ph id (not index) + lp_old <- ph_label_check <- layout_properties(x, layout) + ids <- c(2, 4, 5) + idx <- match(ids, lp_old$id) # row in layout properties + rhs <- paste("ID =", ids) + ph_label_check <- lp_old$ph_label + ph_label_check[idx] <- rhs + layout_rename_ph_labels(x, layout, id = ids) <- rhs + lp_new <- layout_properties(x, layout) + expect_equal(lp_new$ph_label, ph_label_check) +}) + + +test_that("renaming duplicate labels replaces 1st occurrence only", { + opts <- options(cli.num_colors = 1) # suppress colors for error message check + on.exit(options(opts)) + + file <- test_path("docs_dir", "test-pptx-dedupe-ph.pptx") + x <- read_pptx(file) + + # rename first label occurrence only and issue warning (1 duped label) + layout <- "2-dupes" + ph_label_check <- layout_properties(x, layout)$ph_label + idx <- which(ph_label_check == "Content 7") # exists twice + new_value <- "xxxx" + warn_msg <- "When renaming a label with duplicates, only the first occurrence is renamed." + expect_warning(layout_rename_ph_labels(x, layout, "Content 7" = new_value), warn_msg) + ph_label_new <- layout_properties(x, layout)$ph_label + ph_label_check[idx[1]] <- new_value # only first occurrence is replaced + expect_equal(ph_label_check, ph_label_new) + + # rename first label occurrence only and issue warning (2 duped labels) + layout <- "2x2-dupes" + ph_label_check <- layout_properties(x, layout)$ph_label + ii <- which(duplicated(ph_label_check, fromLast = TRUE)) # index of 1st occurrence + dupes <- ph_label_check[ii] + vals <- LETTERS[seq_along(dupes)] + names(vals) <- dupes + warn_msg_1 <- "When renaming a label with duplicates, only the first occurrence is renamed." + warn_msg_2 <- "Renaming 2 ph labels with duplicates" + warn_msg <- paste0(warn_msg_1, ".*", warn_msg_2) + expect_warning(layout_rename_ph_labels(x, layout, .dots = vals), warn_msg) + ph_label_check[ii] <- vals + ph_label_new <- layout_properties(x, layout)$ph_label + expect_equal(ph_label_check, ph_label_new) +}) + diff --git a/tests/testthat/utils.R b/tests/testthat/utils.R index e6b2edab..321f6a7d 100644 --- a/tests/testthat/utils.R +++ b/tests/testthat/utils.R @@ -25,6 +25,3 @@ has_css_attr <- function(x, atname, value){ reg <- sprintf("%s:%s", atname, value) grepl(reg, css) } - - -