added header_separate function based on the suggestion in #541
diff --git a/NAMESPACE b/NAMESPACE
index 005da73..8f9df81 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -16,6 +16,7 @@
 export(footnote_marker_number)
 export(footnote_marker_symbol)
 export(group_rows)
+export(header_separate)
 export(html_dependency_bsTable)
 export(html_dependency_kePrint)
 export(html_dependency_lightable)
diff --git a/R/header_separate.R b/R/header_separate.R
new file mode 100644
index 0000000..7930d28
--- /dev/null
+++ b/R/header_separate.R
@@ -0,0 +1,135 @@
+#' Separate table headers and add additional header rows based on grouping
+#'
+#' @description When you create a summary table for either model or basic
+#' summary stats in R, you usually end up having column names in the form of
+#' "a_mean", "a_sd", "b_mean" and "b_sd". This function streamlines the process
+#' of renaming these column names and adding extra header rows using
+#' `add_header_above`.
+#'
+#' @param kable_input Output of `knitr::kable()` with `format` specified
+#' @param sep A regular expression separator between groups. The default value
+#' is a regular expression that matches any sequence of non-alphanumeric values.
+#'
+#' @export
+header_separate <- function(kable_input, sep = "[^[:alnum:]]+") {
+  kable_format <- attr(kable_input, "format")
+  if (!kable_format %in% c("html", "latex")) {
+    warning("Please specify format in kable. kableExtra can customize either ",
+            "HTML or LaTeX outputs. See https://haozhu233.github.io/kableExtra/ ",
+            "for details.")
+    return(kable_input)
+  }
+  if (kable_format == "html") {
+    return(header_separate_html(kable_input, sep))
+  }
+  if (kable_format == "latex") {
+    return(header_separate_latex(kable_input, sep))
+  }
+}
+
+header_separate_html <- function(kable_input, sep) {
+  kable_attrs <- attributes(kable_input)
+  kable_xml <- kable_as_xml(kable_input)
+
+  kable_thead <- xml_tpart(kable_xml, "thead")
+  thead_depth <- length(xml_children(kable_thead))
+
+  if (thead_depth > 1) {
+    warning("Your table already has more than 1 rows of thead. header_separate ",
+            "won't work in this case and is returning the original input. ")
+    return(kable_input)
+  }
+
+  original_header_row <- xml_child(kable_thead, thead_depth)
+  original_header_cells <- lapply(
+    xml_children(original_header_row),
+    function(x) trimws(as.character(xml2::xml_contents(x)))
+  )
+
+  header_sep <- stringr::str_split(original_header_cells, sep)
+  header_layers <- process_header_sep(header_sep)
+  new_header_row_one <- lapply(header_layers[[1]], function(x) {
+    paste0("<th>", x, "</th>")
+  })
+
+  # Fix the original header row
+  for (i in seq(length(header_sep))) {
+    new_header_row_one[[i]] <- xml2::read_html(new_header_row_one[[i]])
+    xml2::xml_attrs(new_header_row_one[[i]]) <-
+      xml2::xml_attrs(xml_child(original_header_row, i))
+    xml2::xml_replace(xml_child(original_header_row, i),
+                      new_header_row_one[[i]])
+  }
+
+  out <- as_kable_xml(kable_xml)
+  attributes(out) <- kable_attrs
+  if (!"kableExtra" %in% class(out)) class(out) <- c("kableExtra", class(out))
+
+  for (l in seq(2, length(header_layers))) {
+    out <- kableExtra::add_header_above(
+      out, kableExtra::auto_index(header_layers[[l]])
+      )
+  }
+  return(out)
+}
+
+process_header_sep <- function(header_sep) {
+  max_depth <- max(unlist(lapply(header_sep, length)))
+  header_layers <- list()
+  for (i in seq(max_depth)) {
+    header_layers[[i]] <- list()
+    for (j in seq(1, length(header_sep))) {
+      layer_length <- length(header_sep[[j]])
+      if (layer_length > 0) {
+        header_layers[[i]][[j]] <- header_sep[[j]][layer_length]
+        header_sep[[j]] <- header_sep[[j]][-layer_length]
+      } else {
+        header_layers[[i]][[j]] <- " "
+      }
+    }
+  }
+  header_layers <- lapply(header_layers, unlist)
+  return(header_layers)
+}
+
+header_separate_latex <- function(kable_input, sep) {
+  table_info <- magic_mirror(kable_input)
+  out <- solve_enc(kable_input)
+
+  if (table_info$duplicated_rows) {
+    dup_fx_out <- fix_duplicated_rows_latex(out, table_info)
+    out <- dup_fx_out[[1]]
+    table_info <- dup_fx_out[[2]]
+  }
+
+  if (!is.null(table_info$new_header_row)) {
+    warning("Your table already has more than 1 rows of thead. header_separate ",
+            "won't work in this case and is returning the original input. ")
+    return(kable_input)
+  }
+
+  original_header_cells <- str_split(table_info$contents[1], " & ")[[1]]
+
+  header_sep <- stringr::str_split(original_header_cells, sep)
+  header_layers <- process_header_sep(header_sep)
+
+  # Fix the original header row
+  new_header_row_one <- paste0(header_layers[[1]], collapse = ' & ')
+
+  out <- stringr::str_replace(out, paste0(table_info$contents[1], "\\\\\\\\"),
+                              paste0(new_header_row_one, "\\\\\\\\"))
+  table_info$contents[1] <- new_header_row_one
+
+  out <- structure(out, format = "latex", class = "knitr_kable")
+  attr(out, "kable_meta") <- table_info
+
+  for (l in seq(2, length(header_layers))) {
+    out <- kableExtra::add_header_above(
+      out, kableExtra::auto_index(header_layers[[l]])
+    )
+  }
+
+  return(out)
+}
+
+
diff --git a/docs/awesome_table_in_html.html b/docs/awesome_table_in_html.html
index f088dcf..dbb0f4e 100644
--- a/docs/awesome_table_in_html.html
+++ b/docs/awesome_table_in_html.html
@@ -11,7 +11,7 @@
 
 <meta name="author" content="Hao Zhu" />
 
-<meta name="date" content="2020-10-05" />
+<meta name="date" content="2020-10-06" />
 
 <title>Create Awesome HTML Table with knitr::kable and kableExtra</title>
 
@@ -6333,7 +6333,7 @@
 
 <h1 class="title toc-ignore">Create Awesome HTML Table with knitr::kable and kableExtra</h1>
 <h4 class="author">Hao Zhu</h4>
-<h4 class="date">2020-10-05</h4>
+<h4 class="date">2020-10-06</h4>
 
 </div>
 
@@ -11328,7 +11328,7 @@
 1
 </td>
 <td style="text-align:center;">
-1
+0
 </td>
 </tr>
 <tr>
@@ -11344,7 +11344,7 @@
 3
 </td>
 <td style="text-align:center;">
-1
+0
 </td>
 </tr>
 <tr>
@@ -11360,7 +11360,7 @@
 5
 </td>
 <td style="text-align:center;">
-0
+1
 </td>
 </tr>
 <tr>
@@ -11403,7 +11403,7 @@
 10
 </td>
 <td style="text-align:center;">
-0
+1
 </td>
 </tr>
 <tr>
@@ -11417,7 +11417,7 @@
 11
 </td>
 <td style="text-align:center;">
-0
+1
 </td>
 </tr>
 <tr>
@@ -11436,7 +11436,7 @@
 13
 </td>
 <td style="text-align:center;">
-1
+0
 </td>
 </tr>
 <tr>
@@ -11452,7 +11452,7 @@
 15
 </td>
 <td style="text-align:center;">
-0
+1
 </td>
 </tr>
 </tbody>
@@ -16746,8 +16746,8 @@
 <pre class="r"><code># Not evaluated
 library(sparkline)
 sparkline(0)</code></pre>
-<span id="htmlwidget-b273e58dc9cf32d23728" class="sparkline html-widget"></span>
-<script type="application/json" data-for="htmlwidget-b273e58dc9cf32d23728">{"x":{"values":0,"options":{"height":20,"width":60},"width":60,"height":20},"evals":[],"jsHooks":[]}</script>
+<span id="htmlwidget-6114469567685ebe0b50" class="sparkline html-widget"></span>
+<script type="application/json" data-for="htmlwidget-6114469567685ebe0b50">{"x":{"values":0,"options":{"height":20,"width":60},"width":60,"height":20},"evals":[],"jsHooks":[]}</script>
 <pre class="r"><code>spk_dt &lt;- data.frame(
   var = c(&quot;mpg&quot;, &quot;wt&quot;),
   sparkline = c(spk_chr(mtcars$mpg), spk_chr(mtcars$wt))
@@ -16772,8 +16772,8 @@
 mpg
 </td>
 <td style="text-align:left;">
-<span id="htmlwidget-ff58ca5b629d76fb5d1c" class="sparkline html-widget"></span>
-<script type="application/json" data-for="htmlwidget-ff58ca5b629d76fb5d1c">{"x":{"values":[21,21,22.8,21.4,18.7,18.1,14.3,24.4,22.8,19.2,17.8,16.4,17.3,15.2,10.4,10.4,14.7,32.4,30.4,33.9,21.5,15.5,15.2,13.3,19.2,27.3,26,30.4,15.8,19.7,15,21.4],"options":{"height":20,"width":60},"width":60,"height":20},"evals":[],"jsHooks":[]}</script>
+<span id="htmlwidget-926649ac136aa8f3326f" class="sparkline html-widget"></span>
+<script type="application/json" data-for="htmlwidget-926649ac136aa8f3326f">{"x":{"values":[21,21,22.8,21.4,18.7,18.1,14.3,24.4,22.8,19.2,17.8,16.4,17.3,15.2,10.4,10.4,14.7,32.4,30.4,33.9,21.5,15.5,15.2,13.3,19.2,27.3,26,30.4,15.8,19.7,15,21.4],"options":{"height":20,"width":60},"width":60,"height":20},"evals":[],"jsHooks":[]}</script>
 </td>
 </tr>
 <tr>
@@ -16781,8 +16781,8 @@
 wt
 </td>
 <td style="text-align:left;">
-<span id="htmlwidget-a5ea7d5f0f8b0e5e3af5" class="sparkline html-widget"></span>
-<script type="application/json" data-for="htmlwidget-a5ea7d5f0f8b0e5e3af5">{"x":{"values":[2.62,2.875,2.32,3.215,3.44,3.46,3.57,3.19,3.15,3.44,3.44,4.07,3.73,3.78,5.25,5.424,5.345,2.2,1.615,1.835,2.465,3.52,3.435,3.84,3.845,1.935,2.14,1.513,3.17,2.77,3.57,2.78],"options":{"height":20,"width":60},"width":60,"height":20},"evals":[],"jsHooks":[]}</script>
+<span id="htmlwidget-42f9c5dad4ac6b3ba181" class="sparkline html-widget"></span>
+<script type="application/json" data-for="htmlwidget-42f9c5dad4ac6b3ba181">{"x":{"values":[2.62,2.875,2.32,3.215,3.44,3.46,3.57,3.19,3.15,3.44,3.44,4.07,3.73,3.78,5.25,5.424,5.345,2.2,1.615,1.835,2.465,3.52,3.435,3.84,3.845,1.935,2.14,1.513,3.17,2.77,3.57,2.78],"options":{"height":20,"width":60},"width":60,"height":20},"evals":[],"jsHooks":[]}</script>
 </td>
 </tr>
 </tbody>
diff --git a/docs/plots_in_tables.Rmd b/docs/plots_in_tables.Rmd
new file mode 100644
index 0000000..95766c8
--- /dev/null
+++ b/docs/plots_in_tables.Rmd
@@ -0,0 +1,22 @@
+---
+title: "In-table Plots"
+output: 
+  html_document:
+    theme: cosmo
+---
+
+```{r, include=F}
+knitr::opts_chunk$set(warning = F, message = F)
+```
+
+Sometimes it's a delight to see plots in a table. Here we provide some examples and recipes on some common tasks. We will use the following packages.
+
+
+```{r}
+library(kableExtra)
+library(plotly)
+library(sparkline)
+library(tidyverse)
+```
+
+
diff --git a/inst/NEWS.md b/inst/NEWS.md
index c38c202..e3ea48d 100644
--- a/inst/NEWS.md
+++ b/inst/NEWS.md
@@ -1,3 +1,15 @@
+kableExtra 1.3.0
+--------------------------------------------------------------------------------
+
+# Major Change
+
+* Added a `header_separate` function that simplifies the workflow of adding 
+header rows to grouped data. For example, if the original dataframe has columns 
+in the form of "var", "a_mean", "a_sd", "b_mean" and "b_sd", this 
+`header_separate` will change the column names of the 1st row to be "var", 
+"mean", "sd", "mean" and "sd" and add an additional row with group a and b 
+there.
+
 kableExtra 1.2.1
 --------------------------------------------------------------------------------
 
diff --git a/man/header_separate.Rd b/man/header_separate.Rd
new file mode 100644
index 0000000..60a5d31
--- /dev/null
+++ b/man/header_separate.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/header_separate.R
+\name{header_separate}
+\alias{header_separate}
+\title{Separate table headers and add additional header rows based on grouping}
+\usage{
+header_separate(kable_input, sep = "[^[:alnum:]]+")
+}
+\arguments{
+\item{kable_input}{Output of \code{knitr::kable()} with \code{format} specified}
+
+\item{sep}{A regular expression separator between groups. The default value
+is a regular expression that matches any sequence of non-alphanumeric values.}
+}
+\description{
+When you create a summary table for either model or basic
+summary stats in R, you usually end up having column names in the form of
+"a_mean", "a_sd", "b_mean" and "b_sd". This function streamlines the process
+of renaming these column names and adding extra header rows using
+\code{add_header_above}.
+}