diff --git a/.Rbuildignore b/.Rbuildignore index 702baaf9e5..6b6f058429 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -35,3 +35,4 @@ tests/testthat/test-tab_options.R tests/testthat/test-tab_spanner.R tests/testthat/test-tab_spanner_delim.R tests/testthat/test-table_parts.R +tests/testthat/test-as_word.R diff --git a/DESCRIPTION b/DESCRIPTION index d568dccf1d..9926c40f69 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -15,6 +15,8 @@ Authors@R: c( person("Joe", "Cheng", , "joe@rstudio.com", "aut"), person("Barret", "Schloerke", , "barret@rstudio.com", "aut", comment = c(ORCID = "0000-0001-9986-114X")), + person("Ellis", "Hughes", , "ellis.h.hughes@gsk.com", "aut", + comment = c(ORCID = "0000-0003-0637-4436")), person("RStudio", role = c("cph", "fnd")) ) License: MIT + file LICENSE @@ -111,10 +113,11 @@ Collate: 'utils_general_str_formatting.R' 'utils_pipe.R' 'utils_render_common.R' - 'utils_render_footnotes.R' 'utils_render_html.R' 'utils_render_latex.R' 'utils_render_rtf.R' + 'utils_render_xml.R' + 'z_utils_render_footnotes.R' 'zzz.R' Config/testthat/edition: 3 Config/testthat/parallel: true diff --git a/NAMESPACE b/NAMESPACE index 4988f1c4eb..57056c6f04 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -8,6 +8,7 @@ export(adjust_luminance) export(as_latex) export(as_raw_html) export(as_rtf) +export(as_word) export(cell_borders) export(cell_fill) export(cell_text) @@ -121,6 +122,7 @@ importFrom(dplyr,vars) importFrom(ggplot2,ggsave) importFrom(htmltools,css) importFrom(magrittr,"%>%") +importFrom(rlang,`%||%`) importFrom(tidyselect,contains) importFrom(tidyselect,ends_with) importFrom(tidyselect,everything) diff --git a/NEWS.md b/NEWS.md index 958de5c143..0b1a1687e3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # gt (development version) +* word table output via `as_word()`. (#929) + # gt 0.6.0 ## New features diff --git a/R/export.R b/R/export.R index a1af58e45c..93ded84c38 100644 --- a/R/export.R +++ b/R/export.R @@ -548,6 +548,180 @@ as_rtf <- function(data) { rtf_table } +#' Output a **gt** object as Word +#' +#' @description +#' Get the Open Office XML table tag content from a `gt_tbl` object as as a +#' single-element character vector. +#' +#' @param data A table object that is created using the `gt()` function. +#' @param align left, center (default) or right. +#' @param caption_location top (default), bottom, or embed Indicating if the +#' title and subtitle should be listed above, below, or be embedded in the +#' table +#' @param caption_align left (default), center, or right. Alignment of caption +#' (title and subtitle). Used when `caption_location` is not "embed". +#' @param split TRUE or FALSE (default) indicating whether activate Word option +#' 'Allow row to break across pages'. +#' @param keep_with_next TRUE (default) or FALSE indicating whether a table +#' should use Word option 'keep rows together' is activated when TRUEd +#' +#' @examples +#' # Use `gtcars` to create a gt table; +#' # add a header and then export as +#' # OOXML code for Word +#' tab_rtf <- +#' gtcars %>% +#' dplyr::select(mfr, model) %>% +#' dplyr::slice(1:2) %>% +#' gt() %>% +#' tab_header( +#' title = md("Data listing from **gtcars**"), +#' subtitle = md("`gtcars` is an R dataset") +#' ) %>% +#' as_word() +#' +#' @family Export Functions +#' @section Function ID: +#' 13-5 +#' +#' @export +as_word <- function( + data, + align = "center", + caption_location = c("top","bottom","embed"), + caption_align = "left", + split = FALSE, + keep_with_next = TRUE +) { + + # Perform input object validation + stop_if_not_gt(data = data) + caption_location <- match.arg(caption_location) + + # Build all table data objects through a common pipeline + value <- build_data(data = data, context = "word") + + gt_xml <- c() + + # Composition of Word table OOXML ----------------------------------------------- + if (caption_location %in% c("top")) { + header_xml <- as_word_tbl_header_caption(data = value, align = caption_align, split = split, keep_with_next = keep_with_next) + gt_xml <- c(gt_xml, header_xml) + } + + tbl_xml <- as_word_tbl_body(data = value, align = align, split = split, keep_with_next = keep_with_next, embedded_heading = identical(caption_location, "embed")) + gt_xml <- c(gt_xml, tbl_xml) + + + if (caption_location %in% c("bottom")) { + ## set keep_with_next to false here to prevent it trying to keep with non-table content + header_xml <- as_word_tbl_header_caption(data = value, align = caption_align, split = split, keep_with_next = FALSE) + gt_xml <- c(gt_xml, header_xml) + } + + gt_xml <- paste0(gt_xml, collapse = "") + + gt_xml + +} + +#' Generate ooxml for the table caption +#' +#' @param data A processed table object that is created using the `build_data()` function. +#' @param align left (default), center or right. +#' @param split TRUE or FALSE (default) indicating whether activate Word option 'Allow row to break across pages'. +#' @param keep_with_next TRUE (default) or FALSE indicating whether a table should use Word option 'keep rows +#' together' is activated when TRUE +#' +#' @noRd +as_word_tbl_header_caption <- function( + data, + align = "left", + split = FALSE, + keep_with_next = TRUE +) { + + # Perform input object validation + stop_if_not_gt(data = data) + + # Composition of caption OOXML ----------------------------------------------- + + # Create the table caption + caption_xml <- + create_table_caption_component_xml( + data = data, + align = align, + keep_with_next = keep_with_next + ) + + caption_xml +} + +#' Generate ooxml for the table body +#' +#' @param data A processed table object that is created using the `build_data()` +#' function. +#' @param align left, center (default) or right. +#' @param split TRUE or FALSE (default) indicating whether activate Word option +#' 'Allow row to break across pages'. +#' @param keep_with_next TRUE (default) or FALSE indicating whether a table +#' should use Word option 'keep rows together' is activated when TRUE +#' @param embedded_heading TRUE or FALSE (default) indicating whether a table +#' should add the title and subtitle at the top of the table. +#' +#' @noRd +as_word_tbl_body <- function( + data, + align = "center", + split = FALSE, + keep_with_next = TRUE, + embedded_heading = FALSE +) { + + # Perform input object validation + stop_if_not_gt(data = data) + + # Composition of table Word OOXML ----------------------------------------------- + + # Create the table properties component + table_props_component <- create_table_props_component_xml(data = data, align = align) + + # # Create the heading component + if (embedded_heading) { + heading_component <- create_heading_component_xml(data = data, split = split, keep_with_next = keep_with_next) + } else { + heading_component <- NULL + } + + # Create the columns component + columns_component <- create_columns_component_xml(data = data, split = split, keep_with_next = keep_with_next) + + # Create the body component + body_component <- create_body_component_xml(data = data, split = split, keep_with_next = keep_with_next) + + # Create the footnotes component + footnotes_component <- create_footnotes_component_xml(data = data, split = split, keep_with_next = keep_with_next) + + # Create the source notes component + source_notes_component <- create_source_notes_component_xml(data = data, split = split, keep_with_next = keep_with_next) + + # Compose the Word OOXML table + word_tbl <- + xml_tbl( + paste0( + table_props_component, + heading_component, + columns_component, + body_component, + footnotes_component, + source_notes_component, + collapse = "" + ) + ) + + as.character(word_tbl) +} #' Extract a summary list from a **gt** object #' @@ -610,7 +784,7 @@ as_rtf <- function(data) { #' #' @family Export Functions #' @section Function ID: -#' 13-5 +#' 13-6 #' #' @export extract_summary <- function(data) { diff --git a/R/format_data.R b/R/format_data.R index f7e9a1539d..88ce7f0eb2 100644 --- a/R/format_data.R +++ b/R/format_data.R @@ -2792,6 +2792,9 @@ fmt_markdown <- function( rtf = function(x) { markdown_to_rtf(x) }, + word = function(x) { + markdown_to_xml(x) + }, default = function(x) { vapply( x, diff --git a/R/print.R b/R/print.R index 3523fedb24..82221ca08a 100644 --- a/R/print.R +++ b/R/print.R @@ -22,6 +22,11 @@ knitr_is_rtf_output <- function() { "rtf" %in% knitr::opts_knit$get("rmarkdown.pandoc.to") } +knitr_is_word_output <- function() { + + "word_document" %in% rmarkdown::all_output_formats(knitr::current_input()) +} + #' Knit print the table #' #' This facilitates printing of the HTML table within a knitr code chunk. @@ -34,10 +39,21 @@ knitr_is_rtf_output <- function() { knit_print.gt_tbl <- function(x, ...) { if (knitr_is_rtf_output()) { + x <- as_rtf(x) + } else if (knitr::is_latex_output()) { + x <- as_latex(x) + + } else if (knitr_is_word_output()) { + + x <- + paste("```{=openxml}", as_word(x), "```\n\n", sep = "\n") %>% + knitr::asis_output() + } else { + # Default to HTML output x <- as.tags.gt_tbl(x, ...) } diff --git a/R/utils.R b/R/utils.R index 6e87804236..7b7b83d76e 100644 --- a/R/utils.R +++ b/R/utils.R @@ -427,6 +427,20 @@ process_text <- function(text, context = "html") { return(text) } + + } else if (context == "word") { + + # Text processing for Word output + + if (inherits(text, "from_markdown")) { + + return(markdown_to_xml(text)) + + } else { + + return(as.character(text)) + } + } else { # Text processing in the default case @@ -522,8 +536,237 @@ markdown_to_latex <- function(text) { ) } -cmark_rules <- list( +markdown_to_xml <- function(text) { + + text <- + text %>% + as.character() %>% + vapply( + FUN.VALUE = character(1), + USE.NAMES = FALSE, + FUN = commonmark::markdown_xml + ) %>% + vapply( + FUN.VALUE = character(1), + USE.NAMES = FALSE, + FUN = function(cmark) { + # cat(cmark) + x <- xml2::read_xml(cmark) + if (!identical(xml2::xml_name(x), "document")) { + stop("Unexpected result from markdown parsing: `document` element not found") + } + + children <- xml2::xml_children(x) + + if (length(children) == 1 && + xml2::xml_type(children[[1]]) == "element" && + xml2::xml_name(children[[1]]) == "paragraph") { + children <- xml2::xml_children(children[[1]]) + } + + apply_rules <- function(x) { + + if (inherits(x, "xml_nodeset")) { + + len <- length(x) + results <- character(len) # preallocate vector + + for (i in seq_len(len)) { + results[[i]] <- apply_rules(x[[i]]) + } + + # TODO: is collapse = "" correct? + xml_raw(paste0("", results, collapse = "")) + + } else { + + output <- if (xml2::xml_type(x) == "element") { + + rule <- cmark_rules_xml[[xml2::xml_name(x)]] + + if (is.null(rule)) { + + rlang::warn( + paste0("Unknown commonmark element encountered: ", xml2::xml_name(x)), + .frequency = "once", + .frequency_id = "gt_commonmark_unknown_element" + ) + + apply_rules(xml2::xml_contents(x)) + + } else if (is.function(rule)) { + + rule(x, apply_rules) + } + } + + xml_raw(paste0("", output, collapse = "")) + } + } + + apply_rules(children) + } + ) + + text +} + +# TODO: Make XML versions of these +cmark_rules_xml <- list( + + heading = function(x, process) { + + heading_sizes <- c(36, 32, 28, 24, 20, 16) + fs <- heading_sizes[as.numeric(xml2::xml_attr(x, attr = "level"))] + + htmltools::tagList( + xml_sz(process(xml2::xml_children(x)), val = fs) + ) + }, + thematic_break = function(x, process) { + " + + " + }, + link = function(x, process) { + # NOTE: Links are difficult to insert in OOXML documents because + # a relationship must be provided in the 'document.xml.rels' file + xml2::xml_text(x) + }, + list = function(x, process) { + + type <- xml2::xml_attr(x, attr = "type") + n_items <- length(xml2::xml_children(x)) + + # NOTE: `start`, `delim`, and `tight` attrs are ignored; we also + # assume there is only `type` values of "ordered" and "bullet" (unordered) + htmltools::HTML( + paste( + vapply( + seq_len(n_items), + FUN.VALUE = character(1), + USE.NAMES = FALSE, + FUN = function(n) { + + paste( + ifelse(type == "bullet", "\u2022", ""), + process(xml2::xml_children(x)[n]), + collapse = "" + ) + } + ), + collapse = "" + ) + ) + }, + item = function(x, process) { + # TODO: probably needs something like process_children() + xml2::xml_text(x) + }, + code_block = function(x, process) { + htmltools::tagList( + xml_rPr(xml_r_font(ascii_font = "Courier", ansi_font = "Courier")), + xml_t(xml2::xml_text(x), xml_space = "preserve"), + xml_rPr(xml_r_font(ascii_font = "Calibri", ansi_font = "Calibri")) + ) + }, + html_inline = function(x, process) { + + # TODO: make this work for XML + + tag <- xml2::xml_text(x) + + match <- stringr::str_match(tag, pattern = "^<(/?)([a-zA-Z0-9\\-]+)") + + if (!is.na(match[1, 1])) { + + span_map <- + c( + sup = "super", + sub = "sub", + strong = "b", + b = "b", + em = "i", + i = "i", + code = "f1" + ) + + key_map <- c(br = "line") + + is_closing <- match[1, 2] == "/" + tag_name <- match[1, 3] + + if (!is_closing) { + + if (tag_name %in% names(key_map)) { + + return(rtf_key(key_map[tag_name], space = TRUE)) + + } else if (tag_name %in% names(span_map)) { + + return( + rtf_paste0( + rtf_raw("{"), + rtf_key(span_map[tag_name], space = TRUE) + ) + ) + } + + } else { + + if (tag_name %in% names(span_map)) { + return(rtf_raw("}")) + } + } + } + + # Any unrecognized HTML tags are stripped, returning nothing + return(rtf_raw("")) + }, + softbreak = function(x, process) { + "\n " + }, + linebreak = function(x, process) { + "" + }, + block_quote = function(x, process) { + # TODO: Implement + process(xml2::xml_children(x)) + }, + code = function(x, process) { + htmltools::tagList( + xml_rPr(xml_r_font(ascii_font = "Courier", ansi_font = "Courier")), + xml_t(xml2::xml_text(x), xml_space = "preserve"), + xml_rPr(xml_r_font(ascii_font = "Calibri", ansi_font = "Calibri")) + ) + }, + strong = function(x, process) { + htmltools::HTML( + paste0( + xml_rPr(xml_b(active = TRUE)), + as.character(process(xml2::xml_children(x))), + xml_rPr(xml_b(active = FALSE)) + ) + ) + }, + emph = function(x, process) { + htmltools::HTML( + paste0( + xml_rPr(xml_i(active = TRUE)), + as.character(process(xml2::xml_children(x))), + xml_rPr(xml_i(active = FALSE)) + ) + ) + }, + text = function(x, process) { + xml2::xml_text(x) + }, + paragraph = function(x, process) { + xml2::xml_text(x) + } +) +cmark_rules_rtf <- list( heading = function(x, process) { heading_sizes <- c(36, 32, 28, 24, 20, 16) fs <- heading_sizes[as.numeric(xml2::xml_attr(x, attr = "level"))] @@ -725,7 +968,7 @@ markdown_to_rtf <- function(text) { } else { output <- if (xml2::xml_type(x) == "element") { - rule <- cmark_rules[[xml2::xml_name(x)]] + rule <- cmark_rules_rtf[[xml2::xml_name(x)]] if (is.null(rule)) { rlang::warn( paste0("Unknown commonmark element encountered: ", xml2::xml_name(x)), diff --git a/R/utils_formatters.R b/R/utils_formatters.R index 08f31f3742..a8aae6a9e6 100644 --- a/R/utils_formatters.R +++ b/R/utils_formatters.R @@ -461,6 +461,16 @@ context_missing_text <- function(missing_text, context) { } else { process_text(missing_text, context) } + }, + word = + { + if (!inherits(missing_text, "AsIs") && missing_text == "---") { + "\u2014" + } else if (!inherits(missing_text, "AsIs") && missing_text == "--") { + "\u2013" + } else { + process_text(missing_text, context) + } } ) } @@ -498,6 +508,15 @@ context_plusminus_mark <- function(plusminus_mark, context) { } else { plusminus_mark } + }, + word = + { + if (!inherits(plusminus_mark, "AsIs") && + plusminus_mark == " +/- ") { + " +/- " + } else { + plusminus_mark + } } ) } @@ -645,6 +664,7 @@ context_exp_marks <- function(context) { html = c(" × 10", ""), latex = c(" \\times 10^{", "}"), rtf = c(" \\'d7 10{\\super ", "}"), + word = c(" x 10^", ""), c(" x 10(", ")") ) } diff --git a/R/utils_render_common.R b/R/utils_render_common.R index c7e7568e49..f0f2fd97fc 100644 --- a/R/utils_render_common.R +++ b/R/utils_render_common.R @@ -1,5 +1,5 @@ # Define the contexts -all_contexts <- c("html", "latex", "rtf", "default") +all_contexts <- c("html", "latex", "rtf", "word", "default") validate_contexts <- function(contexts) { diff --git a/R/utils_render_xml.R b/R/utils_render_xml.R new file mode 100644 index 0000000000..b75cba6e87 --- /dev/null +++ b/R/utils_render_xml.R @@ -0,0 +1,1931 @@ +# +# XML tag functions +# + +xml_tag_type <- function(tag_name, app) { + paste0(substring(app, 1, 1), ":", tag_name) +} + +# Table +xml_tbl <- function(..., + app = "word") { + htmltools::tag( + `_tag_name` = xml_tag_type("tbl", app), + + varArgs = list( + "xmlns:w"="http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "xmlns:wp"="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "xmlns:r"="http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "xmlns:w14"="http://schemas.microsoft.com/office/word/2010/wordml", + htmltools::HTML(paste0(...))) + ) +} + +# Table properties +xml_tblPr <- function(..., + app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("tblPr", app), + varArgs = list(htmltools::HTML(paste0(...))) + ) +} + +# Table width (child of `tblPr`) +xml_tblW <- function(type = c("pct", "auto", "dxa", "nil"), + w = "100%", + app = "word") { + + type <- match.arg(type) + + htmltools::tag( + `_tag_name` = xml_tag_type("tblW", app), + varArgs = list( + `w:type` = type, + `w:w` = w + ) + ) +} + +# Table look (child of `tblPr`) +xml_tblLook <- function(first_row = "0", + last_row = "0", + first_column = "0", + last_column = "0", + no_h_band = "0", + no_v_band = "0", + app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("tblLook", app), + varArgs = list( + `w:firstRow` = first_row, + `w:lastRow` = last_row, + `w:firstColumn` = first_column, + `w:lastColumn` = last_column, + `w:noHBand` = no_h_band, + `w:noVBand` = no_v_band + ) + ) +} + +# Table style (child of `tblPr`) +xml_tblStyle <- function(val = "Table", + app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("tblStyle", app), + varArgs = list(`w:val` = val) + ) +} + +# Table cell margins +xml_tbl_cell_margins <- function(..., + app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("tblCellMar", app), + varArgs = list(htmltools::HTML(paste0(...))) + ) +} + +# Width specifiers (child of `tblCellMar` or `tcMar`) +xml_width <- function(dir = c("top", "bottom", "left", "right"), + width = 0, + type = c("dxa", "nil"), + app = "word") { + + dir <- match.arg(dir) + type <- match.arg(type) + + dir <- + switch( + dir, + left = "start", + right = "end", + top = "top", + bottom = "bottom" + ) + + htmltools::tag( + `_tag_name` = xml_tag_type(dir, app), + varArgs = list( + `w:w` = width, + `w:type` = type + ) + ) +} + +# Table grid +xml_tblGrid <- function(..., + app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("tblGrid", app), + varArgs = list(htmltools::HTML(paste0(...))) + ) +} + +# Table row +xml_tr <- function(..., + app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("tr", app), + varArgs = list( + htmltools::HTML(paste0(...)) + ) + ) +} + +# Table row properties +xml_trPr <- function(..., + app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("trPr", app), + varArgs = list(htmltools::HTML(paste0(...))) + ) +} + +# Table row height +xml_tr_height <- function(h_rule = c("auto", "exact", "atLeast"), + height_twips = "150", + app = "word") { + + h_rule <- match.arg(h_rule) + + htmltools::tag( + `_tag_name` = xml_tag_type("trHeight", app), + varArgs = list( + `w:hRule` = h_rule, + `w:val` = height_twips + ) + ) +} + +# Indicator of row header +xml_tbl_header <- function(app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("tblHeader", app), + varArgs = list() + ) +} + +# Table cell +xml_tc <- function(..., app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("tc", app), + varArgs = list(htmltools::HTML(paste0(...))) + ) +} + +# Table cell properties +xml_tcPr <- function(..., app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("tcPr", app), + varArgs = list(htmltools::HTML(paste0(...))) + ) +} + +# Table cell margins (child of `tcPr`) +xml_tc_margins <- function(..., + app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("tcMar", app), + varArgs = list(htmltools::HTML(paste0(...))) + ) +} + +# Vertical alignment of paragraph in cell (child of `tcPr`) +xml_v_align <- function(v_align = c("center", "bottom", "top"), app = "word") { + + v_align <- match.arg(v_align) + + htmltools::tag( + `_tag_name` = xml_tag_type("vAlign", app), + varArgs = list(`w:val` = v_align) + ) +} + +# Span cells horizontally (child of `tcPr`) +xml_gridSpan <- function(val = "1", app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("gridSpan", app), + varArgs = list(`w:val` = val) + ) +} + +# Span cells vertically (child of `tcPr`) +xml_v_merge <- function(val = c("restart", "continue"), app = "word") { + + val <- match.arg(val) + + htmltools::tag( + `_tag_name` = xml_tag_type("vMerge", app), + varArgs = list(`w:val` = val) + ) +} + +# Table cell borders +xml_tc_borders <- function(..., app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("tcBorders", app), + varArgs = list(htmltools::HTML(paste0(...))) + ) +} + +# Table cell border setting (child of `tcBorders`) +# The `size` is expressed in eighths of a point (min: 2, max: 96) +xml_border <- function(dir = c("top", "bottom", "left", "right"), + type = "single", + size = 2, + space = 0, + color = "D3D3D3", + app = "word") { + + dir <- match.arg(dir) + + dir <- + switch( + dir, + left = "start", + right = "end", + top = "top", + bottom = "bottom" + ) + + color <- toupper(gsub("#", "", color)) + + htmltools::tag( + `_tag_name` = xml_tag_type(dir, app), + varArgs = list( + `w:val` = type, + `w:sz` = size, + `w:space` = space, + `w:color` = color + ) + ) +} + +# Paragraph +xml_p <- function(..., app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("p", app), + varArgs = list( + htmltools::HTML(paste0(...)) + ) + ) +} + +# Paragraph with namespace defined +xml_p_ns <- function(..., app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("p", app), + varArgs = list( + "xmlns:w"="http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "xmlns:wp"="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "xmlns:r"="http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "xmlns:w14"="http://schemas.microsoft.com/office/word/2010/wordml", + htmltools::HTML(paste0(...)) + ) + ) +} + + +# Paragraph properties +xml_pPr <- function(..., app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("pPr", app), + varArgs = list(htmltools::HTML(paste0(...))) + ) +} + +# Paragraph style +xml_pStyle <- function(..., val = "Compact", app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("pStyle", app), + varArgs = list( + htmltools::HTML(paste0(...)), + `w:val` = val + ) + ) +} + +# Paragraph alignment +xml_jc <- function(val = c("left", "center", "right"), app = "word") { + + val <- match.arg(val) + + val <- + switch( + val, + left = "start", + right = "end", + center = "center" + ) + + htmltools::tag( + `_tag_name` = xml_tag_type("jc", app), + varArgs = list(`w:val` = val) + ) +} + +# Paragraph spacing +xml_spacing <- function(before = 120, after = 120, val = NULL, app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("spacing", app), + varArgs = list( + `w:before` = before, + `w:after` = after, + `w:val` = val + ) + ) +} + +# Text run +xml_r <- function(..., app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("r", app), + varArgs = list(htmltools::HTML(paste0(...))) + ) +} + +# Text run properties +xml_rPr <- function(..., app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("rPr", app), + varArgs = list(htmltools::HTML(paste0(...))) + ) +} + +# Font selection (child of `rPr`) +xml_r_font <- function(ascii_font = "Calibri", + ansi_font = "Calibri", + app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("rFonts", app), + varArgs = list( + `w:ascii` = ascii_font, + `w:hAnsi` = ansi_font + ) + ) +} + +# Font size in half points (child of `rPr`) +xml_sz <- function(val = 24, + app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("sz", app), + varArgs = list(`w:val` = val) + ) +} + +# Baseline adjustment of text (subscript, superscript) (child of `rPr`) +xml_baseline_adj <- function(v_align = c("superscript", "subscript", "baseline"), + app = "word") { + + v_align <- match.arg(v_align) + + htmltools::tag( + `_tag_name` = xml_tag_type("vertAlign", app), + varArgs = list(`w:val` = v_align) + ) +} + +# Literal text +xml_t <- function(..., + xml_space = c("default", "preserve"), + app = "word") { + + xml_space <- match.arg(xml_space) + + htmltools::tag( + `_tag_name` = xml_tag_type("t", app), + varArgs = list( + htmltools::HTML(paste0(...)), + `xml:space` = xml_space + ) + ) +} + +# Bold text specifier (toggle property) +xml_b <- function(active = TRUE, + app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("b", app), + varArgs = list(`w:val` = tolower(as.character(active))) + ) +} + +# Italics text specifier (toggle property) +xml_i <- function(active = TRUE, + app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("i", app), + varArgs = list(tolower(as.character(active))) + ) +} + +# Specification of text color +xml_color <- function(color = "D3D3D3", app = "word") { + + color <- toupper(gsub("#", "", color)) + + htmltools::tag( + `_tag_name` = xml_tag_type("color", app), + varArgs = list(`w:val` = color) + ) +} + +xml_shd <- function(fill = "auto", app = "word"){ + + fill <- toupper(gsub("#", "", fill)) + + htmltools::tag( + `_tag_name` = xml_tag_type("shd", app), + varArgs = list( + `w:val` = "clear", + `w:color` = "auto", + `w:fill` = fill + ) + ) +} + +# Text break +xml_br <- function(app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("br", app), + varArgs = list() + ) +} + +# Carriage return +xml_cr <- function(app = "word") { + + htmltools::tag( + `_tag_name` = xml_tag_type("cr", app), + varArgs = list() + ) +} + +# contents within the current cell shall be rendered on a single page +xml_cantSplit <- function(app = "word"){ + + htmltools::tag( + `_tag_name` = xml_tag_type("cantSplit", app), + varArgs = list() + ) +} + +# keep with Next +# contents of this paragraph are at least partly rendered on the same page +# as the following paragraph whenever possible +xml_keepNext <- function(app = "word"){ + + htmltools::tag( + `_tag_name` = xml_tag_type("keepNext", app), + varArgs = list() + ) +} + + +# Specifies the start or end of a complex field +xml_fldChar <- function(fldCharType = c("begin","separate","end"), dirty = TRUE, app = "word"){ + + fldCharType <- match.arg(fldCharType) + stopifnot(is_bool(dirty)) + + htmltools::tag( + `_tag_name` = xml_tag_type("fldChar", app), + varArgs = list( + `w:fldCharType` = fldCharType, + `w:dirty` = tolower(dirty)) + ) +} + +# field instructions for specify the codes for the fields. +# used within a fldChar's +xml_instrText <- function(instr, dirty = TRUE, app = "word"){ + htmltools::tag( + `_tag_name` = xml_tag_type("instrText", app), + varArgs = list( + `xml:space` = "preserve", + `w:dirty` = tolower(dirty), + instr), + ) +} + +# declares that the noProof property +xml_noProof <- function(app = "word"){ + htmltools::tag( + `_tag_name` = xml_tag_type("noProof", app), + varArgs = list() + ) +} + +# Add automatic table numbering +# Add components necessary for table auto-numbering +xml_table_autonum <- function(font = xml_r_font(), size = xml_sz(val = 24), app = "word"){ + + htmltools::tagList( + xml_r( + xml_rPr( + font, + size + ), + xml_t( + xml_space = "preserve", + "Table " + ) + ), + xml_r( + xml_fldChar(fldCharType = "begin") + ), + xml_r( + xml_instrText(" SEQ Table \\* ARABIC ") + ), + xml_r( + xml_fldChar(fldCharType = "separate") + ), + xml_r( + xml_rPr( + xml_noProof(), + font, + size + ), + xml_t(1) + ), + xml_r( + xml_fldChar(fldCharType = "end") + ), + xml_r( + xml_rPr( + font, + size + ), + xml_t( + ": ", + xml_space = "preserve" + ) + ) + ) +} + + +#' Transform a footnote mark to an XML representation +#' +#' @noRd +footnote_mark_to_xml <- function(mark) { + + as.character( + htmltools::tagList( + xml_rPr( + xml_baseline_adj(v_align = "superscript"), + xml_i(active = TRUE), + xml_t(mark), + xml_i(active = FALSE), + xml_baseline_adj(v_align = "baseline") + ) + ) + ) +} + +# Mark the given text as being XML, meaning, it should not be escaped if passed +# to xml_text +xml_raw <- function(...) { + text <- paste0(..., collapse = "") + class(text) <- "xml_text" + text +} + +# TODO: make table widths work for XML +# Get the attributes for the table tag +create_table_props_component_xml <- function(data, align = "center") { + + boxh <- dt_boxhead_get(data = data) + + # Get the `table-layout` value, which is set in `_options` + table_style <- + paste0( + "table-layout: ", + dt_options_get_value( + data = data, + option = "table_layout" + ), + ";" + ) + + doc_page_width <- getOption("gt.rtf_page_width") + + # In the case that column widths are not set for any columns, + # there should not be a `` tag requirement + # if (length(unlist(boxh$column_width)) < 1) { + # return(list(table_style = NULL, table_colgroups = NULL)) + # } + + # Get the table's width (which or may not have been set) + table_width <- + dt_options_get_value( + data = data, + option = "table_width" + ) + + widths <- + boxh %>% + dplyr::filter(type %in% c("default", "stub")) %>% + dplyr::arrange(dplyr::desc(type)) %>% # This ensures that the `stub` is first + .$column_width %>% + unlist() + + # Stop function if all length dimensions (where provided) + # don't conform to accepted CSS length definitions + validate_css_lengths(widths) + + # If all of the widths are defined as px values for all columns, + # then ensure that the width values are strictly respected as + # absolute width values (even if a table width has already been set) + if (all(grepl("px", widths)) && table_width == "auto") { + table_width <- "0px" + } + + if (all(grepl("%", widths)) && table_width == "auto") { + table_width <- "100%" + } + + if (table_width != "auto") { + table_style <- paste(table_style, paste0("width: ", table_width), sep = "; ") + } + + table_properties <- + xml_tblPr( + xml_tbl_cell_margins( + xml_width("top"), + xml_width("bottom"), + xml_width("left", width = 60), + xml_width("right", width = 60) + ), + xml_tblW(), + xml_tblLook(), + xml_jc(val = align) + ) + + htmltools::tagList(table_properties) +} + +#' Create the caption component of a table (OOXML) +#' +#' The table heading component contains the title and possibly a subtitle; if +#' there are no heading components defined this function will return an empty +#' string. +#' +#' @noRd +create_table_caption_component_xml <- function(data, align = "center", keep_with_next = TRUE) { + + # If there is no title or heading component, then return an empty string + if (!dt_heading_has_title(data = data)) { + return(c("")) + } + + heading <- dt_heading_get(data = data) + footnotes_tbl <- dt_footnotes_get(data = data) + styles_tbl <- dt_styles_get(data = data) + subtitle_defined <- dt_heading_has_subtitle(data = data) + + # Get table options + table_font_color <- dt_options_get_value(data, option = "table_font_color") + + # Get the footnote marks for the title + if ("title" %in% footnotes_tbl$locname) { + + footnote_title_marks <- + coalesce_marks( + fn_tbl = footnotes_tbl, + locname = "title" + ) + + footnote_title_marks <- + footnote_mark_to_xml(mark = footnote_title_marks$fs_id_c) + + } else { + footnote_title_marks <- "" + } + + title_caption <- as.character( + xml_p_ns( + xml_pPr( + xml_pStyle(val = "caption"), + xml_color(color = table_font_color), + xml_jc(val = align), + if(keep_with_next){xml_keepNext()} + ), + xml_table_autonum( + font = xml_r_font(), + size = xml_sz(val = 24) + ), + xml_r( + xml_rPr( + xml_r_font(), + xml_sz(val = 24) + ), + xml_t( + paste0(heading$title, footnote_title_marks) + ) + ) + ) + ) + + if(subtitle_defined){ + + # Get the footnote marks for the subtitle + if ("subtitle" %in% footnotes_tbl$locname) { + + footnote_subtitle_marks <- + coalesce_marks( + fn_tbl = footnotes_tbl, + locname = "subtitle" + ) + + footnote_subtitle_marks <- + footnote_mark_to_xml(mark = footnote_subtitle_marks$fs_id_c) + + } else { + footnote_subtitle_marks <- "" + } + + subtitle_caption <- as.character( + xml_p_ns( + xml_pPr( + xml_pStyle(val = "caption"), + xml_color(color = table_font_color), + xml_jc(val = align), + if(keep_with_next){xml_keepNext()} + ), + xml_r( + xml_rPr( + xml_r_font(), + xml_sz(val = 20) + ), + xml_t( + paste0(heading$subtitle, footnote_subtitle_marks), + xml_space = "preserve" + ) + ) + ) + ) + + title_caption <- c(title_caption, subtitle_caption) + + } + + + title_caption +} + + +#' Create the heading component of a table (OOXML) +#' +#' The table heading component contains the title and possibly a subtitle; if +#' there are no heading components defined this function will return an empty +#' string. +#' +#' @noRd +create_heading_component_xml <- function(data, split = FALSE, keep_with_next = TRUE) { + + # If there is no title or heading component, then return an empty string + if (!dt_heading_has_title(data = data)) { + return("") + } + + heading <- dt_heading_get(data = data) + footnotes_tbl <- dt_footnotes_get(data = data) + styles_tbl <- dt_styles_get(data = data) + stub_components <- dt_stub_components(data = data) + subtitle_defined <- dt_heading_has_subtitle(data = data) + + # Obtain the number of visible columns in the built table + n_data_cols <- length(dt_boxhead_get_vars_default(data = data)) + + # Determine whether the stub is available + stub_available <- dt_stub_components_has_rowname(stub_components) + + # If a stub is present then the effective number of columns increases by 1 + if (stub_available) { + n_cols <- n_data_cols + 1 + } else { + n_cols <- n_data_cols + } + + # Get table options + table_font_color <- dt_options_get_value(data, option = "table_font_color") + table_border_top_include <- dt_options_get_value(data, option = "table_border_top_include") + heading_border_bottom_color <- dt_options_get_value(data, option = "heading_border_bottom_color") + + + # Get the footnote marks for the title + if ("title" %in% footnotes_tbl$locname) { + + footnote_title_marks <- + coalesce_marks( + fn_tbl = footnotes_tbl, + locname = "title" + ) + + footnote_title_marks <- + footnote_mark_to_xml(mark = footnote_title_marks$fs_id_c) + + } else { + footnote_title_marks <- "" + } + + # Get the footnote marks for the subtitle + if (subtitle_defined && "subtitle" %in% footnotes_tbl$locname) { + + footnote_subtitle_marks <- + coalesce_marks( + fn_tbl = footnotes_tbl, + locname = "subtitle" + ) + + footnote_subtitle_marks <- + footnote_mark_to_xml(mark = footnote_subtitle_marks$fs_id_c) + + } else { + footnote_subtitle_marks <- "" + } + + + + title_html <- htmltools::tagList( + xml_t( + paste0(heading$title, footnote_title_marks) + ), + if (subtitle_defined) { + htmltools::tagList( + xml_br(), + xml_r( + xml_rPr( + xml_r_font(), + xml_sz(val = 16) + ), + xml_t( + paste0(heading$subtitle, footnote_subtitle_marks) + ) + ) + ) + } + ) + + title_row <- + xml_tr( + xml_trPr( + if(!split){xml_cantSplit()}, + xml_tbl_header() + ), + xml_table_cell( + text = title_html, + size = 24, + color = table_font_color, + align = "center", + col_span = n_cols, + border = if (table_border_top_include) { + list( + "top" = cell_border( + type = "single", + size = 16, + color = heading_border_bottom_color + ), + "bottom" = cell_border( + type = "single", + size = 16, + color = heading_border_bottom_color + ) + ) + }, + keep_with_next = TRUE + ) + ) + + htmltools::tagList(title_row) +} + +#' Create the columns component of a table (OOXML) +#' +#' @noRd +create_columns_component_xml <- function(data, split = FALSE, keep_with_next = TRUE) { + + boxh <- dt_boxhead_get(data = data) + stubh <- dt_stubhead_get(data = data) + body <- dt_body_get(data = data) + styles_tbl <- dt_styles_get(data = data) + stub_available <- dt_stub_df_exists(data = data) + spanners_present <- dt_spanners_exists(data = data) + + # Get the column alignments for all visible columns + col_alignment <- dplyr::pull(subset(boxh, type == "default"), column_align) + + # Get the column headings + headings_vars <- dplyr::pull(subset(boxh, type == "default"), var) + headings_labels <- dt_boxhead_get_vars_labels_default(data = data) + + # Determine the finalized number of spanner rows + spanner_row_count <- + dt_spanners_matrix_height( + data = data, + omit_columns_row = TRUE + ) + + # Should the column labels be hidden? + column_labels_hidden <- + dt_options_get_value( + data = data, + option = "column_labels_hidden" + ) + + if (column_labels_hidden) { + return("") + } + + # Get table options + table_border_top_include <- dt_options_get_value(data, option = "table_border_top_include") + column_labels_border_top_color <- dt_options_get_value(data = data, option = "column_labels_border_top_color") + column_labels_border_bottom_color <- dt_options_get_value(data = data, option = "column_labels_border_bottom_color") + column_labels_vlines_color <- dt_options_get_value(data = data, option = "column_labels_vlines_color") + + # If `stub_available` == TRUE, then replace with a set stubhead + # label or nothing + if (isTRUE(stub_available) && length(stubh$label) > 0) { + + headings_labels <- prepend_vec(headings_labels, stubh$label) + headings_vars <- prepend_vec(headings_vars, "::stub") + + } else if (isTRUE(stub_available)) { + + headings_labels <- prepend_vec(headings_labels, "") + headings_vars <- prepend_vec(headings_vars, "::stub") + } + + stubhead_label_alignment <- "left" + + table_col_headings_list <- list() + + ## Create first row of table column headings - + table_cell_vals <- list() + + # Create the cell for the stubhead label + if (stub_available ) { + + ## if there are spanners, make the first row an empty cell that continues merge + if(spanner_row_count < 1){ + + cell_style <- styles_tbl %>% + dplyr::filter( + locname %in% c("stubhead") + ) %>% + dplyr::pull("styles") %>% + .[1] %>% .[[1]] + + table_cell_vals[[length(table_cell_vals) + 1]] <- + xml_table_cell( + text = headings_labels[1], + font = cell_style[["cell_text"]][["font"]], + size = cell_style[["cell_text"]][["size"]] %||% 20, + color = cell_style[["cell_text"]][["color"]], + stretch = cell_style[["cell_text"]][["stretch"]], + align = cell_style[["cell_text"]][["align"]] %||% stubhead_label_alignment, + v_align = cell_style[["cell_text"]][["v_align"]], + fill = cell_style[["cell_fill"]][["color"]], + border = list( + top = cell_border(size = 16, color = column_labels_border_top_color), + bottom = cell_border(size = 16, color = column_labels_border_bottom_color), + left = cell_border(color = column_labels_vlines_color), + right = cell_border(color = column_labels_vlines_color) + ), + keep_with_next = keep_with_next + ) + + }else{ + + table_cell_vals[[length(table_cell_vals) + 1]] <- + xml_table_cell( + row_span = "continue", + border = list( + left = cell_border(color = column_labels_vlines_color), + right = cell_border(color = column_labels_vlines_color), + bottom = cell_border(size = 16, color = column_labels_border_bottom_color) + ), + keep_with_next = TRUE + ) + } + } + + for (i in seq_len(length(headings_vars) - stub_available)) { + + cell_style <- styles_tbl %>% + dplyr::filter( + locname %in% c("columns_columns"), + rownum == -1, + colnum == i + ) %>% + dplyr::pull("styles") %>% + .[1] %>% + .[[1]] + + table_cell_vals[[length(table_cell_vals) + 1]] <- + xml_table_cell( + text = headings_labels[i + stub_available], + font = cell_style[["cell_text"]][["font"]], + size = cell_style[["cell_text"]][["size"]] %||% 20, + color = cell_style[["cell_text"]][["color"]], + stretch = cell_style[["cell_text"]][["stretch"]], + align = cell_style[["cell_text"]][["align"]], + v_align = cell_style[["cell_text"]][["v_align"]], + fill = cell_style[["cell_fill"]][["color"]], + border = list( + top = if(!spanners_present){cell_border(size = 16, color = column_labels_border_top_color)}, + bottom = cell_border(size = 16, color = column_labels_border_bottom_color), + left = if(i == 1){cell_border(color = column_labels_vlines_color)}, + right = if(i == length(headings_vars) - stub_available){ cell_border(color = column_labels_vlines_color)} + ), + keep_with_next = keep_with_next + ) + } + + table_col_headings_list[[1]] <- + xml_tr( + xml_trPr( + if(!split){xml_cantSplit()}, + xml_tbl_header() + ), + paste( + vapply( + table_cell_vals, + FUN.VALUE = character(1), + FUN = paste + ), + collapse = "" + ) + ) + + if(spanners_present){ + + spanners <- + dt_spanners_print_matrix( + data = data, + include_hidden = FALSE + ) + + spanner_ids <- + dt_spanners_print_matrix( + data = data, + include_hidden = FALSE, + ids = TRUE + ) + + for(span_row_idx in rev(seq_len(spanner_row_count))){ + + spanner_row_values <- spanners[span_row_idx,] + spanner_row_ids <- spanner_ids[span_row_idx,] + + spanner_cell_vals <- list() + + # Create the cell for the stub head label + + if (stub_available) { + + if(span_row_idx == 1){ + + cell_style <- styles_tbl %>% + dplyr::filter( + locname %in% c("stubhead") + ) %>% + dplyr::pull("styles") %>% + .[1] %>% .[[1]] + + + spanner_cell_vals[[length(spanner_cell_vals) + 1]] <- + xml_table_cell( + text = headings_labels[1], + font = cell_style[["cell_text"]][["font"]] %||% "Calibri", + size = cell_style[["cell_text"]][["size"]] %||% 20, + color = cell_style[["cell_text"]][["color"]], + stretch = cell_style[["cell_text"]][["stretch"]], + align = cell_style[["cell_text"]][["align"]] %||% stubhead_label_alignment, + v_align = cell_style[["cell_text"]][["v_align"]] %||% "bottom", + fill = cell_style[["cell_fill"]][["color"]], + row_span = "start", + border = list( + top = cell_border(size = 16, color = column_labels_border_top_color), + left = cell_border(color = column_labels_vlines_color), + right = cell_border(color = column_labels_vlines_color) + ), + keep_with_next = TRUE + ) + + }else{ + + spanner_cell_vals[[length(spanner_cell_vals) + 1]] <- + xml_table_cell( + row_span = "continue", + border = list( + left = cell_border(color = column_labels_vlines_color), + right = cell_border(color = column_labels_vlines_color), + bottom = if(span_row_idx == nrow(spanners)){cell_border(size = 16, color = column_labels_border_bottom_color)} + ), + keep_with_next = TRUE + ) + } + } + + # NOTE: rle treats NA values as distinct from each other; in other words, + # each NA value starts a new run of length 1. + spanners_rle <- rle(spanner_row_ids) + # sig_cells contains the indices of spanners' elements where the value is + # either NA, or, is different than the previous value. (Because NAs are + # distinct, every NA element will be present sig_cells.) + sig_cells <- c(1, utils::head(cumsum(spanners_rle$lengths) + 1, -1)) + # colspans matches spanners in length; each element is the number of + # columns that the at that position should span. If 0, then skip the + # at that position. + colspans <- ifelse( + seq_along(spanner_row_values) %in% sig_cells, + spanners_rle$lengths[match(seq_along(spanner_row_ids), sig_cells)], + 0 + ) + + for (i in seq_along(spanner_row_values)) { + + if (is.na(spanner_row_ids[i])) { + + spanner_cell_vals[[length(spanner_cell_vals) + 1]] <- + xml_table_cell( + border = list( + left = if(i == 1){cell_border(color = column_labels_vlines_color)}, + right = if(i == length(spanner_row_values)){ cell_border(color = column_labels_vlines_color)}, + top = if(span_row_idx == 1){cell_border(size = 16, color = column_labels_border_top_color)} + ), + keep_with_next = keep_with_next + ) + + } else { + # Case with no spanner labels are in top row + # (merge cells horizontally and align text to bottom) + if (colspans[i] > 0) { + + cell_style <- styles_tbl %>% + dplyr::filter( + locname %in% c("columns_groups"), + grpname %in% spanner_row_ids[i] + ) %>% + dplyr::pull("styles") %>% + .[1] %>% .[[1]] + + ## check if there are any open cells above to determine + spanner_cell_vals[[length(spanner_cell_vals) + 1]] <- + xml_table_cell( + text = spanner_row_values[i], + font = cell_style[["cell_text"]][["font"]], + size = cell_style[["cell_text"]][["size"]] %||% 20, + color = cell_style[["cell_text"]][["color"]], + stretch = cell_style[["cell_text"]][["stretch"]], + align = cell_style[["cell_text"]][["align"]] %||% "center", + v_align = cell_style[["cell_text"]][["v_align"]], + fill = cell_style[["cell_fill"]][["color"]], + col_span = colspans[i], + border = list( + left = if(i == 1){cell_border(color = column_labels_vlines_color)}, + right = if(i == (length(spanner_row_values) + 1 - colspans[i] )){ cell_border(color = column_labels_vlines_color)}, + bottom = cell_border(size = 16, color = column_labels_border_bottom_color), + top = if(span_row_idx == 1){cell_border(size = 16, color = column_labels_border_top_color)} + ), + keep_with_next = keep_with_next + ) + + } + } + + } + + + table_col_headings_list[[length(table_col_headings_list) + 1 ]] <- + xml_tr( + xml_trPr( + if(!split){xml_cantSplit()}, + xml_tbl_header() + ), + paste( + vapply( + spanner_cell_vals, + FUN.VALUE = character(1), + FUN = paste + ), + collapse = "" + ) + ) + + } + + } + + do.call(htmltools::tagList, rev(table_col_headings_list)) + +} + +#' Create the table body component (OOXML) +#' +#' @importFrom rlang `%||%` +#' @noRd +create_body_component_xml <- function(data, split = FALSE, keep_with_next = TRUE) { + + boxh <- dt_boxhead_get(data = data) + body <- dt_body_get(data = data) + + summaries_present <- dt_summary_exists(data = data) + list_of_summaries <- dt_summary_df_get(data = data) + groups_rows_df <- dt_groups_rows_get(data = data) + stub_components <- dt_stub_components(data = data) + + # Get table styles + styles_tbl <- dt_styles_get(data = data) + + # Get table options + row_group_border_top_color <- dt_options_get_value(data = data, option = "row_group_border_top_color") + row_group_border_bottom_color <- dt_options_get_value(data = data, option = "row_group_border_bottom_color") + row_group_border_left_color <- dt_options_get_value(data = data, option = "row_group_border_left_color") + row_group_border_right_color <- dt_options_get_value(data = data, option = "row_group_border_right_color") + table_body_hlines_color <- dt_options_get_value(data = data, option = "table_body_hlines_color") + table_body_vlines_color <- dt_options_get_value(data = data, option = "table_body_vlines_color") + table_border_bottom_color <- dt_options_get_value(data, option = "table_border_bottom_color") + table_border_top_color <- dt_options_get_value(data, option = "table_border_top_color") + + n_data_cols <- length(dt_boxhead_get_vars_default(data = data)) + n_rows <- nrow(body) + + # Get the column alignments for the data columns (this + # doesn't include the stub alignment) + col_alignment <- boxh[boxh$type == "default", ][["column_align"]] + + # Determine whether the stub is available through analysis + # of the `stub_components` + stub_available <- dt_stub_components_has_rowname(stub_components) + + # Obtain all of the visible (`"default"`), non-stub + # column names for the table + default_vars <- dt_boxhead_get_vars_default(data = data) + + all_default_vals <- unname(as.matrix(body[, default_vars])) + + alignment_classes <- paste0("gt_", col_alignment) + + if (stub_available) { + + n_cols <- n_data_cols + 1 + + alignment_classes <- c("gt_left", alignment_classes) + + stub_var <- dt_boxhead_get_var_stub(data = data) + all_stub_vals <- as.matrix(body[, stub_var]) + + } else { + n_cols <- n_data_cols + } + + # Define function to get a character vector of formatted cell + # data (this includes the stub, if it is present) + output_df_row_as_vec <- function(i) { + + default_vals <- all_default_vals[i, ] + + if (stub_available) { + default_vals <- c(all_stub_vals[i], default_vals) + } + + default_vals + } + + # Get the sequence of column numbers in the table body (these + # are the visible columns in the table exclusive of the stub) + column_series <- seq(n_cols) + + # Replace an NA group with an empty string + if (any(is.na(groups_rows_df$group_label))) { + + groups_rows_df <- + groups_rows_df %>% + dplyr::mutate(group_label = ifelse(is.na(group_label), "", group_label)) %>% + dplyr::mutate(group_label = gsub("^NA", "\u2014", group_label)) + } + + body_rows <- + lapply( + seq_len(n_rows), + function(i) { + + body_section <- list() + # + # Create a group heading row + # + if (!is.null(groups_rows_df) && i %in% groups_rows_df$row_start) { + + group_label <- + groups_rows_df[ + which(groups_rows_df$row_start %in% i), "group_label" + ][[1]] + + cell_style <- styles_tbl %>% + dplyr::filter( + locname == "row_groups", + rownum == (i-.1) + ) %>% + dplyr::pull("styles") %>% + .[1] %>% .[[1]] + + group_heading_row <- + xml_tr( + xml_trPr( + if(!split){xml_cantSplit()} + ), + xml_table_cell( + text = htmltools::HTML(group_label), + font = cell_style[["cell_text"]][["font"]], + size = cell_style[["cell_text"]][["size"]] %||% 20, + color = cell_style[["cell_text"]][["color"]], + stretch = cell_style[["cell_text"]][["stretch"]], + align = cell_style[["cell_text"]][["align"]], + v_align = cell_style[["cell_text"]][["v_align"]], + col_span = n_cols, + fill = cell_style[["cell_fill"]][["color"]], + margins = list( + top = cell_margin(width = 25) + ), + border = list( + top = cell_border(size = 16, color = row_group_border_top_color), + bottom = cell_border(size = 16, color = row_group_border_bottom_color), + left = cell_border(color = row_group_border_left_color), + right = cell_border(color = row_group_border_right_color) + ), + keep_with_next = keep_with_next + ) + ) + + body_section <- append(body_section, list(group_heading_row)) + } + + # + # Create a body row + # + + row_cells <- list() + col_idx <- + + i + + for (y in seq_along(output_df_row_as_vec(i))) { + + style_col_idx <- ifelse(stub_available, y - 1, y ) + + cell_style <- styles_tbl %>% + dplyr::filter( + locname %in% c("data","stub"), + rownum == i, + colnum == style_col_idx + ) %>% + dplyr::pull("styles") %>% + .[1] %>% .[[1]] + + row_cells[[length(row_cells) + 1]] <- + xml_table_cell( + text = output_df_row_as_vec(i)[y], + font = cell_style[["cell_text"]][["font"]], + size = cell_style[["cell_text"]][["size"]], + color = cell_style[["cell_text"]][["color"]], + stretch = cell_style[["cell_text"]][["stretch"]], + align = cell_style[["cell_text"]][["align"]], + v_align = cell_style[["cell_text"]][["v_align"]], + border = list( + top = cell_border(color = table_body_hlines_color), + bottom = cell_border(color = table_body_hlines_color), + left = cell_border(color = table_body_vlines_color), + right = cell_border(color = table_body_vlines_color) + ), + fill = cell_style[["cell_fill"]][["color"]], + keep_with_next = keep_with_next + ) + } + + body_row <- + xml_tr( + xml_trPr( + if(!split){xml_cantSplit()} + ), + paste( + vapply( + row_cells, + FUN.VALUE = character(1), + FUN = paste + ), collapse = "" + ) + ) + + body_section <- append(body_section, list(body_row)) + + # + # Add groupwise summary rows + # + + if (summaries_present && + i %in% groups_rows_df$row_end) { + + group_id <- + groups_rows_df[ + stats::na.omit(groups_rows_df$row_end == i), + "group_id", drop = TRUE + ] + + summary_styles <- styles_tbl %>% + dplyr::filter( + locname %in% c("summary_cells"), + grpname %in% group_id + ) %>% + dplyr::mutate( + rownum = ceiling(rownum*100 - i*100) + ) + + summary_section <- + summary_rows_xml( + list_of_summaries = list_of_summaries, + boxh = boxh, + group_id = group_id, + locname = "summary_cells", + col_alignment = col_alignment, + table_body_hlines_color = table_body_hlines_color, + table_body_vlines_color = table_body_vlines_color, + styles = summary_styles, + split = split, + keep_with_next = keep_with_next + ) + + body_section <- append(body_section, summary_section) + } + + body_section + } + ) + + body_rows <- flatten_list(body_rows) + + # + # Add grand summary rows + # + + if (summaries_present && + grand_summary_col %in% names(list_of_summaries$summary_df_display_list)) { + + summary_styles <- styles_tbl %>% + dplyr::filter( + locname %in% "grand_summary_cells", + grpname %in% c("::GRAND_SUMMARY") + ) + + grand_summary_section <- + summary_rows_xml( + list_of_summaries = list_of_summaries, + boxh = boxh, + group_id = grand_summary_col, + locname = "grand_summary_cells", + col_alignment = col_alignment, + table_body_hlines_color = table_body_hlines_color, + table_body_vlines_color = table_body_vlines_color, + styles = summary_styles, + split = split, + keep_with_next = keep_with_next + ) + + body_rows <- c(body_rows, grand_summary_section) + } + + htmltools::tagList(body_rows) +} + +#' Create the table source note component (OOXML) +#' +#' @noRd +create_source_notes_component_xml <- function(data, split = FALSE, keep_with_next = TRUE) { + + source_note <- dt_source_notes_get(data = data) + + if (is.null(source_note)) { + return("") + } + + stub_components <- dt_stub_components(data = data) + + cell_style <- dt_styles_get(data = data) %>% + dplyr::filter( + locname == "source_notes" + ) %>% + dplyr::pull("styles") %>% + .[1] %>% .[[1]] + + n_data_cols <- length(dt_boxhead_get_vars_default(data = data)) + + # Determine whether the stub is available + stub_available <- dt_stub_components_has_rowname(stub_components = stub_components) + + if (stub_available) { + n_cols <- n_data_cols + 1 + } else { + n_cols <- n_data_cols + } + + source_note_rows <- + lapply( + source_note, + function(x) { + + as.character( + xml_tr( + xml_trPr( + if(!split){xml_cantSplit()} + ), + xml_table_cell( + text = htmltools::HTML(x), + font = cell_style[["cell_text"]][["font"]], + size = cell_style[["cell_text"]][["size"]] %||% 20, + color = cell_style[["cell_text"]][["color"]], + stretch = cell_style[["cell_text"]][["stretch"]], + align = cell_style[["cell_text"]][["align"]], + v_align = cell_style[["cell_text"]][["v_align"]], + col_span = n_cols, + fill = cell_style[["cell_fill"]][["color"]], + keep_with_next = keep_with_next + ) + ) + ) + } + ) + + paste0(unlist(source_note_rows), collapse = "\n") +} + +#' Create the table footnote component (OOXML) +#' +#' @noRd +create_footnotes_component_xml <- function(data, split = FALSE, keep_with_next = TRUE) { + + footnotes_tbl <- dt_footnotes_get(data = data) + + # If the `footnotes_resolved` object has no + # rows, then return an empty footnotes component + if (nrow(footnotes_tbl) == 0) { + return("") + } + + stub_components <- dt_stub_components(data = data) + + cell_style <- dt_styles_get(data = data) %>% + dplyr::filter( + locname == "footnotes" + ) %>% + dplyr::pull("styles") %>% + .[1] %>% .[[1]] + + n_data_cols <- length(dt_boxhead_get_vars_default(data = data)) + + # Determine whether the stub is available + stub_available <- dt_stub_components_has_rowname(stub_components = stub_components) + + if (stub_available) { + n_cols <- n_data_cols + 1 + } else { + n_cols <- n_data_cols + } + + footnotes_tbl <- + footnotes_tbl %>% + dplyr::select(fs_id, footnotes) %>% + dplyr::distinct() + + # Get the footnote separator option + separator <- dt_options_get_value(data = data, option = "footnotes_sep") + + footnote_ids <- footnotes_tbl[["fs_id"]] + footnote_text <- footnotes_tbl[["footnotes"]] + + footnote_rows <- + lapply( + seq_along(footnote_ids), + function(x) { + + as.character( + xml_tr( + xml_trPr( + if(!split){xml_cantSplit()} + ), + xml_table_cell( + paragraph_xml = htmltools::tagList( + xml_r( + xml_rPr( + xml_r_font( + ascii_font= cell_style[["cell_text"]][["font"]] %||% "Calibri", + ansi_font= cell_style[["cell_text"]][["font"]] %||% "Calibri" + ), + if(!is.null(cell_style[["cell_text"]][["color"]])){ + xml_color(color = cell_style[["cell_text"]][["color"]]) + }, + xml_sz(val = cell_style[["cell_text"]][["size"]] %||% 20), + xml_baseline_adj(v_align = "superscript"), + xml_i() + ), + xml_t(if(!is.na(footnote_ids[x])){footnote_ids[x]}) + ), + xml_r( + xml_rPr( + xml_r_font( + ascii_font= cell_style[["cell_text"]][["font"]] %||% "Calibri", + ansi_font= cell_style[["cell_text"]][["font"]] %||% "Calibri" + ), + if(!is.null(cell_style[["cell_text"]][["color"]])){ + xml_color(color = cell_style[["cell_text"]][["color"]]) + }, + xml_sz(val = cell_style[["cell_text"]][["size"]] %||% 20), + xml_baseline_adj(v_align = "baseline") + ), + xml_t(footnote_text[x]) + ) + ), + stretch = cell_style[["cell_text"]][["stretch"]], + align = cell_style[["cell_text"]][["align"]], + v_align = cell_style[["cell_text"]][["v_align"]], + col_span = n_cols, + fill = cell_style[["cell_fill"]][["color"]], + keep_with_next = keep_with_next + ) + ) + ) + } + ) + + paste0(unlist(footnote_rows), collapse = "") +} + +summary_rows_xml <- function(list_of_summaries, + boxh, + group_id, + locname, + col_alignment, + table_body_hlines_color, + table_body_vlines_color, + styles, + split = FALSE, + keep_with_next = TRUE) { + + # Obtain all of the visible (`"default"`), non-stub column names + # for the table from the `boxh` object + default_vars <- boxh[boxh$type == "default", "var", drop = TRUE] + + summary_row_lines <- list() + + if (group_id %in% names(list_of_summaries$summary_df_display_list)) { + + # Obtain the summary data table specific to the group ID and + # select the column named `rowname` and all of the visible columns + summary_df <- + list_of_summaries$summary_df_display_list[[group_id]] %>% + dplyr::select(.env$rowname_col_private, .env$default_vars) + + n_cols <- ncol(summary_df) + + summary_df_row <- function(j) { + unname(unlist(summary_df[j, ])) + } + + for (j in seq_len(nrow(summary_df))) { + + summary_row_cells <- list() + + for (y in seq_along(summary_df_row(j))) { + + cell_style <- styles %>% + dplyr::filter( + rownum == j, + colnum == y -1 + ) %>% + dplyr::pull("styles") %>% + .[1] %>% .[[1]] + + summary_row_cells[[length(summary_row_cells) + 1]] <- + xml_table_cell( + text = summary_df_row(j)[y], + font = cell_style[["cell_text"]][["font"]], + size = cell_style[["cell_text"]][["size"]], + color = cell_style[["cell_text"]][["color"]], + stretch = cell_style[["cell_text"]][["stretch"]], + align = cell_style[["cell_text"]][["align"]], + v_align = cell_style[["cell_text"]][["v_align"]], + fill = cell_style[["cell_fill"]][["color"]], + border = list( + "top" = cell_border(size = if (j == 1) 16 else 2, color = table_body_hlines_color), + "bottom" = cell_border(size = if (j == 1) 16 else 2, color = table_body_hlines_color), + "left" = cell_border(color = table_body_vlines_color), + "right" = cell_border(color = table_body_vlines_color) + ), + margins = list( + "top" = cell_margin(width = 50) + ), + keep_with_next = keep_with_next + ) + } + + summary_row <- + xml_tr( + xml_trPr( + if(!split){xml_cantSplit()} + ), + paste( + vapply( + summary_row_cells, + FUN.VALUE = character(1), + FUN = paste + ), collapse = "" + ) + ) + + summary_row_lines <- append(summary_row_lines, list(summary_row)) + } + } + + summary_row_lines +} + +cell_border <- function(color = "#D3D3D3", size = NULL, type = "single"){ + list( + color = color, + size = size, + type = type, + type = NULL + ) +} + +cell_margin <- function(width, type = c("dxa", "nil")){ + type <- match.arg(type) + list( + width = width, + type = type + ) +} + +stretch_to_xml_stretch <- function(x){ + x <- + match.arg( + x, + choices = c( + "ultra-condensed", + "extra-condensed", + "condensed", + "semi-condensed", + "normal", + "semi-expanded", + "expanded", + "extra-expanded", + "ultra-expanded" + ) + ) + + c( + "ultra-condensed" = -60, + "extra-condensed" = -40, + "condensed" = -20, + "semi-condensed" = 0, + "normal" = 20, + "semi-expanded" = 40, + "expanded" = 60, + "extra-expanded" = 80, + "ultra-expanded" = 100 + )[x] +} + +v_align_to_xml_v_align <- function(x){ + x <- + match.arg(x, + choices = c("middle", "top", "bottom")) + + c( + "middle" = "center", + "bottom" = "bottom", + "top" = "top" + )[x] +} + +row_span_to_xml_v_merge <- function(x){ + + x <- match.arg(x,choices = c("start","continue")) + + c( + "continue" = "continue", + "start" = "restart" + )[x] + +} + +#' define ooxml table cells +#' +#' paragrah +#' +#' @importFrom rlang `%||%` +#' @noRd +xml_table_cell <- + function(text = NULL, + size = NULL, + font = NULL, + color = NULL, + stretch = NULL, + align = NULL, + v_align = NULL, + col_span = NULL, + row_span = NULL, + fill = NULL, + margins = NULL, + border = NULL, + keep_with_next = TRUE, + paragraph_xml = NULL) { + + + xml_tc( + xml_tcPr( + if(!is.null(border)){ + xml_tc_borders( + if(!is.null(border[["top"]])){xml_border("top", color = border[["top"]][["color"]], size = border[["top"]][["size"]], type = border[["top"]][["type"]])}, + if(!is.null(border[["bottom"]])){xml_border("bottom", color = border[["bottom"]][["color"]], size = border[["bottom"]][["size"]], type = border[["bottom"]][["type"]])}, + if(!is.null(border[["left"]])){xml_border("left", color = border[["left"]][["color"]], size = border[["left"]][["size"]], type = border[["left"]][["type"]])}, + if(!is.null(border[["right"]])){xml_border("right", color = border[["right"]][["color"]], size = border[["right"]][["size"]], type = border[["right"]][["type"]])} + ) + }, + if(!is.null(fill)){xml_shd(fill = fill)}, + if(!is.null(row_span)){xml_v_merge(val = row_span_to_xml_v_merge(row_span))}, + if(!is.null(v_align)){xml_v_align(v_align = v_align_to_xml_v_align(v_align))}, + if(!is.null(margins)){ + xml_tc_margins( + if(!is.null(margins[["top"]])){xml_width("top", width = margins[["top"]][["width"]], type = margins[["top"]][["type"]])}, + if(!is.null(margins[["bottom"]])){xml_width("bottom", width = margins[["bottom"]][["width"]], type = margins[["bottom"]][["type"]])}, + if(!is.null(margins[["left"]])){xml_width("left", width = margins[["left"]][["width"]], type = margins[["left"]][["type"]])}, + if(!is.null(margins[["right"]])){xml_width("right", width = margins[["right"]][["width"]], type = margins[["right"]][["type"]])} + ) + } + ), + + xml_p( + xml_pPr( + xml_spacing(before = 0, after = 60), + if(!is.null(col_span)){xml_gridSpan(val = as.character(col_span))}, + if(keep_with_next){xml_keepNext()}, + if(!is.null(stretch)){ + xml_rPr( + xml_spacing(val = stretch_to_xml_stretch(stretch)) + ) + }, + if(!is.null(align)){xml_jc(val = align)} + ), + if(is.null(paragraph_xml)){ + xml_r( + xml_rPr( + xml_r_font( + ascii_font= font %||% "Calibri", + ansi_font= font %||% "Calibri" + ), + xml_sz(val = size %||% 20), + if(!is.null(color)){ + xml_color(color = color) + } + ), + if(is.character(text)){ + xml_t(text) + }else if(inherits(text, "shiny.tag.list")){ + text + } + ) + }else{ + paragraph_xml + } + ) + ) + +} diff --git a/R/utils_render_footnotes.R b/R/z_utils_render_footnotes.R similarity index 85% rename from R/utils_render_footnotes.R rename to R/z_utils_render_footnotes.R index 0fea205009..6db52522be 100644 --- a/R/utils_render_footnotes.R +++ b/R/z_utils_render_footnotes.R @@ -443,7 +443,12 @@ set_footnote_marks_columns <- function(data, vector_indices <- which(spanner_ids == footnotes_columns_group_marks$grpname[i]) text <- unique(spanner_labels[vector_indices]) - text <- paste0(text, footnote_mark_fn(footnotes_columns_group_marks$fs_id_coalesced[i])) + + text <- + paste0( + text, + footnotes_dispatch[[context]](footnotes_columns_group_marks$fs_id_coalesced[i]) + ) spanners_i <- which( @@ -467,17 +472,15 @@ set_footnote_marks_columns <- function(data, dplyr::select(colname, fs_id_coalesced) %>% dplyr::distinct() - for (i in seq(nrow(footnotes_columns_column_marks))) { - - text <- boxh$column_label[ - boxh$var == footnotes_columns_column_marks$colname[i]][[1]] - text <- paste0(text, footnote_mark_fn(footnotes_columns_column_marks$fs_id_coalesced[i])) + for (i in seq(nrow(footnotes_columns_column_marks))) { + text <- + paste0( + boxh$column_label[ + boxh$var == footnotes_columns_column_marks$colname[i]][[1]], + footnotes_dispatch[[context]](footnotes_columns_column_marks$fs_id_coalesced[i]) + ) - # boxh$column_label <- dplyr::case_when( - # var == footnotes_columns_column_marks$colname[i] ~ list(text), - # TRUE ~ column_label - # ) boxh <- dplyr::mutate( boxh, @@ -521,21 +524,7 @@ set_footnote_marks_stubhead <- function(data, dplyr::distinct() %>% dplyr::pull(fs_id_coalesced) - if (context == "html") { - - label <- - paste0(label, footnote_mark_to_html(footnotes_stubhead_marks)) - - } else if (context == "rtf") { - - label <- - paste0(label, footnote_mark_to_rtf(footnotes_stubhead_marks)) - - } else if (context == "latex") { - - label <- - paste0(label, footnote_mark_to_latex(footnotes_stubhead_marks)) - } + label <- paste0(label, footnotes_dispatch[[context]](footnotes_stubhead_marks)) } } @@ -593,13 +582,7 @@ apply_footnotes_to_output <- function(data, context = context ) - if (context == "html") { - mark <- footnote_mark_to_html(footnotes_data_marks$fs_id_coalesced[i]) - } else if (context == "rtf") { - mark <- footnote_mark_to_rtf(footnotes_data_marks$fs_id_coalesced[i]) - } else if (context == "latex") { - mark <- footnote_mark_to_latex(footnotes_data_marks$fs_id_coalesced[i]) - } + mark <- footnotes_dispatch[[context]](footnotes_data_marks$fs_id_coalesced[i]) if (footnote_placement == "right") { text <- paste0(text, mark) @@ -635,42 +618,17 @@ set_footnote_marks_row_groups <- function(data, dplyr::select(grpname, fs_id_coalesced) %>% dplyr::distinct() + fn <- footnotes_dispatch[[context]] + for (i in seq(nrow(footnotes_row_groups_marks))) { row_index <- which(groups_rows_df[, "group_id"] == footnotes_row_groups_marks$grpname[i]) - text <- groups_rows_df[row_index, "group_label"] - - if (context == "html") { - - text <- - paste0( - text, - footnote_mark_to_html( - footnotes_row_groups_marks$fs_id_coalesced[i]) - ) - - } else if (context == "rtf") { - - text <- - paste0( - text, - footnote_mark_to_rtf( - footnotes_row_groups_marks$fs_id_coalesced[i]) - ) - - } else if (context == "latex") { - - text <- - paste0( - text, - footnote_mark_to_latex( - footnotes_row_groups_marks$fs_id_coalesced[i]) - ) - } - - groups_rows_df[row_index, "group_label"] <- text + groups_rows_df[row_index, "group_label"] <- paste0( + groups_rows_df[row_index, "group_label"], + fn(footnotes_row_groups_marks$fs_id_coalesced[i]) + ) } } @@ -694,11 +652,6 @@ apply_footnotes_to_summary <- function(data, summary_df_list <- list_of_summaries$summary_df_display_list - # if (!("summary_cells" %in% footnotes_tbl$locname | - # "grand_summary_cells" %in% footnotes_tbl$locname)) { - # return(list_of_summaries) - # } - if ("summary_cells" %in% footnotes_tbl$locname) { footnotes_tbl_data <- footnotes_tbl[footnotes_tbl$locname == "summary_cells", ] @@ -714,29 +667,11 @@ apply_footnotes_to_summary <- function(data, dplyr::distinct() for (i in seq(nrow(footnotes_data_marks))) { - - text <- - summary_df_list[[footnotes_data_marks[i, ][["grpname"]]]][[ - footnotes_data_marks$row[i], footnotes_data_marks$colname[i]]] - - if (context == "html") { - - text <- - paste0(text, footnote_mark_to_html(footnotes_data_marks$fs_id_coalesced[i])) - - } else if (context == "rtf") { - - text <- - paste0(text, footnote_mark_to_rtf(footnotes_data_marks$fs_id_coalesced[i])) - - } else if (context == "latex") { - - text <- - paste0(text, footnote_mark_to_latex(footnotes_data_marks$fs_id_coalesced[i])) - } - summary_df_list[[footnotes_data_marks[i, ][["grpname"]]]][[ - footnotes_data_marks$row[i], footnotes_data_marks$colname[i]]] <- text + footnotes_data_marks$row[i], footnotes_data_marks$colname[i]]] <- paste0( + summary_df_list[[footnotes_data_marks[i, ][["grpname"]]]][[ + footnotes_data_marks$row[i], footnotes_data_marks$colname[i]]], + footnotes_dispatch[[context]](footnotes_data_marks$fs_id_coalesced[i])) } list_of_summaries$summary_df_display_list <- summary_df_list @@ -756,29 +691,11 @@ apply_footnotes_to_summary <- function(data, dplyr::distinct() for (i in seq(nrow(footnotes_data_marks))) { - - text <- - summary_df_list[[grand_summary_col]][[ - footnotes_data_marks$rownum[i], footnotes_data_marks$colname[i]]] - - if (context == "html") { - - text <- - paste0(text, footnote_mark_to_html(footnotes_data_marks$fs_id_coalesced[i])) - - } else if (context == "rtf") { - - text <- - paste0(text, footnote_mark_to_rtf(footnotes_data_marks$fs_id_coalesced[i])) - - } else if (context == "latex") { - - text <- - paste0(text, footnote_mark_to_latex(footnotes_data_marks$fs_id_coalesced[i])) - } - summary_df_list[[grand_summary_col]][[ - footnotes_data_marks$rownum[i], footnotes_data_marks$colname[i]]] <- text + footnotes_data_marks$rownum[i], footnotes_data_marks$colname[i]]] <- paste0( + summary_df_list[[grand_summary_col]][[ + footnotes_data_marks$rownum[i], footnotes_data_marks$colname[i]]], + footnotes_dispatch[[context]](footnotes_data_marks$fs_id_coalesced[i])) } list_of_summaries$summary_df_display_list[[grand_summary_col]] <- @@ -793,3 +710,11 @@ apply_footnotes_to_summary <- function(data, data } + +# TODO DOCUMENT THIS +footnotes_dispatch <- list( + html = footnote_mark_to_html, + rtf = footnote_mark_to_rtf, + latex = footnote_mark_to_latex, + word = footnote_mark_to_xml +) diff --git a/_pkgdown.yml b/_pkgdown.yml index df806fb2f4..6750c36a59 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -241,6 +241,7 @@ reference: - as_raw_html - as_latex - as_rtf + - as_word - extract_summary navbar: diff --git a/man/as_latex.Rd b/man/as_latex.Rd index d26e50e7cf..f6fec3f41a 100644 --- a/man/as_latex.Rd +++ b/man/as_latex.Rd @@ -48,6 +48,7 @@ just the LaTeX code as a single-element vector. Other Export Functions: \code{\link{as_raw_html}()}, \code{\link{as_rtf}()}, +\code{\link{as_word}()}, \code{\link{extract_summary}()}, \code{\link{gtsave}()} } diff --git a/man/as_raw_html.Rd b/man/as_raw_html.Rd index b05f592605..6296144dc9 100644 --- a/man/as_raw_html.Rd +++ b/man/as_raw_html.Rd @@ -54,6 +54,7 @@ document but rather an HTML fragment. Other Export Functions: \code{\link{as_latex}()}, \code{\link{as_rtf}()}, +\code{\link{as_word}()}, \code{\link{extract_summary}()}, \code{\link{gtsave}()} } diff --git a/man/as_rtf.Rd b/man/as_rtf.Rd index 8ae5f5f14b..f763d7d559 100644 --- a/man/as_rtf.Rd +++ b/man/as_rtf.Rd @@ -42,6 +42,7 @@ code. Other Export Functions: \code{\link{as_latex}()}, \code{\link{as_raw_html}()}, +\code{\link{as_word}()}, \code{\link{extract_summary}()}, \code{\link{gtsave}()} } diff --git a/man/as_word.Rd b/man/as_word.Rd new file mode 100644 index 0000000000..2e7c993a1d --- /dev/null +++ b/man/as_word.Rd @@ -0,0 +1,67 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/export.R +\name{as_word} +\alias{as_word} +\title{Output a \strong{gt} object as Word} +\usage{ +as_word( + data, + align = "center", + caption_location = c("top", "bottom", "embed"), + caption_align = "left", + split = FALSE, + keep_with_next = TRUE +) +} +\arguments{ +\item{data}{A table object that is created using the \code{gt()} function.} + +\item{align}{left, center (default) or right.} + +\item{caption_location}{top (default), bottom, or embed Indicating if the +title and subtitle should be listed above, below, or be embedded in the +table} + +\item{caption_align}{left (default), center, or right. Alignment of caption +(title and subtitle). Used when \code{caption_location} is not "embed".} + +\item{split}{TRUE or FALSE (default) indicating whether activate Word option +'Allow row to break across pages'.} + +\item{keep_with_next}{TRUE (default) or FALSE indicating whether a table +should use Word option 'keep rows together' is activated when TRUEd} +} +\description{ +Get the Open Office XML table tag content from a \code{gt_tbl} object as as a +single-element character vector. +} +\section{Function ID}{ + +13-5 +} + +\examples{ +# Use `gtcars` to create a gt table; +# add a header and then export as +# OOXML code for Word +tab_rtf <- + gtcars \%>\% + dplyr::select(mfr, model) \%>\% + dplyr::slice(1:2) \%>\% + gt() \%>\% + tab_header( + title = md("Data listing from **gtcars**"), + subtitle = md("`gtcars` is an R dataset") + ) \%>\% + as_word() + +} +\seealso{ +Other Export Functions: +\code{\link{as_latex}()}, +\code{\link{as_raw_html}()}, +\code{\link{as_rtf}()}, +\code{\link{extract_summary}()}, +\code{\link{gtsave}()} +} +\concept{Export Functions} diff --git a/man/extract_summary.Rd b/man/extract_summary.Rd index 1a6e18ae98..2283a6b406 100644 --- a/man/extract_summary.Rd +++ b/man/extract_summary.Rd @@ -101,7 +101,7 @@ Use the summary list to make a new \strong{gt} table. The key thing is to use \section{Function ID}{ -13-5 +13-6 } \seealso{ @@ -109,6 +109,7 @@ Other Export Functions: \code{\link{as_latex}()}, \code{\link{as_raw_html}()}, \code{\link{as_rtf}()}, +\code{\link{as_word}()}, \code{\link{gtsave}()} } \concept{Export Functions} diff --git a/man/gt-package.Rd b/man/gt-package.Rd index df73a5acb4..cd50cf0335 100644 --- a/man/gt-package.Rd +++ b/man/gt-package.Rd @@ -24,6 +24,7 @@ Authors: \itemize{ \item Joe Cheng \email{joe@rstudio.com} \item Barret Schloerke \email{barret@rstudio.com} (\href{https://orcid.org/0000-0001-9986-114X}{ORCID}) + \item Ellis Hughes \email{ellis.h.hughes@gsk.com} (\href{https://orcid.org/0000-0003-0637-4436}{ORCID}) } Other contributors: diff --git a/man/gtsave.Rd b/man/gtsave.Rd index a355e23c29..41d6465ee4 100644 --- a/man/gtsave.Rd +++ b/man/gtsave.Rd @@ -112,6 +112,7 @@ Other Export Functions: \code{\link{as_latex}()}, \code{\link{as_raw_html}()}, \code{\link{as_rtf}()}, +\code{\link{as_word}()}, \code{\link{extract_summary}()} } \concept{Export Functions} diff --git a/scripts/gt_word_testing.Rmd b/scripts/gt_word_testing.Rmd new file mode 100644 index 0000000000..e286849c43 --- /dev/null +++ b/scripts/gt_word_testing.Rmd @@ -0,0 +1,12 @@ +--- +output: word_document +--- + +```{r setup, include=FALSE} +knitr::opts_chunk$set(echo = TRUE) +library(gt) +``` + +```{r echo=FALSE} + +``` diff --git a/scripts/raw_word_testing.Rmd b/scripts/raw_word_testing.Rmd new file mode 100644 index 0000000000..254def4f4a --- /dev/null +++ b/scripts/raw_word_testing.Rmd @@ -0,0 +1,12 @@ +--- +output: word_document +--- + +```{r setup, include=FALSE} +knitr::opts_chunk$set(echo = TRUE) +library(gt) +``` + +```{=openxml} + +``` diff --git a/tests/testthat/_snaps/as_word.md b/tests/testthat/_snaps/as_word.md new file mode 100644 index 0000000000..98c9ec3c58 --- /dev/null +++ b/tests/testthat/_snaps/as_word.md @@ -0,0 +1,56 @@ +# word ooxml can be generated from gt object + + Code + . + Output + [1] "numcharfctrdatetimedatetimecurrencyrowgroup0.1111apricotone2015-01-1513:352018-01-01 02:2249.95row_1grp_a" + +--- + + Code + . + Output + [1] "Table \n\n SEQ Table \\* ARABIC \n\n1\n\n: TABLE TITLEtable subtitlenumcharfctrdatetimedatetimecurrencyrowgroup0.1111apricotone2015-01-1513:352018-01-01 02:2249.95row_1grp_a" + +--- + + Code + . + Output + [1] "numcharfctrdatetimedatetimecurrencyrowgroup0.1111apricotone2015-01-1513:352018-01-01 02:2249.95row_1grp_aTable \n\n SEQ Table \\* ARABIC \n\n1\n\n: TABLE TITLEtable subtitle" + +--- + + Code + . + Output + [1] "TABLE TITLE\n\ntable subtitlenumcharfctrdatetimedatetimecurrencyrowgroup0.1111apricotone2015-01-1513:352018-01-01 02:2249.95row_1grp_a" + +--- + + Code + . + Output + [1] "Table \n\n SEQ Table \\* ARABIC \n\n1\n\n: TABLE TITLEtable subtitlenumcharfctrdatetimedatetimecurrencyrowgroup0.1111apricotone2015-01-1513:352018-01-01 02:2249.95row_1grp_a" + +--- + + Code + . + Output + [1] "Table \n\n SEQ Table \\* ARABIC \n\n1\n\n: TABLE TITLEtable subtitlenumcharfctrdatetimedatetimecurrencyrowgroup0.1111apricotone2015-01-1513:352018-01-01 02:2249.95row_1grp_a" + +--- + + Code + . + Output + [1] "numfctrdatetimedatetimecurrencyrowgroupMy Row Group 2\ncoconut33.3300three2015-03-1515:452018-03-03 03:441.39row_3grp_a\ndurian444.4000four2015-04-1516:502018-04-04 15:5565100.00row_4grp_a\nMy Row Group 1\napricot0.1111one2015-01-1513:352018-01-01 02:2249.95row_1grp_a\nbanana2.2220two2015-02-1514:402018-02-02 14:3317.95row_2grp_a" + +--- + + Code + . + Output + [1] "My Span Label top\nMy Span Label\nnumfctrdatetimedatetimecurrencyrowgroupMy Row Group 2\ncoconut33.3300three2015-03-1515:452018-03-03 03:441.39row_3grp_a\ndurian444.4000four2015-04-1516:502018-04-04 15:5565100.00row_4grp_a\nMy Row Group 1\napricot0.1111one2015-01-1513:352018-01-01 02:2249.95row_1grp_a\nbanana2.2220two2015-02-1514:402018-02-02 14:3317.95row_2grp_a" + diff --git a/tests/testthat/test-as_word.R b/tests/testthat/test-as_word.R new file mode 100644 index 0000000000..28a173be36 --- /dev/null +++ b/tests/testthat/test-as_word.R @@ -0,0 +1,1552 @@ +skip_on_cran() + +#' @title Add gt table into a Word document +#' @description Add a gt into a Word document. +#' @param x `rdocx` object +#' @param value `gt` object +#' @param align left, center (default) or right. +#' @param caption_location top (default), bottom, or embed Indicating if the title and subtitle should be listed above, below, or be embedded in the table +#' @param caption_align left (default), center, or right. Alignment of caption (title and subtitle). Used when `caption_location` is not "embed". +#' @param split set to TRUE if you want to activate Word +#' option 'Allow row to break across pages'. +#' @param keep_with_next Word option 'keep rows together' can be +#' activated when TRUE. It avoids page break within tables. +#' @param pos where to add the gt table relative to the cursor, +#' one of "after" (default), "before", "on" (end of line). +#' +#' @seealso \link[flextable]{body_add_flextable} +#' +#' @examples +#' +#' library(officer) +#' library(gt) +#' gt_tbl <- gt( head( exibble ) ) +#' +#' doc <- read_docx() +#' doc <- body_add_gt(doc, value = gt_tbl) +#' fileout <- tempfile(fileext = ".docx") +#' print(doc, target = fileout) +#' @noRd +body_add_gt <- function( + x, + value, + align = "center", + pos = c("after","before","on"), + caption_location = c("top","bottom","embed"), + caption_align = "left", + split = FALSE, + keep_with_next = TRUE +) { + + ## check that officer is available + if (!rlang::is_installed("officer")) { + stop("{officer} package is necessary to add gt tables to word documents.") + } + + ## check that inputs are an officer rdocx and gt tbl + stopifnot(inherits(x, "rdocx")) + stopifnot(inherits(value, "gt_tbl")) + + pos <- match.arg(pos) + caption_location <- match.arg(caption_location) + + # Build all table data objects through a common pipeline + value <- build_data(data = value, context = "word") + + ## Create and add table caption if it is to come before the table + if (caption_location %in% c("top")) { + header_xml <- as_word_tbl_header_caption(data = value, align = caption_align, split = split, keep_with_next = keep_with_next) + if (!identical(header_xml,c(""))) { + for (header_component in header_xml) { + x <- officer::body_add_xml(x, str = header_component, pos) + } + } + } + + ## Create and add the table to the docxr. If the + tbl_xml <- as_word_tbl_body(data = value, align = align, split = split, keep_with_next = keep_with_next, embedded_heading = identical(caption_location, "embed")) + x <- officer::body_add_xml(x, str = tbl_xml, pos) + + ## Create and add table caption if it is to come after the table + if (caption_location %in% c("bottom")) { + ## set keep_with_next to false here to prevent it trying to keep with non-table content + header_xml <- as_word_tbl_header_caption(data = value, align = caption_align, split = split, keep_with_next = FALSE) + if (!identical(header_xml,c(""))) { + for (header_component in header_xml) { + x <- officer::body_add_xml(x, str = header_component, pos) + } + } + } + + x +} + +# Function to skip tests if Suggested packages not available on system +check_suggests_xml <- function() { + skip_if_not_installed("officer") + skip_if_not_installed("xml2") +} + +test_that("word ooxml can be generated from gt object", { + + + # Create a one-row table for these tests + exibble_min <- exibble[1, ] + + ## basic table + exibble_min %>% + gt() %>% + as_word() %>% + expect_snapshot() + + ## basic table with title + exibble_min %>% + gt() %>% + tab_header( + title = "TABLE TITLE", + subtitle = "table subtitle" + ) %>% + as_word() %>% + expect_snapshot() + + ## basic table with title added below table + exibble_min %>% + gt() %>% + tab_header( + title = "TABLE TITLE", + subtitle = "table subtitle" + ) %>% + as_word(caption_location = "bottom") %>% + expect_snapshot() + + ## basic table with title embedded on the top of table + exibble_min %>% + gt() %>% + tab_header( + title = "TABLE TITLE", + subtitle = "table subtitle" + ) %>% + as_word(caption_location = "embed") %>% + expect_snapshot() + + ## basic table with split enabled + exibble_min %>% + gt() %>% + tab_header( + title = "TABLE TITLE", + subtitle = "table subtitle" + ) %>% + as_word( + split = TRUE + ) %>% + expect_snapshot() + + ## basic table with keep_with_next disabled (should only appear in the column + ## headers) + exibble_min %>% + gt() %>% + tab_header( + title = "TABLE TITLE", + subtitle = "table subtitle" + ) %>% + as_word( + keep_with_next = FALSE + ) %>% + expect_snapshot() + + ## Table with cell styling + exibble[1:4,] %>% + gt(rowname_col = "char") %>% + tab_row_group("My Row Group 1",c(1:2)) %>% + tab_row_group("My Row Group 2",c(3:4)) %>% + tab_style( + style = cell_fill(color = "orange"), + locations = cells_body( + columns = c(num,fctr,time,currency, group) + ) + ) %>% + tab_style( + style = cell_fill(color = "orange"), + locations = cells_body( + columns = c(num,fctr,time,currency, group) + ) + ) %>% + tab_style( + style = cell_text( + color = "green", + font = "Biome", + ), + locations = cells_stub() + ) %>% + tab_style( + style = cell_text( + color = "blue" + ), + locations = cells_row_groups() + ) %>% + as_word( + keep_with_next = FALSE + ) %>% + expect_snapshot() + + ## table with column and span styling + gt_exibble_min <- exibble[1:4,] %>% + gt(rowname_col = "char") %>% + tab_row_group("My Row Group 1",c(1:2)) %>% + tab_row_group("My Row Group 2",c(3:4)) %>% + tab_spanner("My Span Label", columns = 1:5) %>% + tab_spanner("My Span Label top", columns = 2:4, level = 2) %>% + tab_style( + style = cell_text(color = "purple"), + locations = cells_column_labels() + ) %>% + tab_style( + style = cell_fill(color = "green"), + locations = cells_column_labels() + ) %>% + tab_style( + style = cell_fill(color = "orange"), + locations = cells_column_spanners("My Span Label") + ) %>% + tab_style( + style = cell_fill(color = "red"), + locations = cells_column_spanners("My Span Label top") + ) %>% + tab_style( + style = cell_fill(color = "pink"), + locations = cells_stubhead() + ) %>% + as_word() %>% + expect_snapshot() +}) + +test_that("tables can be added to a word doc", { + + skip_on_ci() + check_suggests_xml() + + ## simple table + gt_exibble_min <- exibble[1:2,] %>% + gt() %>% + tab_header( + title = "table title", + subtitle = "table subtitle" + ) + + ## Add table to empty word document + word_doc <- officer::read_docx() %>% + body_add_gt( + gt_exibble_min, + align = "center" + ) + + ## save word doc to temporary file + temp_word_file <- tempfile(fileext = ".docx") + print(word_doc,target = temp_word_file) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file) + } + + ## Programmatic Review + docx <- officer::read_docx(temp_word_file) + + ## get docx table contents + docx_contents <- docx$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + ## extract table caption + docx_table_caption_text <- docx_contents[1:2] %>% + xml2::xml_text() + + ## extract table contents + docx_table_body_header <- docx_contents[3] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + docx_table_body_contents <- docx_contents[3] %>% + xml2::xml_find_all(".//w:tr") %>% + setdiff(docx_table_body_header) + + expect_equal( + docx_table_caption_text, + c("Table SEQ Table \\* ARABIC 1: table title", "table subtitle") + ) + + expect_equal( + docx_table_body_header %>% + xml2::xml_find_all(".//w:p") %>% + xml2::xml_text(), + c("num", "char", "fctr", + "date", "time","datetime", + "currency", "row", "group") + ) + + expect_equal( + lapply(docx_table_body_contents, function(x) + x %>% xml2::xml_find_all(".//w:p") %>% xml2::xml_text()), + list( + c( + "0.1111", + "apricot", + "one", + "2015-01-15", + "13:35", + "2018-01-01 02:22", + "49.95", + "row_1", + "grp_a" + ), + c( + "2.2220", + "banana", + "two", + "2015-02-15", + "14:40", + "2018-02-02 14:33", + "17.95", + "row_2", + "grp_a" + ) + ) + ) +}) + +test_that("tables with embedded titles can be added to a word doc", { + + skip_on_ci() + check_suggests_xml() + + ## simple table + gt_exibble_min <- exibble[1:2,] %>% + gt() %>% + tab_header( + title = "table title", + subtitle = "table subtitle" + ) + + ## Add table to empty word document + word_doc <- officer::read_docx() %>% + body_add_gt( + gt_exibble_min, + caption_location = "embed", + align = "center" + ) + + ## save word doc to temporary file + temp_word_file <- tempfile(fileext = ".docx") + print(word_doc,target = temp_word_file) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file) + } + + ## Programmatic Review + docx <- officer::read_docx(temp_word_file) + + ## get docx table contents + docx_contents <- docx$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + ## extract table contents + docx_table_body_header <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + docx_table_body_contents <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tr") %>% + setdiff(docx_table_body_header) + + + expect_equal( + docx_table_body_header %>% + xml2::xml_find_all(".//w:t") %>% + xml2::xml_text(), + c("table title", "table subtitle", + "num", "char", "fctr", + "date", "time","datetime", + "currency", "row", "group") + ) + + expect_equal( + lapply(docx_table_body_contents, function(x) + x %>% xml2::xml_find_all(".//w:p") %>% xml2::xml_text()), + list( + c( + "0.1111", + "apricot", + "one", + "2015-01-15", + "13:35", + "2018-01-01 02:22", + "49.95", + "row_1", + "grp_a" + ), + c( + "2.2220", + "banana", + "two", + "2015-02-15", + "14:40", + "2018-02-02 14:33", + "17.95", + "row_2", + "grp_a" + ) + ) + ) +}) + +test_that("tables with spans can be added to a word doc", { + + skip_on_ci() + check_suggests_xml() + + ## simple table + gt_exibble_min <- exibble[1:2,] %>% + gt() %>% + tab_header( + title = "table title", + subtitle = "table subtitle" + ) %>% + ## add spanner across columns 1:5 + tab_spanner( + "My Column Span", + columns = 3:5 + ) + + ## Add table to empty word document + word_doc <- officer::read_docx() %>% + body_add_gt( + gt_exibble_min, + align = "center" + ) + + ## save word doc to temporary file + temp_word_file <- tempfile(fileext = ".docx") + print(word_doc,target = temp_word_file) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file) + } + + ## Programmatic Review + docx <- officer::read_docx(temp_word_file) + + ## get docx table contents + docx_contents <- docx$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + ## extract table caption + docx_table_caption_text <- docx_contents[1:2] %>% + xml2::xml_text() + + ## extract table contents + docx_table_body_header <- docx_contents[3] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + docx_table_body_contents <- docx_contents[3] %>% + xml2::xml_find_all(".//w:tr") %>% + setdiff(docx_table_body_header) + + expect_equal( + docx_table_caption_text, + c("Table SEQ Table \\* ARABIC 1: table title", "table subtitle") + ) + + expect_equal( + docx_table_body_header %>% + xml2::xml_find_all(".//w:p") %>% + xml2::xml_text(), + c( "","","My Column Span", "","","","", + "num", "char","fctr", "date", "time","datetime", "currency", "row","group") + ) + + expect_equal( + lapply(docx_table_body_contents, function(x) + x %>% xml2::xml_find_all(".//w:p") %>% xml2::xml_text()), + list( + c( + "0.1111", + "apricot", + "one", + "2015-01-15", + "13:35", + "2018-01-01 02:22", + "49.95", + "row_1", + "grp_a" + ), + c( + "2.2220", + "banana", + "two", + "2015-02-15", + "14:40", + "2018-02-02 14:33", + "17.95", + "row_2", + "grp_a" + ) + ) + ) +}) + +test_that("tables with multi-level spans can be added to a word doc", { + + skip_on_ci() + check_suggests_xml() + + ## simple table + gt_exibble_min <- exibble[1:2,] %>% + gt() %>% + tab_header( + title = "table title", + subtitle = "table subtitle" + ) %>% + ## add spanner across columns 1:5 + tab_spanner( + "My 1st Column Span L1", + columns = 1:5 + ) %>% + tab_spanner( + "My Column Span L2", + columns = 2:5,level = 2 + ) %>% + tab_spanner( + "My 2nd Column Span L1", + columns = 8:9 + ) + + ## Add table to empty word document + word_doc <- officer::read_docx() %>% + body_add_gt( + gt_exibble_min, + align = "center" + ) + + ## save word doc to temporary file + temp_word_file <- tempfile(fileext = ".docx") + print(word_doc,target = temp_word_file) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file) + } + + ## Programmatic Review + docx <- officer::read_docx(temp_word_file) + + ## get docx table contents + docx_contents <- docx$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + ## extract table caption + docx_table_caption_text <- docx_contents[1:2] %>% + xml2::xml_text() + + ## extract table contents + docx_table_body_header <- docx_contents[3] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + docx_table_body_contents <- docx_contents[3] %>% + xml2::xml_find_all(".//w:tr") %>% + setdiff(docx_table_body_header) + + expect_equal( + docx_table_caption_text, + c("Table SEQ Table \\* ARABIC 1: table title", "table subtitle") + ) + + expect_equal( + docx_table_body_header %>% + xml2::xml_find_all(".//w:p") %>% + xml2::xml_text(), + c("", "My Column Span L2", "","","","", + "My 1st Column Span L1", "","", "My 2nd Column Span L1", + "num", "char","fctr", "date", "time","datetime", "currency", "row","group") + ) + + expect_equal( + lapply(docx_table_body_contents, function(x) + x %>% xml2::xml_find_all(".//w:p") %>% xml2::xml_text()), + list( + c( + "0.1111", + "apricot", + "one", + "2015-01-15", + "13:35", + "2018-01-01 02:22", + "49.95", + "row_1", + "grp_a" + ), + c( + "2.2220", + "banana", + "two", + "2015-02-15", + "14:40", + "2018-02-02 14:33", + "17.95", + "row_2", + "grp_a" + ) + ) + ) +}) + +test_that("tables with summaries can be added to a word doc", { + + skip_on_ci() + check_suggests_xml() + + ## simple table + gt_exibble_min <- exibble %>% + dplyr::select(-c(fctr, date, time, datetime)) %>% + gt(rowname_col = "row", groupname_col = "group") %>% + summary_rows( + groups = TRUE, + columns = num, + fns = list( + avg = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + s.d. = ~sd(., na.rm = TRUE) + ) + ) + + ## Add table to empty word document + word_doc <- officer::read_docx() %>% + body_add_gt( + gt_exibble_min, + align = "center" + ) + + ## save word doc to temporary file + temp_word_file <- tempfile(fileext = ".docx") + print(word_doc,target = temp_word_file) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file) + } + + ## Programmatic Review + docx <- officer::read_docx(temp_word_file) + + ## get docx table contents + docx_contents <- docx$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + ## extract table contents + docx_table_body_header <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + docx_table_body_contents <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tr") %>% + setdiff(docx_table_body_header) + + ## "" at beginning for stubheader + expect_equal( + docx_table_body_header %>% + xml2::xml_find_all(".//w:p") %>% + xml2::xml_text(), + c( "", "num", "char", "currency") + ) + + expect_equal( + lapply(docx_table_body_contents, function(x) + x %>% xml2::xml_find_all(".//w:p") %>% xml2::xml_text()), + list( + "grp_a", + c("row_1", "1.111e-01", "apricot", "49.950"), + c("row_2", "2.222e+00", "banana", "17.950"), + c("row_3", "3.333e+01", "coconut", "1.390"), + c("row_4", "4.444e+02", "durian", "65100.000"), + c("avg", "120.02", "—", "—"), + c("total", "480.06", "—", "—"), + c("s.d.", "216.79", "—", "—"), + "grp_b", + c("row_5", "5.550e+03", "NA", "1325.810"), + c("row_6", "NA", "fig", "13.255"), + c("row_7", "7.770e+05", "grapefruit", "NA"), + c("row_8", "8.880e+06", "honeydew", "0.440"), + c("avg", "3,220,850.00", "—", "—"), + c("total", "9,662,550.00", "—", "—"), + c("s.d.", "4,916,123.25", "—", "—") + ) + ) +}) + +test_that("tables with footnotes can be added to a word doc", { + + skip_on_ci() + check_suggests_xml() + + ## simple table + gt_exibble_min <- exibble[1:2,] %>% + gt() %>% + tab_footnote( + footnote = md("this is a footer example"), + locations = cells_column_labels(columns = num ) + ) %>% + tab_footnote( + footnote = md("this is a second footer example"), + locations = cells_column_labels(columns = char ) + ) + + ## Add table to empty word document + word_doc <- officer::read_docx() %>% + body_add_gt( + gt_exibble_min, + align = "center" + ) + + ## save word doc to temporary file + temp_word_file <- tempfile(fileext = ".docx") + print(word_doc,target = temp_word_file) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file) + } + + ## Programmatic Review + docx <- officer::read_docx(temp_word_file) + + ## get docx table contents + docx_contents <- docx$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + ## extract table contents + docx_table_body_header <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + docx_table_body_contents <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tr") %>% + setdiff(docx_table_body_header) + + + ## superscripts will display as "true#false" due to + ## xml being: + ## true1false, + ## and being converted to TRUE due to italic being true, then the superscript, then turning off italics + expect_equal( + docx_table_body_header %>% + xml2::xml_find_all(".//w:p") %>% + xml2::xml_text(), + c("numtrue1false", "chartrue2false", "fctr", + "date", "time","datetime", + "currency", "row", "group") + ) + + ## superscripts will display as "true##" due to + ## xml being: + ## true1, + ## and being converted to TRUE due to italic being true, then the superscript, + expect_equal( + lapply(docx_table_body_contents, function(x) + x %>% xml2::xml_find_all(".//w:p") %>% xml2::xml_text()), + list( + c( + "0.1111", + "apricot", + "one", + "2015-01-15", + "13:35", + "2018-01-01 02:22", + "49.95", + "row_1", + "grp_a" + ), + c( + "2.2220", + "banana", + "two", + "2015-02-15", + "14:40", + "2018-02-02 14:33", + "17.95", + "row_2", + "grp_a" + ), + c("true1this is a footer example"), + c("true2this is a second footer example") + ) + ) +}) + +test_that("tables with source notes can be added to a word doc", { + + skip_on_ci() + check_suggests_xml() + + ## simple table + gt_exibble_min <- exibble[1:2, ] %>% + gt() %>% + tab_source_note(source_note = "this is a source note example") + + ## Add table to empty word document + word_doc <- officer::read_docx() %>% + body_add_gt(gt_exibble_min, + align = "center") + + ## save word doc to temporary file + temp_word_file <- tempfile(fileext = ".docx") + print(word_doc, target = temp_word_file) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file) + } + + ## Programmatic Review + docx <- officer::read_docx(temp_word_file) + + ## get docx table contents + docx_contents <- docx$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + ## extract table contents + docx_table_body_header <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + docx_table_body_contents <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tr") %>% + setdiff(docx_table_body_header) + + expect_equal( + docx_table_body_header %>% + xml2::xml_find_all(".//w:p") %>% + xml2::xml_text(), + c( + "num", + "char", + "fctr", + "date", + "time", + "datetime", + "currency", + "row", + "group" + ) + ) + + expect_equal( + lapply(docx_table_body_contents, function(x) + x %>% xml2::xml_find_all(".//w:p") %>% xml2::xml_text()), + list( + c( + "0.1111", + "apricot", + "one", + "2015-01-15", + "13:35", + "2018-01-01 02:22", + "49.95", + "row_1", + "grp_a" + ), + c( + "2.2220", + "banana", + "two", + "2015-02-15", + "14:40", + "2018-02-02 14:33", + "17.95", + "row_2", + "grp_a" + ), + c("this is a source note example") + ) + ) +}) + +test_that("long tables can be added to a word doc", { + + skip_on_ci() + check_suggests_xml() + + ## simple table + gt_letters <- tibble::tibble( + upper_case = c(LETTERS,LETTERS), + lower_case = c(letters,letters) + ) %>% + gt() %>% + tab_header( + title = "LETTERS" + ) + + ## Add table to empty word document + word_doc <- officer::read_docx() %>% + body_add_gt( + gt_letters, + align = "center" + ) + + ## save word doc to temporary file + temp_word_file <- tempfile(fileext = ".docx") + print(word_doc,target = temp_word_file) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file) + } + + ## Programmatic Review + docx <- officer::read_docx(temp_word_file) + + ## get docx table contents + docx_contents <- docx$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + ## extract table caption + docx_table_caption_text <- docx_contents[1] %>% + xml2::xml_text() + + ## extract table contents + docx_table_body_header <- docx_contents[2] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + docx_table_body_contents <- docx_contents[2] %>% + xml2::xml_find_all(".//w:tr") %>% + setdiff(docx_table_body_header) + + expect_equal( + docx_table_caption_text, + c("Table SEQ Table \\* ARABIC 1: LETTERS") + ) + + expect_equal( + docx_table_body_header %>% + xml2::xml_find_all(".//w:p") %>% + xml2::xml_text(), + c("upper_case", "lower_case") + ) + + expect_equal( + lapply(docx_table_body_contents, function(x) + x %>% xml2::xml_find_all(".//w:p") %>% xml2::xml_text()), + lapply(c(1:26,1:26),function(i)c(LETTERS[i], letters[i])) + ) +}) + +test_that("long tables with spans can be added to a word doc", { + + skip_on_ci() + check_suggests_xml() + + ## simple table + gt_letters <- tibble::tibble( + upper_case = c(LETTERS,LETTERS), + lower_case = c(letters,letters) + ) %>% + gt() %>% + tab_header( + title = "LETTERS" + ) %>% + tab_spanner( + "LETTERS", + columns = 1:2 + ) + + ## Add table to empty word document + word_doc <- officer::read_docx() %>% + body_add_gt( + gt_letters, + align = "center" + ) + + ## save word doc to temporary file + temp_word_file <- tempfile(fileext = ".docx") + print(word_doc,target = temp_word_file) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file) + } + + ## Programmatic Review + docx <- officer::read_docx(temp_word_file) + + ## get docx table contents + docx_contents <- docx$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + ## extract table caption + docx_table_caption_text <- docx_contents[1] %>% + xml2::xml_text() + + ## extract table contents + docx_table_body_header <- docx_contents[2] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + docx_table_body_contents <- docx_contents[2] %>% + xml2::xml_find_all(".//w:tr") %>% + setdiff(docx_table_body_header) + + expect_equal( + docx_table_caption_text, + c("Table SEQ Table \\* ARABIC 1: LETTERS") + ) + + expect_equal( + docx_table_body_header %>% + xml2::xml_find_all(".//w:p") %>% + xml2::xml_text(), + c("LETTERS", "upper_case", "lower_case") + ) + + expect_equal( + lapply(docx_table_body_contents, function(x) + x %>% xml2::xml_find_all(".//w:p") %>% xml2::xml_text()), + lapply(c(1:26,1:26),function(i)c(LETTERS[i], letters[i])) + ) +}) + +test_that("tables with cell & text coloring can be added to a word doc - no spanner", { + + skip_on_ci() + check_suggests_xml() + + ## simple table + gt_exibble_min <- exibble[1:4,] %>% + gt(rowname_col = "char") %>% + tab_row_group("My Row Group 1",c(1:2)) %>% + tab_row_group("My Row Group 2",c(3:4)) %>% + tab_style( + style = cell_fill(color = "orange"), + locations = cells_body( + columns = c(num,fctr,time,currency, group) + ) + ) %>% + tab_style( + style = cell_text( + color = "green", + font = "Biome" + ), + locations = cells_stub() + ) %>% + tab_style( + style = cell_text(size = 25, v_align = "middle"), + locations = cells_body( + columns = c(num,fctr,time,currency, group) + ) + ) %>% + tab_style( + style = cell_text( + color = "blue", + stretch = "extra-expanded" + ), + locations = cells_row_groups() + ) %>% + tab_style( + style = cell_text(color = "teal"), + locations = cells_column_labels() + ) %>% + tab_style( + style = cell_fill(color = "green"), + locations = cells_column_labels() + ) %>% + tab_style( + style = cell_fill(color = "pink"), + locations = cells_stubhead() + ) + + if (!testthat::is_testing() & interactive()) { + print(gt_exibble_min) + } + + ## Add table to empty word document + word_doc <- officer::read_docx() %>% + body_add_gt( + gt_exibble_min, + align = "center" + ) + + ## save word doc to temporary file + temp_word_file <- tempfile(fileext = ".docx") + print(word_doc,target = temp_word_file) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file) + } + + ## Programmatic Review + docx <- officer::read_docx(temp_word_file) + + ## get docx table contents + docx_contents <- docx$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + ## extract table contents + docx_table_body_header <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + docx_table_body_contents <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tr") %>% + setdiff(docx_table_body_header) + + ## header + expect_equal( + docx_table_body_header %>% xml2::xml_find_all(".//w:p") %>% xml2::xml_text(), + c("","num", "fctr","date", "time","datetime","currency", "row", "group") + ) + expect_equal( + lapply(docx_table_body_header, function(x) x %>% xml2::xml_find_all(".//w:shd") %>% xml2::xml_attr(attr = "fill")), + list(c("FFC0CB","00FF00","00FF00","00FF00","00FF00","00FF00","00FF00","00FF00","00FF00")) + ) + expect_equal( + lapply(docx_table_body_header, function(x) x %>% xml2::xml_find_all(".//w:color") %>% xml2::xml_attr(attr = "val")), + list(c("008080","008080","008080","008080","008080","008080","008080","008080")) + ) + + ## cell background styling + expect_equal( + lapply(docx_table_body_contents, function(x) { + x %>% xml2::xml_find_all(".//w:tc") %>% lapply(function(y) { + y %>% xml2::xml_find_all(".//w:shd") %>% xml2::xml_attr(attr = "fill") + })}), + list( + list(character()), + list(character(),"FFA500","FFA500",character(),"FFA500",character(),"FFA500",character(),"FFA500"), + list(character(),"FFA500","FFA500",character(),"FFA500",character(),"FFA500",character(),"FFA500"), + list(character()), + list(character(),"FFA500","FFA500",character(),"FFA500",character(),"FFA500",character(),"FFA500"), + list(character(),"FFA500","FFA500",character(),"FFA500",character(),"FFA500",character(),"FFA500") + ) + ) + + ## cell text styling + expect_equal( + lapply(docx_table_body_contents, function(x) { + x %>% xml2::xml_find_all(".//w:tc") %>% lapply(function(y) { + y %>% xml2::xml_find_all(".//w:color") %>% xml2::xml_attr(attr = "val") + })}), + list( + list("0000FF"), + list("00FF00",character(),character(),character(),character(),character(),character(),character(),character()), + list("00FF00",character(),character(),character(),character(),character(),character(),character(),character()), + list("0000FF"), + list("00FF00",character(),character(),character(),character(),character(),character(),character(),character()), + list("00FF00",character(),character(),character(),character(),character(),character(),character(),character()) + ) + ) + + expect_equal( + lapply(docx_table_body_contents, function(x) + x %>% xml2::xml_find_all(".//w:p") %>% xml2::xml_text()), + list( + "My Row Group 2", + c( + "coconut", + "33.3300", + "three", + "2015-03-15", + "15:45", + "2018-03-03 03:44", + "1.39", + "row_3", + "grp_a" + ), + c( + "durian", + "444.4000", + "four", + "2015-04-15", + "16:50", + "2018-04-04 15:55", + "65100.00", + "row_4", + "grp_a" + ), + "My Row Group 1", + c( + "apricot", + "0.1111", + "one", + "2015-01-15", + "13:35", + "2018-01-01 02:22", + "49.95", + "row_1", + "grp_a" + ), + c( + "banana", + "2.2220", + "two", + "2015-02-15", + "14:40", + "2018-02-02 14:33", + "17.95", + "row_2", + "grp_a" + ) + ) + ) +}) + +test_that("tables with cell & text coloring can be added to a word doc - with spanners", { + + skip_on_ci() + check_suggests_xml() + + ## simple table + gt_exibble_min <- exibble[1:4,] %>% + gt(rowname_col = "char") %>% + tab_row_group("My Row Group 1",c(1:2)) %>% + tab_row_group("My Row Group 2",c(3:4)) %>% + tab_spanner("My Span Label", columns = 1:5) %>% + tab_spanner("My Span Label top", columns = 2:4, level = 2) %>% + tab_style( + style = cell_text(color = "purple"), + locations = cells_column_labels() + ) %>% + tab_style( + style = cell_fill(color = "green"), + locations = cells_column_labels() + ) %>% + tab_style( + style = cell_fill(color = "orange"), + locations = cells_column_spanners("My Span Label") + ) %>% + tab_style( + style = cell_fill(color = "red"), + locations = cells_column_spanners("My Span Label top") + ) %>% + tab_style( + style = cell_fill(color = "pink"), + locations = cells_stubhead() + ) + + if (!testthat::is_testing() & interactive()) { + print(gt_exibble_min) + } + + ## Add table to empty word document + word_doc <- officer::read_docx() %>% + body_add_gt( + gt_exibble_min, + align = "center" + ) + + ## save word doc to temporary file + temp_word_file <- tempfile(fileext = ".docx") + print(word_doc,target = temp_word_file) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file) + } + + ## Programmatic Review + docx <- officer::read_docx(temp_word_file) + + ## get docx table contents + docx_contents <- docx$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + ## extract table contents + docx_table_body_header <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + ## header + expect_equal( + docx_table_body_header %>% xml2::xml_find_all(".//w:p") %>% xml2::xml_text(), + c("", "", "My Span Label top", "", "", "", "", "", + "", "My Span Label", "", "", "", "", + "", "num", "fctr", "date", "time", "datetime", "currency", "row", "group") + ) + expect_equal( + lapply(docx_table_body_header, function(x) { + x %>% xml2::xml_find_all(".//w:tc") %>% lapply(function(y) { + y %>% xml2::xml_find_all(".//w:shd") %>% xml2::xml_attr(attr = "fill") + })}), + list( + list("FFC0CB", character(0), "FF0000", character(0), character(0), character(0), character(0), character(0)), + list(character(0), "FFA500", character(0), character(0), character(0), character(0)), + list(character(0), "00FF00", "00FF00", "00FF00", "00FF00", "00FF00", "00FF00", "00FF00", "00FF00") + ) + ) + expect_equal( + lapply(docx_table_body_header, function(x) { + x %>% xml2::xml_find_all(".//w:tc") %>% lapply(function(y) { + y %>% xml2::xml_find_all(".//w:color") %>% xml2::xml_attr(attr = "val") + })}), + list( + list(character(0), character(0), character(0), character(0),character(0), character(0), character(0), character(0)), + list(character(0), character(0), character(0), character(0),character(0), character(0)), + list(character(0), "A020F0","A020F0", "A020F0", "A020F0", "A020F0", "A020F0", "A020F0","A020F0") + ) + ) +}) + +test_that("tables with cell & text coloring can be added to a word doc - with source_notes and footnotes", { + + skip_on_ci() + check_suggests_xml() + + ## simple table + gt_exibble_min <- exibble[1:2,] %>% + gt() %>% + tab_source_note("My Source Note") %>% + tab_footnote("My Footnote") %>% + tab_footnote("My Footnote 2", locations = cells_column_labels(1)) %>% + tab_style( + style = cell_text(color = "orange"), + locations = cells_source_notes() + ) %>% + tab_style( + style = cell_text(color = "purple"), + locations = cells_footnotes() + ) + + if (!testthat::is_testing() & interactive()) { + print(gt_exibble_min) + } + + ## Add table to empty word document + word_doc <- officer::read_docx() %>% + body_add_gt( + gt_exibble_min, + align = "center" + ) + + ## save word doc to temporary file + temp_word_file <- tempfile(fileext = ".docx") + print(word_doc,target = temp_word_file) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file) + } + + ## Programmatic Review + docx <- officer::read_docx(temp_word_file) + + ## get docx table contents + docx_contents <- docx$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + docx_table_body_header <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + docx_table_meta_info <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tr") %>% + setdiff(docx_table_body_header) %>% + tail(3) + + ## header + expect_equal( + docx_table_meta_info %>% lapply(function(x) x %>% xml2::xml_find_all(".//w:t") %>% xml2::xml_text()), + list( + c("", "My Footnote"), + c("1", "My Footnote 2"), + c("My Source Note") + ) + ) + + expect_equal( + lapply(docx_table_meta_info, function(x) { + x %>% xml2::xml_find_all(".//w:tc") %>% lapply(function(y) { + y %>% xml2::xml_find_all(".//w:color") %>% xml2::xml_attr(attr = "val") + })}), + list( + list(c("A020F0", "A020F0")), + list(c("A020F0", "A020F0")), + list("FFA500") + ) + ) + +}) + +test_that("tables with cell & text coloring can be added to a word doc - with summaries (grand/group)", { + + skip_on_ci() + check_suggests_xml() + + ## simple table + gt_exibble_min <- exibble %>% + dplyr::select(-c(fctr, date, time, datetime)) %>% + gt(rowname_col = "row", groupname_col = "group") %>% + summary_rows( + groups = TRUE, + columns = num, + fns = list( + avg = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + s.d. = ~sd(., na.rm = TRUE) + ) + ) %>% + grand_summary_rows( + columns = num, + fns = list( + avg = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + s.d. = ~sd(., na.rm = TRUE) + ) + ) %>% + tab_style( + style = cell_text(color = "orange"), + locations = cells_summary(groups = "grp_a", columns = char) + ) %>% + tab_style( + style = cell_text(color = "green"), + locations = cells_stub_summary() + ) %>% + tab_style( + style = cell_text(color = "purple"), + locations = cells_grand_summary(columns = num, rows = 3) + ) %>% + tab_style( + style = cell_fill(color = "yellow"), + locations = cells_stub_grand_summary() + ) + + if (!testthat::is_testing() & interactive()) { + print(gt_exibble_min) + } + + ## Add table to empty word document + word_doc <- officer::read_docx() %>% + body_add_gt( + gt_exibble_min, + align = "center" + ) + + ## save word doc to temporary file + temp_word_file <- tempfile(fileext = ".docx") + print(word_doc,target = temp_word_file) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file) + } + + ## Programmatic Review + docx <- officer::read_docx(temp_word_file) + + ## get docx table contents + docx_contents <- docx$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + docx_table_body_header <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + docx_table_body_contents <- docx_contents[1] %>% + xml2::xml_find_all(".//w:tr") %>% + setdiff(docx_table_body_header) + + ## body text + expect_equal( + docx_table_body_contents %>% lapply(function(x) x %>% xml2::xml_find_all(".//w:t") %>% xml2::xml_text()), + list( + "grp_a", + c("row_1", "1.111e-01", "apricot", "49.950"), + c("row_2","2.222e+00", "banana", "17.950"), + c("row_3", "3.333e+01", "coconut","1.390"), + c("row_4", "4.444e+02", "durian", "65100.000"), + c("avg","120.02", "—", "—"), + c("total", "480.06", "—", "—"), + c("s.d.","216.79", "—", "—"), + "grp_b", + c("row_5", "5.550e+03", "NA", "1325.810"), + c("row_6", "NA", "fig", "13.255"), + c("row_7", "7.770e+05","grapefruit", "NA"), + c("row_8", "8.880e+06", "honeydew", "0.440"), + c("avg", "3,220,850.00", "—", "—"), + c("total", "9,662,550.00","—", "—"), + c("s.d.", "4,916,123.25", "—", "—"), + c("avg", "1,380,432.87","—", "—"), + c("total", "9,663,030.06", "—", "—"), + c("s.d.", "3,319,613.32","—", "—") + ) + ) + + ## the summaries for group a and b are green, + ## the 2nd column of the group a summary is orange, + ## the 1st col, 3rd value in the grand total is purple + expect_equal( + lapply(docx_table_body_contents, function(x) { + x %>% xml2::xml_find_all(".//w:tc") %>% sapply(function(y) { + val <- y %>% xml2::xml_find_all(".//w:color") %>% xml2::xml_attr(attr = "val") + if (identical(val, character())) { + "" + }else{ + val + } + })}), + list("", + c("", "", "", ""), + c("", "", "", ""), + c("", "", "",""), + c("", "", "", ""), + c("00FF00", "", "FFA500", ""), + c("00FF00", "", "FFA500", ""), + c("00FF00", "", "FFA500", ""), + "", + c("", "", "", ""), + c("", "", "", ""), + c("", "", "", ""), + c("", "", "", ""), + c("00FF00", "", "", ""), + c("00FF00", "", "", ""), + c("00FF00", "", "", ""), + c("", "", "", ""), + c("", "", "", ""), + c("", "A020F0", "", "") + ) + ) + + ## the grand total row names fill is is yellow + expect_equal( + lapply(docx_table_body_contents, function(x) { + x %>% xml2::xml_find_all(".//w:tc") %>% sapply(function(y) { + val <- y %>% xml2::xml_find_all(".//w:shd") %>% xml2::xml_attr(attr = "fill") + if (identical(val, character())) { + "" + }else{ + val + } + })}), + list("", + c("", "", "", ""), + c("", "", "", ""), + c("", "", "", ""), + c("", "", "", ""), + c("", "", "", ""), + c("", "", "", ""), + c("", "", "", ""), + "", + c("", "", "", ""), + c("", "", "", ""), + c("", "", "", ""), + c("", "", "", ""), + c("", "", "", ""), + c("", "", "", ""), + c("", "", "", ""), + c("FFFF00", "", "", ""), + c("FFFF00", "", "", ""), + c("FFFF00", "", "", "") + ) + ) +})