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", "", "", "")
+ )
+ )
+})
|