diff --git a/NEWS.md b/NEWS.md index 388ac45f01..c71d8c77ce 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,9 @@ * Update word processing to htmlEscape all characters before it goes into xml_t. This means now footnotes are escaped too. (#1303) +* Update word processing to handle the new `summary_rows()` and `grand_summary_rows()` `side` argument, as well as handle cases where a table doesn't have rownames. (#1325) + + # gt 0.9.0 ## New features diff --git a/R/utils_render_xml.R b/R/utils_render_xml.R index 0ea6096652..c4bbd43ef1 100644 --- a/R/utils_render_xml.R +++ b/R/utils_render_xml.R @@ -1543,7 +1543,7 @@ create_body_component_xml <- function( # Determine whether the stub is available through analysis # of the `stub_components` - stub_available <- dt_stub_components_has_rowname(stub_components) + stub_available <- dt_stub_components_has_rowname(stub_components) || summaries_present # Obtain all of the visible (`"default"`), non-stub # column names for the table @@ -1599,6 +1599,7 @@ create_body_component_xml <- function( body_section <- list() + # # Create a group heading row # @@ -1660,8 +1661,9 @@ create_body_component_xml <- function( row_cells <- list() row_idx <- i + row_vec <- output_df_row_as_vec(i) - for (y in seq_along(output_df_row_as_vec(i))) { + for (y in seq_along(row_vec)) { style_col_idx <- ifelse(stub_available, y - 1, y) @@ -1673,11 +1675,12 @@ create_body_component_xml <- function( colnum == style_col_idx ) %>% dplyr::pull("styles") %>% - .[1] %>% .[[1]] + .[1] %>% + .[[1]] row_cells[[length(row_cells) + 1]] <- xml_table_cell( - content = output_df_row_as_vec(i)[y], + content = row_vec[y], font = cell_style[["cell_text"]][["font"]], size = cell_style[["cell_text"]][["size"]], color = cell_style[["cell_text"]][["color"]], @@ -1712,43 +1715,57 @@ create_body_component_xml <- function( ) ) - body_section <- append(body_section, list(body_row)) + # - # Add groupwise summary rows + # Add groupwise summary rows. # - if (summaries_present && i %in% groups_rows_df$row_end) { + if (summaries_present && nrow(groups_rows_df) > 0){ - group_id <- - groups_rows_df[ - stats::na.omit(groups_rows_df$row_end == i), - "group_id", drop = TRUE - ] + group_info <- groups_rows_df[ + i >= groups_rows_df$row_start & i <= groups_rows_df$row_end, ] - 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 - ) + group_summary_row_side <- unique(group_info[, "summary_row_side"])[[1]] + + group_row_add_row_loc <- group_info[,ifelse(group_summary_row_side == "top", "row_start","row_end")][[1]] - body_section <- append(body_section, summary_section) + if(i == group_row_add_row_loc) { + + summary_styles <- + styles_tbl %>% + dplyr::filter( + locname %in% c("summary_cells"), + grpname %in% group_info[["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_info[["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 + ) + + if(group_summary_row_side == "top"){ + body_section <- append(body_section, summary_section) + body_section <- append(body_section, list(body_row)) + }else{ + body_section <- append(body_section, list(body_row)) + body_section <- append(body_section, summary_section) + } + }else{ + body_section <- append(body_section, list(body_row)) + } + }else{ + body_section <- append(body_section, list(body_row)) } body_section @@ -1784,7 +1801,13 @@ create_body_component_xml <- function( keep_with_next = keep_with_next ) - body_rows <- c(body_rows, grand_summary_section) + grand_summary_loc <- unique(list_of_summaries$summary_df_display_list[[grand_summary_col]][["::side::"]]) + + if(grand_summary_loc == "top"){ + body_rows <- c(grand_summary_section, body_rows) + }else{ + body_rows <- c(body_rows, grand_summary_section) + } } htmltools::tagList(body_rows) diff --git a/tests/testthat/test-as_word.R b/tests/testthat/test-as_word.R index f5c18bc576..2701aae790 100644 --- a/tests/testthat/test-as_word.R +++ b/tests/testthat/test-as_word.R @@ -256,7 +256,6 @@ test_that("word ooxml escapes special characters in gt object footer", { }) - test_that("tables can be added to a word doc", { check_suggests_xml() @@ -807,8 +806,223 @@ test_that("tables with summaries can be added to a word doc", { c("s.d.", "4,916,123.25", "—", "—") ) ) + + + ## Now place the summary on the top + + ## simple table + gt_exibble_min_top <- exibble %>% + dplyr::select(-c(fctr, date, time, datetime)) %>% + gt(rowname_col = "row", groupname_col = "group") %>% + summary_rows( + groups = everything(), + columns = num, + fns = list( + avg = ~mean(., na.rm = TRUE), + total = ~sum(., na.rm = TRUE), + s.d. = ~sd(., na.rm = TRUE) + ), + fmt = list(~ fmt_number(.)), + side = "top" + ) + + ## Add table to empty word document + word_doc_top <- officer::read_docx() %>% + body_add_gt( + gt_exibble_min_top, + align = "center" + ) + + ## save word doc to temporary file + temp_word_file_top <- tempfile(fileext = ".docx") + print(word_doc_top,target = temp_word_file_top) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file_top) + } + + ## Programmatic Review + docx_top <- officer::read_docx(temp_word_file_top) + + ## get docx table contents + docx_contents_top <- docx_top$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + ## extract table contents + docx_table_body_header_top <- docx_contents_top[1] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + docx_table_body_contents_top <- docx_contents_top[1] %>% + xml2::xml_find_all(".//w:tr") %>% + setdiff(docx_table_body_header_top) + + ## "" at beginning for stubheader + expect_equal( + docx_table_body_header_top %>% + xml2::xml_find_all(".//w:p") %>% + xml2::xml_text(), + c( "", "num", "char", "currency") + ) + + expect_equal( + lapply(docx_table_body_contents_top, function(x) + x %>% xml2::xml_find_all(".//w:p") %>% xml2::xml_text()), + list( + "grp_a", + c("avg", "120.02", "—", "—"), + c("total", "480.06", "—", "—"), + c("s.d.", "216.79", "—", "—"), + 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"), + "grp_b", + c("avg", "3,220,850.00", "—", "—"), + c("total", "9,662,550.00", "—", "—"), + c("s.d.", "4,916,123.25", "—", "—"), + 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") + ) + ) + +}) + +test_that("tables with grand summaries but no rownames can be added to a word doc", { + + check_suggests_xml() + + ## simple table + gt_exibble_min <- exibble %>% + dplyr::select(-c(fctr, date, time, datetime, row, group)) %>% + dplyr::slice(1:3) %>% + gt() %>% + grand_summary_rows( + c(everything(), -char), + fns = c("Total" = ~length(.)) + ) + + ## 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( + c("", "0.1111", "apricot", "49.95"), + c("", "2.2220", "banana","17.95"), + c("", "33.3300", "coconut", "1.39"), + c("Total", "3","—", "3") + ) + ) + + ## simple table + gt_exibble_min_top <- exibble %>% + dplyr::select(-c(fctr, date, time, datetime, row, group)) %>% + dplyr::slice(1:3) %>% + gt() %>% + grand_summary_rows( + c(everything(), -char), + fns = c("Total" = ~length(.)), + side = "top" + ) + + ## Add table to empty word document + word_doc_top <- officer::read_docx() %>% + body_add_gt( + gt_exibble_min_top, + align = "center" + ) + + ## save word doc to temporary file + temp_word_file_top <- tempfile(fileext = ".docx") + print(word_doc_top,target = temp_word_file_top) + + ## Manual Review + if (!testthat::is_testing() & interactive()) { + shell.exec(temp_word_file_top) + } + + ## Programmatic Review + docx_top <- officer::read_docx(temp_word_file_top) + + ## get docx table contents + docx_contents_top <- docx_top$doc_obj$get() %>% + xml2::xml_children() %>% + xml2::xml_children() + + ## extract table contents + docx_table_body_header_top <- docx_contents_top[1] %>% + xml2::xml_find_all(".//w:tblHeader/ancestor::w:tr") + + docx_table_body_contents_top <- docx_contents_top[1] %>% + xml2::xml_find_all(".//w:tr") %>% + setdiff(docx_table_body_header_top) + + ## "" at beginning for stubheader + expect_equal( + docx_table_body_header_top %>% + xml2::xml_find_all(".//w:p") %>% + xml2::xml_text(), + c( "", "num", "char", "currency") + ) + + expect_equal( + lapply(docx_table_body_contents_top, function(x) + x %>% xml2::xml_find_all(".//w:p") %>% xml2::xml_text()), + list( + c("Total", "3","—", "3"), + c("", "0.1111", "apricot", "49.95"), + c("", "2.2220", "banana","17.95"), + c("", "33.3300", "coconut", "1.39") + ) + ) + + }) + + test_that("tables with footnotes can be added to a word doc", { check_suggests_xml()