blob: 7930d2874ca0fff4cd6e48407b1dd24c7c46725a [file] [log] [blame]
Hao Zhud4f54982020-10-07 16:26:35 -04001#' 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
14header_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
30header_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
76process_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
95header_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