Hao Zhu | d4f5498 | 2020-10-07 16:26:35 -0400 | [diff] [blame^] | 1 | #' Separate table headers and add additional header rows based on grouping |
| 2 | #' |
| 3 | #' @description When you create a summary table for either model or basic |
| 4 | #' summary stats in R, you usually end up having column names in the form of |
| 5 | #' "a_mean", "a_sd", "b_mean" and "b_sd". This function streamlines the process |
| 6 | #' of renaming these column names and adding extra header rows using |
| 7 | #' `add_header_above`. |
| 8 | #' |
| 9 | #' @param kable_input Output of `knitr::kable()` with `format` specified |
| 10 | #' @param sep A regular expression separator between groups. The default value |
| 11 | #' is a regular expression that matches any sequence of non-alphanumeric values. |
| 12 | #' |
| 13 | #' @export |
| 14 | header_separate <- function(kable_input, sep = "[^[:alnum:]]+") { |
| 15 | kable_format <- attr(kable_input, "format") |
| 16 | if (!kable_format %in% c("html", "latex")) { |
| 17 | warning("Please specify format in kable. kableExtra can customize either ", |
| 18 | "HTML or LaTeX outputs. See https://haozhu233.github.io/kableExtra/ ", |
| 19 | "for details.") |
| 20 | return(kable_input) |
| 21 | } |
| 22 | if (kable_format == "html") { |
| 23 | return(header_separate_html(kable_input, sep)) |
| 24 | } |
| 25 | if (kable_format == "latex") { |
| 26 | return(header_separate_latex(kable_input, sep)) |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | header_separate_html <- function(kable_input, sep) { |
| 31 | kable_attrs <- attributes(kable_input) |
| 32 | kable_xml <- kable_as_xml(kable_input) |
| 33 | |
| 34 | kable_thead <- xml_tpart(kable_xml, "thead") |
| 35 | thead_depth <- length(xml_children(kable_thead)) |
| 36 | |
| 37 | if (thead_depth > 1) { |
| 38 | warning("Your table already has more than 1 rows of thead. header_separate ", |
| 39 | "won't work in this case and is returning the original input. ") |
| 40 | return(kable_input) |
| 41 | } |
| 42 | |
| 43 | original_header_row <- xml_child(kable_thead, thead_depth) |
| 44 | original_header_cells <- lapply( |
| 45 | xml_children(original_header_row), |
| 46 | function(x) trimws(as.character(xml2::xml_contents(x))) |
| 47 | ) |
| 48 | |
| 49 | header_sep <- stringr::str_split(original_header_cells, sep) |
| 50 | header_layers <- process_header_sep(header_sep) |
| 51 | new_header_row_one <- lapply(header_layers[[1]], function(x) { |
| 52 | paste0("<th>", x, "</th>") |
| 53 | }) |
| 54 | |
| 55 | # Fix the original header row |
| 56 | for (i in seq(length(header_sep))) { |
| 57 | new_header_row_one[[i]] <- xml2::read_html(new_header_row_one[[i]]) |
| 58 | xml2::xml_attrs(new_header_row_one[[i]]) <- |
| 59 | xml2::xml_attrs(xml_child(original_header_row, i)) |
| 60 | xml2::xml_replace(xml_child(original_header_row, i), |
| 61 | new_header_row_one[[i]]) |
| 62 | } |
| 63 | |
| 64 | out <- as_kable_xml(kable_xml) |
| 65 | attributes(out) <- kable_attrs |
| 66 | if (!"kableExtra" %in% class(out)) class(out) <- c("kableExtra", class(out)) |
| 67 | |
| 68 | for (l in seq(2, length(header_layers))) { |
| 69 | out <- kableExtra::add_header_above( |
| 70 | out, kableExtra::auto_index(header_layers[[l]]) |
| 71 | ) |
| 72 | } |
| 73 | return(out) |
| 74 | } |
| 75 | |
| 76 | process_header_sep <- function(header_sep) { |
| 77 | max_depth <- max(unlist(lapply(header_sep, length))) |
| 78 | header_layers <- list() |
| 79 | for (i in seq(max_depth)) { |
| 80 | header_layers[[i]] <- list() |
| 81 | for (j in seq(1, length(header_sep))) { |
| 82 | layer_length <- length(header_sep[[j]]) |
| 83 | if (layer_length > 0) { |
| 84 | header_layers[[i]][[j]] <- header_sep[[j]][layer_length] |
| 85 | header_sep[[j]] <- header_sep[[j]][-layer_length] |
| 86 | } else { |
| 87 | header_layers[[i]][[j]] <- " " |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 | header_layers <- lapply(header_layers, unlist) |
| 92 | return(header_layers) |
| 93 | } |
| 94 | |
| 95 | header_separate_latex <- function(kable_input, sep) { |
| 96 | table_info <- magic_mirror(kable_input) |
| 97 | out <- solve_enc(kable_input) |
| 98 | |
| 99 | if (table_info$duplicated_rows) { |
| 100 | dup_fx_out <- fix_duplicated_rows_latex(out, table_info) |
| 101 | out <- dup_fx_out[[1]] |
| 102 | table_info <- dup_fx_out[[2]] |
| 103 | } |
| 104 | |
| 105 | if (!is.null(table_info$new_header_row)) { |
| 106 | warning("Your table already has more than 1 rows of thead. header_separate ", |
| 107 | "won't work in this case and is returning the original input. ") |
| 108 | return(kable_input) |
| 109 | } |
| 110 | |
| 111 | original_header_cells <- str_split(table_info$contents[1], " & ")[[1]] |
| 112 | |
| 113 | header_sep <- stringr::str_split(original_header_cells, sep) |
| 114 | header_layers <- process_header_sep(header_sep) |
| 115 | |
| 116 | # Fix the original header row |
| 117 | new_header_row_one <- paste0(header_layers[[1]], collapse = ' & ') |
| 118 | |
| 119 | out <- stringr::str_replace(out, paste0(table_info$contents[1], "\\\\\\\\"), |
| 120 | paste0(new_header_row_one, "\\\\\\\\")) |
| 121 | table_info$contents[1] <- new_header_row_one |
| 122 | |
| 123 | out <- structure(out, format = "latex", class = "knitr_kable") |
| 124 | attr(out, "kable_meta") <- table_info |
| 125 | |
| 126 | for (l in seq(2, length(header_layers))) { |
| 127 | out <- kableExtra::add_header_above( |
| 128 | out, kableExtra::auto_index(header_layers[[l]]) |
| 129 | ) |
| 130 | } |
| 131 | |
| 132 | return(out) |
| 133 | } |
| 134 | |
| 135 | |