File: htmlTable.R

package info (click to toggle)
r-cran-htmltable 2.4.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,600 kB
  • sloc: javascript: 6,797; makefile: 2
file content (1052 lines) | stat: -rw-r--r-- 40,374 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
#' Output an HTML table
#'
#' Generates advanced HTML tables with column and row groups
#' for a dense representation of complex data. Designed for
#' maximum compatibility with copy-paste into word processors.
#' For styling, see [addHtmlTableStyle()] and [setHtmlTableTheme()].
#' *Note:* If you are using \pkg{tidyverse} and \pkg{dplyr} you may
#' want to check out [tidyHtmlTable()] that automates many of the arguments
#' that `htmlTable` requires.
#'
#' @section Multiple rows of column spanners `cgroup`:
#'
#' If you want to have a column spanner in multiple levels (rows) you can
#' set the `cgroup` and `n.cgroup` arguments to a `matrix` or `list`.
#'
#' For different level elements, set absent ones to NA in a matrix. For example,
#' `cgroup = rbind(c("first", "second", NA), c("a", "b", "c"))`.
#' And the corresponding `n.cgroup` would be `n.cgroup = rbind(c(1, 2, NA), c(2, 1, 2))`.
#' for a table consisting of 5 columns. The "first" spans the first two columns,
#' the "second" spans the last three columns, "a" spans the first two, "b"
#' the middle column, and "c" the last two columns.
#'
#' Using a list is recommended to avoid handling `NA`s.
#'
#' For an empty `cgroup`, use `""`.
#'
#' @section The `rgroup` argument:
#'
#'  The `rgroup` groups rows seamlessly. Each row in a group is indented by two
#'  spaces (unless the rgroup is `""`) and grouped by its rgroup element. The `sum(n.rgroup)`
#'  should be \if{html}{\out{≤}}\eqn{\leq} matrix rows. If fewer, remaining rows are padded with an empty rgroup (`""`). If `rgroup`
#'  has one more element than `n.rgroup`, the last `n.rgroup` is computed as `nrow(x) - sum(n.rgroup)`
#'  for a smoother table generation.
#'
#' @section The add attribute to `rgroup`:
#'
#' To add an extra element at the `rgroup` level/row, use `attr(rgroup, 'add')`.
#' The value can either be a `vector`, a `list`,
#' or a `matrix`. See `vignette("general", package = "htmlTable")` for examples.
#'
#' * A `vector` of either equal number of `rgroup`s to the number
#'   of `rgroup`s that aren't empty, i.e. `rgroup[rgroup != ""]`. Or a named vector where
#'   the name must correspond to either an `rgroup` or to an `rgroup` number.
#' * A `list` that has exactly the same requirements as the vector.
#'   In addition to the previous we can also have a list with column numbers within
#'   as names within the list.
#' * A `matrix` with the dimension `nrow(x) x ncol(x)` or
#'   `nrow(x) x 1` where the latter is equivalent to a named vector.
#'   If you have `rownames` these will resolve similarly to the names to the
#'   `list`/`vector` arguments. The same thing applies to `colnames`.
#'
#' @section Important \pkg{knitr}-note:
#'
#' This function will only work with \pkg{knitr} outputting *HTML*, i.e.
#' markdown mode. As the function returns raw HTML-code
#' the compatibility with non-HTML formatting is limited,
#' even with [pandoc](https://pandoc.org/).
#'
#' Thanks to the the [knitr::knit_print()] and the [knitr::asis_output()]
#' the `results='asis'` is *no longer needed* except within for-loops.
#' If you have a knitr-chunk with a for loop and use `print()` to produce
#' raw HTML you must set the chunk option `results='asis'`. *Note*:
#' the print-function relies on the [base::interactive()] function
#' for determining if the output should be sent to a browser or to the terminal.
#' In vignettes and other directly knitted documents you may need to either set
#' `useViewer = FALSE` alternatively set `options(htmlTable.cat = TRUE)`.
#'
#' @section RStudio's notebook:
#'
#' RStudio has an interactive notebook that allows output directly into the document.
#' In order for the output to be properly formatted it needs to have the `class`
#' of `html`. The `htmlTable` tries to identify if the environment is a
#' notebook document (uses the \pkg{rstudioapi} and identifies if its a file with and `Rmd`
#' file ending or if there is an element with `html_notebook`). If you don't want this
#' behavior you can remove it using the `options(htmlTable.skip_notebook = TRUE)`.
#'
#' @section Table counter:
#'
#' If you set the option table_counter you will get a Table 1,2,3
#' etc before each table, just set `options(table_counter=TRUE)`. If
#' you set it to a number then that number will correspond to the start of
#' the table_counter. The `table_counter` option will also contain the number
#' of the last table, this can be useful when referencing it in text. By
#' setting the option `options(table_counter_str = "<b>Table %s:</b> ")`
#' you can manipulate the counter table text that is added prior to the
#' actual caption. Note, you should use the [sprintf()] `%s`
#' instead of `%d` as the software converts all numbers to characters
#' for compatibility reasons. If you set `options(table_counter_roman = TRUE)`
#' then the table counter will use Roman numerals instead of Arabic.
#'
#' @section Empty data frames:
#' An empty data frame will result in a warning and output an empty table, provided that
#' `rgroup` and `n.rgroup` are not specified. All other row layout options will be ignored.
#'
#' @section Options:
#'
#' There are multiple options that can be set, here is a set of the perhaps most used
#' * `table_counter` - logical - activates a counter for each table
#' * `table_counter_roman` - logical - if true the counter is in Roman numbers, i.e. I, II, III, IV...
#' * `table_counter_str` - string - the string used for generating the table counter text
#' * `useViewer` - logical - if viewer should be used fro printing the table
#' * `htmlTable.cat` - logical - if the output should be directly sent to `cat()`
#' * `htmlTable.skip_notebook` - logical - skips the logic for detecting notebook
#' * `htmlTable.pretty_indentation` - logical - there was some issues in previous Pandoc versions
#'  where HTML indentation caused everything to be interpreted as code. This seems to be fixed
#'  and if you want to look at the raw HTML code it is nice to have this set to `TRUE` so that
#'  the tags and elements are properly indented.
#' * `htmlTableCompat` - string - see parameter description
#'
#' @section Other:
#'
#' *Copy-pasting:* As you copy-paste results into Word you need to keep
#' the original formatting. Either right click and choose that paste option or click
#' on the icon appearing after a paste. Currently the following compatibilities
#' have been tested with MS Word 2016:
#'
#' * **Internet Explorer** (v. 11.20.10586.0) Works perfectly when copy-pasting into Word
#' * **RStudio** (v. 0.99.448) Works perfectly when copy-pasting into Word.
#'   *Note:* can have issues with multi-line `cgroup`s -
#'   see [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=305130)
#' * **Chrome** (v. 47.0.2526.106) Works perfectly when copy-pasting into Word.
#'        *Note:* can have issues with multi-line `cgroup`s -
#'        see [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=305130)
#' * **Firefox** (v. 43.0.3) Works poorly - looses font-styling, lines and general feel
#' * **Edge** (v. 25.10586.0.0) Works poorly - looses lines and general feel
#'
#' *Direct word processor opening:* Opening directly in Libre Office or Word is no longer
#' recommended. You get much prettier results using the cut-and-paste option.
#'
#' *Google docs*: Copy-paste directly into a Google docs document is handled rather well. This
#'  seems to work especially well when the paste comes directly from a Chrome browser.
#'
#' *Note* that when using complex `cgroup` alignments with multiple levels
#' not every browser is able to handle this. For instance the RStudio
#' webkit browser seems to have issues with this and a
#' [bug has been filed](https://bugs.chromium.org/p/chromium/issues/detail?id=305130).
#'
#' As the table uses HTML for rendering you need to be aware of that headers,
#' row names, and cell values should try respect this for optimal display. Browsers
#' try to compensate and frequently the tables still turn out fine but it is
#' not advised. Most importantly you should try to use
#' `&lt;` instead of `<` and
#' `&gt;` instead of `>`. You can find a complete list
#' of HTML characters [here](https://ascii.cl/htmlcodes.htm).
#'
#' Lastly, I want to mention that function was inspired by the [Hmisc::latex()]
#' that can be an excellent alternative if you wish to switch to PDF-output.
#' For the sibling function [tidyHtmlTable()] you can directly switch between
#' the two using the `table_fn` argument.
#'
#' @param x The matrix/data.frame with the data. For the `print` and `knit_print`
#'  it takes a string of the class `htmlTable` as `x` argument.
#' @param header A vector of character strings specifying column
#'  header, defaulting to [`colnames(x)`][base::colnames]
#' @param rnames Default row names are generated from [`rownames(x)`][base::colnames]. If you
#'  provide `FALSE` then it will skip the row names. *Note:* For `data.frames`
#'  if you do [`rownames(my_dataframe) <- NULL`][base::colnames] it still has
#'  row names. Thus you need to use `FALSE` if you want to
#'  supress row names for `data.frames`.
#' @param rowlabel If the table has row names or `rnames`,
#'  `rowlabel` is a character string containing the
#'  column heading for the `rnames`.
#' @param caption Adds a table caption.
#' @param tfoot Adds a table footer (uses the `<tfoot>` HTML element). The
#'  output is run through [txtMergeLines()] simplifying the generation
#'  of multiple lines.
#' @param label A text string representing a symbolic label for the
#'  table for referencing as an anchor. All you need to do is to reference the
#'  table, for instance `<a href="#anchor_name">see table 2</a>`. This is
#'  known as the element's id attribute, i.e. table id, in HTML linguo, and should
#'  be unique id for an HTML element in contrast to the `css.class` element attribute.
#' @param rgroup A vector of character strings containing headings for row groups.
#'  `n.rgroup` must be present when `rgroup` is given. See
#'   detailed description in section below.
#' @param n.rgroup An integer vector giving the number of rows in each grouping. If `rgroup`
#'  is not specified, `n.rgroup` is just used to divide off blocks of rows by horizontal
#'  lines. If `rgroup` is given but `n.rgroup` is omitted, `n.rgroup` will
#'  default so that each row group contains the same number of rows. If you want additional
#'  rgroup column elements to the cells you can sett the "add" attribute to `rgroup` through
#'  `attr(rgroup, "add")`, see below explaining section.
#' @param cgroup A vector, matrix or list of character strings defining major column header. The default
#'  is to have none. These elements are also known as *column spanners*. If you want a column *not*
#'  to have a spanner then put that column as "". If you pass cgroup and `n.crgroup` as
#'  matrices you can have column spanners for several rows. See cgroup section below for details.
#' @param n.cgroup An integer vector, matrix or list containing the number of columns for which each element in
#'  cgroup is a heading. For example, specify `cgroup=c("Major_1","Major_2")`,
#'  `n.cgroup=c(3,3)` if `"Major_1"` is to span columns 1-3 and
#'  `"Major_2"` is to span columns 4-6.
#'  `rowlabel` does not count in the column numbers. You can omit `n.cgroup`
#'  if all groups have the same number of columns. If the `n.cgroup` is one less than
#'  the number of columns in the matrix/data.frame then it automatically adds those.
#' @param tspanner The table spanner is somewhat of a table header that
#'  you can use when you want to join different tables with the same columns.
#' @param n.tspanner An integer vector with the number of rows or `rgroup`s in the original
#'  matrix that the table spanner should span. If you have provided one fewer n.tspanner elements
#'  the last will be imputed from the number of `rgroup`s (if you have provided `rgroup` and
#'  `sum(n.tspanner) < length(rgroup)`) or the number of rows in the table.
#' @param cspan.rgroup The number of columns that an `rgroup` should span. It spans
#'  by default all columns but you may want to limit this if you have column colors
#'  that you want to retain.
#' @param total The last row is sometimes a row total with a border on top and
#'  bold fonts. Set this to `TRUE` if you are interested in such a row. If you
#'  want a total row at the end of each table spanner you can set this to `"tspanner"`.
#' @param ... Passed on to `print.htmlTable` function and any argument except the
#'  `useViewer` will be passed on to the [base::cat()] functions arguments.
#'  *Note:* as of version 2.0.0 styling options are still allowed but it is recommended
#'  to instead preprocess your object with [addHtmlTableStyle()].
#' @param ctable If the table should have a double top border or a single a' la LaTeX ctable style
#' @param compatibility Is default set to `LibreOffice` as some
#'  settings need to be in old HTML format as Libre Office can't
#'  handle some commands such as the css caption-alignment. Note: this
#'  option is not yet fully implemented for all details, in the future
#'  I aim to generate a HTML-correct table and one that is aimed
#'  at Libre Office compatibility. Word-compatibility is difficult as
#'  Word ignores most settings and destroys all layout attempts
#'  (at least that is how my 2010 version behaves). You can additinally use the
#'  `options(htmlTableCompat = "html")` if you want a change to apply
#'  to the entire document.
#'  MS Excel sometimes misinterprets certain cell data when opening HTML-tables (eg. 1/2 becomes 1. February).
#'  To avoid this please specify the correct Microsoft Office format for each cell in the table using the css.cell-argument.
#'  To make MS Excel interpret everything as text use "mso-number-format:\"\\@\"".
#' @param escape.html logical: should HTML characters be escaped? Defaults to FALSE.
#' @return Returns a formatted string representing an HTML table of class `htmlTable`.
#'
#' @example inst/examples/htmlTable_example.R
#'
#' @seealso [addHtmlTableStyle()],
#'          [setHtmlTableTheme()],
#'          [tidyHtmlTable()].
#'          [txtMergeLines()],
#'          [Hmisc::latex()]
#'
#' @export
#' @rdname htmlTable
#' @family table functions
htmlTable <- function(x,
                      header = NULL,
                      rnames = NULL,
                      rowlabel = NULL,
                      caption = NULL,
                      tfoot = NULL,
                      label = NULL,
                      # Grouping
                      rgroup = NULL,
                      n.rgroup = NULL,
                      cgroup = NULL,
                      n.cgroup = NULL,
                      tspanner = NULL,
                      n.tspanner = NULL,
                      total = NULL,
                      ctable = TRUE,
                      compatibility = getOption("htmlTableCompat", "LibreOffice"),
                      cspan.rgroup = "all",
                      escape.html = FALSE,
                      ...) {
  UseMethod("htmlTable")
}

#' @export
htmlTable.data.frame <- function(x, ...) {
  # deal gracefully with an empty data frame - issue a warning.
  if (nrow(x) == 0) {
    warning(paste(deparse(substitute(x)), "is an empty object"))
  }
  htmlTable.default(prConvertDfFactors(x), ...)
}

#' @export
htmlTable.matrix <- function(x, ...) {
  # deal gracefully with an empty matrix - issue a warning.
  if (nrow(x) == 0) {
    warning(paste(deparse(substitute(x)), "is an empty object"))
  }

  # Default to a sum-row when provided a table that
  dots <- list(...)
  if (all(class(x) %in% c("table", "matrix", "array")) &&
    !is.null(rownames(x)) &&
    grepl("^sum$", tail(rownames(x), 1), ignore.case = TRUE) &&
    is.null(dots$total)) {
    dots$total <- TRUE
  }
  dots$x <- x

  do.call(htmlTable.default, dots)
}

`.` <- "magrittr CMD check issue"

#' @importFrom stringr str_replace str_replace_all str_trim
#' @importFrom htmltools htmlEscape
#' @import checkmate
#' @import magrittr
#' @rdname htmlTable
#' @export
htmlTable.default <- function(x,
                              header = NULL,
                              rnames = NULL,
                              rowlabel = NULL,
                              caption = NULL,
                              tfoot = NULL,
                              label = NULL,
                              # Grouping
                              rgroup = NULL,
                              n.rgroup = NULL,
                              cgroup = NULL,
                              n.cgroup = NULL,
                              tspanner = NULL,
                              n.tspanner = NULL,
                              total = NULL,
                              ctable = TRUE,
                              compatibility = getOption("htmlTableCompat", "LibreOffice"),
                              cspan.rgroup = "all",
                              escape.html = FALSE,
                              ...) {
  if (isTRUE(escape.html)) {
    x <- prEscapeHtml(x)
  }

  x <- prPrepInputMatrixDimensions(x, header = header)
  dots <- list(...)
  style_dots <- names(dots) %in% Filter(
    function(x) !(x %in% c("", "x")),
    formals(addHtmlTableStyle) %>% names()
  )
  if (sum(style_dots) > 0) {
    style_dots_list <- dots[style_dots]
    dots <- dots[!style_dots]
    style_dots_list$x <- x
    x <- do.call(addHtmlTableStyle, style_dots_list)
  }

  style_list <- prGetAttrWithDefault(x,
    which = style_attribute_name,
    default = getHtmlTableTheme()
  )

  if (is.null(rgroup) && !is.null(n.rgroup)) {
    # Add "" rgroups corresponding to the n.rgroups
    rgroup <- rep("", length.out = length(n.rgroup))
  }

  # Unfortunately in knitr there seems to be some issue when the
  # rnames is specified immediately as: rnames=rownames(x)
  if (is.null(rnames)) {
    if (any(is.null(rownames(x)) == FALSE)) {
      rnames <- rownames(x)
    }

    if (any(is.null(rownames(x))) &&
      !is.null(rgroup)) {
      warning(
        "You have not specified rnames but you seem to have rgroups.",
        " If you have the first column as rowname but you want the rgroups",
        " to result in subhedings with indentation below then, ",
        " you should change the rnames to the first column and then",
        " remove it from the table matrix (the x argument object)."
      )
    }
  }

  if (!is.null(rowlabel) &&
    prSkipRownames(rnames)) {
    stop(
      "You can't have a row label and no rownames.",
      " Either remove the rowlabel argument",
      ", set the rnames argument",
      ", or set the rownames of the x argument."
    )
  }

  if (is.null(header) && !is.null(colnames(x))) {
    header <- colnames(x)
  } else if (!is.null(header)) {
    if (length(header) != ncol(x)) {
      stop(
        "You have a header with ", length(header), " cells",
        " while your output matrix has only ", ncol(x), " columns"
      )
    }
  }

  # Fix alignment to match with the matrix
  style_list$align <- prPrepareAlign(style_list$align, x = x, rnames = rnames)
  style_list$align.header <- prPrepareAlign(style_list$align.header, x = x, rnames = rnames, default_rn = "c")

  if (tolower(compatibility) %in% c(
    "libreoffice", "libre office",
    "open office", "openoffice",
    "word", "ms word", "msword"
  )) {
    compatibility <- "LibreOffice"
  }

  if (!is.null(rgroup)) {
    if (is.null(n.rgroup)) {
      stop("You need to specify the argument n.rgroup if you want to use rgroups")
    }

    if (any(n.rgroup < 1)) {
      warning(
        "You have provided rgroups with less than 1 elements,",
        " these will therefore be removed: ",
        paste(sprintf("'%s' = %d", rgroup, n.rgroup)[n.rgroup < 1],
          collapse = ", "
        )
      )
      rgroup <- rgroup[n.rgroup >= 1]
      n.rgroup <- n.rgroup[n.rgroup >= 1]
    }

    # Sanity check for rgroup
    if (sum(n.rgroup) > nrow(x)) {
      stop(
        "Your rows are fewer than suggested by the n.rgroup,",
        " i.e. ", sum(n.rgroup), "(n.rgroup) > ", nrow(x), "(rows in x)"
      )
    }

    if (sum(n.rgroup) < nrow(x) &&
      (length(n.rgroup) == length(rgroup) - 1 ||
        length(n.rgroup) == length(rgroup))) {
      # Add an empty rgroup if missing
      if (length(n.rgroup) == length(rgroup)) {
        rgroup <- c(rgroup, "")
      }
      # Calculate the remaining rows and add those
      n.rgroup <- c(n.rgroup, nrow(x) - sum(n.rgroup))
    } else if (sum(n.rgroup) != nrow(x)) {
      stop("Your n.rgroup doesn't add up")
    }


    # Sanity checks style_list$css.rgroup and prepares the style
    if (length(style_list$css.rgroup) > 1 &&
      length(style_list$css.rgroup) != length(rgroup)) {
      stop(sprintf(
        "You must provide the same number of styles as the rgroups, %d != %d",
        length(style_list$css.rgroup), length(rgroup)
      ))
    } else if (length(style_list$css.rgroup) == 1) {
      style_list$css.rgroup <- prGetStyle(style_list$css.rgroup)

      if (length(rgroup) > 0) {
        style_list$css.rgroup <- rep(style_list$css.rgroup, length.out = length(rgroup))
      }
    } else {
      for (i in 1:length(style_list$css.rgroup)) {
        style_list$css.rgroup[i] <- prGetStyle(style_list$css.rgroup[i])
      }
    }

    # Sanity checks style_list$css.rgroup.sep and prepares the style
    if (length(style_list$css.rgroup.sep) > 1 &&
      length(style_list$css.rgroup.sep) != length(rgroup) - 1) {
      stop(sprintf(
        "You must provide the same number of separators as the rgroups - 1, %d != %d",
        length(style_list$css.rgroup.sep), length(rgroup) - 1
      ))
    } else if (length(style_list$css.rgroup.sep) == 1) {
      style_list$css.rgroup.sep <- prAddSemicolon2StrEnd(style_list$css.rgroup.sep)

      if (length(rgroup) > 0) {
        style_list$css.rgroup.sep <- rep(style_list$css.rgroup.sep, length.out = length(rgroup))
      }
    } else {
      for (i in 1:length(style_list$css.rgroup.sep)) {
        style_list$css.rgroup.sep[i] <- prAddSemicolon2StrEnd(style_list$css.rgroup.sep[i])
      }
    }

    cspan.rgroup <- rep(cspan.rgroup, length.out = length(rgroup))
  }

  ## this will convert color names to hexadecimal (easier for user)
  ## but also leaves hex format unchanged
  style_list$col.rgroup <- prPrepareColors(style_list$col.rgroup, n = nrow(x), ng = n.rgroup, gtxt = rgroup)
  style_list$col.columns <- prPrepareColors(style_list$col.columns, ncol(x))

  if (!is.null(tspanner)) {

    # Sanity checks style_list$css.tspanner and prepares the style
    if (length(style_list$css.tspanner) > 1 &&
      length(style_list$css.tspanner) != length(tspanner)) {
      stop(sprintf(
        "You must provide the same number of styles as the tspanners, %d != %d",
        length(style_list$css.tspanner), length(tspanner)
      ))
    } else if (length(style_list$css.tspanner) == 1) {
      style_list$css.tspanner <- prAddSemicolon2StrEnd(style_list$css.tspanner)

      if (length(tspanner) > 0) {
        style_list$css.tspanner <- rep(style_list$css.tspanner, length.out = length(tspanner))
      }
    } else {
      for (i in 1:length(style_list$css.tspanner)) {
        style_list$css.tspanner[i] <- prAddSemicolon2StrEnd(style_list$css.tspanner[i])
      }
    }


    # Sanity checks style_list$css.tspanner.sep and prepares the style
    if (length(style_list$css.tspanner.sep) > 1 &&
      length(style_list$css.tspanner.sep) != length(tspanner) - 1) {
      stop(sprintf(
        "You must provide the same number of separators as the tspanners - 1, %d != %d",
        length(style_list$css.tspanner.sep), length(tspanner) - 1
      ))
    } else if (length(style_list$css.tspanner.sep) == 1) {
      style_list$css.tspanner.sep <- prGetStyle(style_list$css.tspanner.sep)

      if (length(tspanner) > 0) {
        style_list$css.tspanner.sep <- rep(style_list$css.tspanner.sep, length.out = length(tspanner) - 1)
      }
    } else {
      for (i in 1:length(style_list$css.tspanner.sep)) {
        style_list$css.tspanner.sep[i] <- prGetStyle(style_list$css.tspanner.sep[i])
      }
    }
  }

  # Convert dimnames to something useful
  if (!is.null(names(dimnames(x)))) {
    # First dimname is always the variable name for the row
    dimname4row <- names(dimnames(x))[1]
    if (!is.null(dimname4row) && dimname4row != "") {
      # Use rgroup or tspanner as this is visually more separated than rowlabel
      # if these are available
      if (is.null(rgroup)) {
        rgroup <- dimname4row
        n.rgroup <- nrow(x)
      } else if (is.null(tspanner)) {
        tspanner <- dimname4row
        n.tspanner <- nrow(x)
      } else if (is.null(rowlabel)) {
        rowlabel <- dimname4row
      }
    }

    # Second dimname is always the variable name for the columns
    dimname4col <- names(dimnames(x))[2]
    if (!is.null(dimname4col) && dimname4col != "") {
      # Use rgroup or tspanner as this is visually more separated than rowlabel
      # if these are available
      if (is.null(cgroup)) {
        cgroup <- dimname4col
        n.cgroup <- ncol(x)

        # If this is a addmargins object we shouldn't have the cspanner including the
        # sum marker
        if (!is.null(total) && total &&
          grepl("^sum$", tail(colnames(x), 1), ignore.case = TRUE)) {
          cgroup %<>% c("")
          n.cgroup <- c(n.cgroup[1] - 1, 1)
        }
      }
    }
  }

  # Sanity check for tspanner
  if (!is.null(tspanner)) {
    if (is.null(n.tspanner)) {
      stop("You need to specify the argument n.tspanner if you want to use table spanners")
    }

    if (any(n.tspanner < 1)) {
      stop(
        "You have  provided invalid number of rows in the n.tspanner argument - minimum is 1, you have: ",
        vector2string(n.tspanner),
        " where no. ", vector2string(which(n.tspanner)),
        " was less than 1"
      )
    }
    if (length(n.tspanner) == length(tspanner) - 1) {
      if (is.null(rgroup) || sum(n.tspanner) > length(rgroup)) {
        n.tspanner <- append(n.tspanner, nrow(x) - sum(n.tspanner))
      } else {
        n.tspanner <- append(n.tspanner, length(rgroup) - sum(n.tspanner))
      }
    }
    if (any(n.tspanner < 1)) {
      stop("You have more tspannners than n.tspanner while the number of rows doesn't leave room for more tspanners")
    }

    if (sum(n.tspanner) != nrow(x)) {
      if (is.null(rgroup)) {
        stop(sprintf(
          "Your rows don't match in the n.tspanner, i.e. %d != %d",
          sum(n.tspanner), nrow(x)
        ))
      }

      if (sum(n.tspanner) != length(rgroup)) {
        stop(sprintf(
          "Your rows don't match either the total number of rows '%d'
                     or the number of rgroups '%d' the sum of n.tspanner %d",
          nrow(x),
          length(rgroup),
          sum(n.tspanner)
        ))
      }

      org_nt <- n.tspanner
      for (i in 1:length(n.tspanner)) {
        offset <- sum(org_nt[0:(i - 1)]) + 1
        n.tspanner[i] <- sum(n.rgroup[offset:(offset + org_nt[i] - 1)])
      }
    }

    # Make sure there are no collisions with rgrou
    if (!is.null(n.rgroup)) {
      for (i in 1:length(n.tspanner)) {
        rows <- sum(n.tspanner[1:i])
        if (!rows %in% cumsum(n.rgroup)) {
          stop(
            "There is no splitter that matches the table spanner ",
            tspanner[i],
            " (no. ", i, ") with rgroup splits.",
            " The missing row splitter should be on row number ", rows,
            " and is not in the n.rgroup list: ", vector2string(n.rgroup),
            " note, it should match the cumulative sum n.rgroup", vector2string(cumsum(n.rgroup))
          )
        }
      }
    }
  }

  # With multiple rows in cgroup we need to keep track of
  # how many spacer cells occur between the groups
  cgroup_spacer_cells <- rep(0, times = (ncol(x) - 1))

  # Sanity check for cgroup
  if (!is.null(cgroup)) {
    ret <- prPrepareCgroup(
      x = x,
      cgroup = cgroup,
      n.cgroup = n.cgroup,
      style_list = style_list
    )

    cgroup <- ret$cgroup
    n.cgroup <- ret$n.cgroup
    cgroup_spacer_cells <- ret$cgroup_spacer_cells
    style_list$align.cgroup <- ret$align.cgroup
    style_list$css.cgroup <- ret$css.cgroup
  }

  style_list$pos.rowlabel <- prGetRowlabelPos(cgroup, style_list$pos.rowlabel, header)

  tc <- getOption("table_counter", FALSE)
  if (tc) {
    # Count which table it currently is
    if (is.numeric(tc)) {
      tc <- tc + 1
    } else {
      tc <- 1
    }
    options(table_counter = tc)
  }

  # The id works just as well as any anchor
  table_id <- getOption("table_counter", "")
  if (!is.null(label)) {
    table_id <- sprintf(" id='%s'", label)
  } else if (is.numeric(table_id)) {
    table_id <- paste0(" id='table_", table_id, "'")
  } else if (table_id == FALSE) {
    table_id <- ""
  }

  # A column counter that is used for <td colspan="">
  total_columns <- ncol(x) + !prSkipRownames(rnames)
  if (!is.null(cgroup)) {
    if (!is.matrix(cgroup)) {
      total_columns <- total_columns + length(cgroup) - 1
    } else {
      total_columns <- total_columns + sum(cgroup_spacer_cells) * prGetEmptySpacerCellSize(style_list = style_list)
    }
  }

  if (is.null(total) ||
    (is.logical(total) &&
      all(total == FALSE))) {
    total <- c()
  } else if (is.logical(total)) {
    if (length(total) == 1) {
      total <- nrow(x)
    } else if (length(total) == nrow(x)) {
      total <- which(total)
    } else if (!is.null(n.tspanner) &&
      length(total) == length(n.tspanner)) {
      total <- cumsum(n.tspanner)[total]
    } else {
      stop(
        "You have provided an invalid 'total' argument:",
        " '", paste(total, collapse = "', '"), "'.",
        " Logical values accepted are either single TRUE elements",
        ", of the same length as the output matrix (", nrow(x), ")",
        ", or of the same length as the tspanner (",
        ifelse(is.null(n.tspanner), "not provided", length(n.tspanner)), ")."
      )
    }
  } else if (is.numeric(total)) {
    if (any(!total %in% 1:nrow(x))) {
      stop(
        "You have indicated an invalid row as the total row.",
        " Valid rows are only 1 to ", nrow(x),
        " and you have provided invalid row(s): ",
        "'", paste(total[!total %in% 1:nrow(x)], collapse = "', '"), "'"
      )
    }
  } else if (all(total == "tspanner")) {
    total <- cumsum(n.tspanner)
  } else {
    stop(
      "You have provided an invalid 'total' argument:",
      " '", paste(total, collapse = "', '"), "' ",
      " of the class ", paste(class(total), collapse = " & "), ".",
      " The function currently only accepts logical or numerical",
      " values."
    )
  }

  style_list$css.total <- rep(style_list$css.total, length.out = length(total))

  assert(
    check_matrix(style_list$css.cell),
    check_character(style_list$css.cell)
  )
  prepped_cell_css <- prPrepareCss(x,
    css = style_list$css.cell,
    rnames = rnames, header = header,
    style_list = style_list
  )

  ###############################
  # Start building table string #
  ###############################
  table_str <- str_interp(
    "<table class='${CLASS_NAME}' style='border-collapse: collapse; ${TABLE_CSS}' ${TABLE_ID}>",
    list(
      CLASS_NAME = paste(style_list$css.class, collapse = ", "),
      TABLE_CSS = paste(style_list$css.table, collapse = "; "),
      TABLE_ID = table_id
    )
  )

  # Theoretically this should be added to the table but the
  # import to word processors works then less well and therefore I've
  # constructed this work-around with borders for the top and bottom cells
  first_row <- TRUE
  if (isTRUE(ctable)) {
    top_row_style <- "border-top: 2px solid grey;"
    bottom_row_style <- "border-bottom: 2px solid grey;"
  } else if (any(ctable %in% c("single", "double"))) {
    ctable <- rep_len(ctable, 2L)
    ctable[ctable %in% "single"] <- "solid"
    top_row_style <- ifelse(ctable[1] == "solid", "border-top: 2px solid grey;", "border-top: 4px double grey;")
    bottom_row_style <- ifelse(ctable[2] == "solid",
      "border-bottom: 2px solid grey;",
      "border-bottom: 4px double grey;"
    )
  } else {
    top_row_style <- "border-top: 4px double grey;"
    bottom_row_style <- "border-bottom: 1px solid grey;"
  }


  # Add caption according to standard HTML
  if (!is.null(caption)) {
    # Combine a table counter if provided
    caption <- paste0("\n\t", prTblNo(caption))

    if (compatibility != "LibreOffice") {
      if (style_list$pos.caption %in% c("bottom", "below")) {
        table_str %<>% paste0("\n\t<caption style='caption-side: bottom'>")
      } else {
        table_str %<>% paste0("\n\t<caption style='caption-side: top'>")
      }

      table_str %<>% paste0(caption, "</caption>")
    }
  }

  if (!is.null(header) ||
    !is.null(cgroup) ||
    !is.null(caption)) {
    thead <- prGetThead(
      x = x,
      header = header,
      cgroup = cgroup,
      n.cgroup = n.cgroup,
      caption = caption,
      compatibility = compatibility,
      total_columns = total_columns,
      style_list = style_list,
      top_row_style = top_row_style,
      rnames = rnames,
      rowlabel = rowlabel,
      cgroup_spacer_cells = cgroup_spacer_cells,
      prepped_cell_css = prepped_cell_css,
      cell_style = cell_style
    )
    first_row <- FALSE
    table_str %<>%
      paste0(thead)
  }

  table_str %<>%
    paste0("\n\t<tbody>")

  if (is.null(rgroup)) {
    row_clrs <- style_list$col.rgroup
  } else {
    row_clrs <- unlist(attr(style_list$col.rgroup, "group"))
  }

  rgroup_iterator <- 0
  tspanner_iterator <- 0
  if (nrow(x) > 0) {
    for (row_nr in 1:nrow(x)) {
      rname_style <- attr(prepped_cell_css, "rnames")[row_nr]

      # First check if there is a table spanner that should be applied
      if (!is.null(tspanner) &&
        (row_nr == 1 ||
          row_nr > sum(n.tspanner[1:tspanner_iterator]))) {
        tspanner_iterator <- tspanner_iterator + 1

        rs <- c(
          rname_style,
          style_list$css.tspanner[tspanner_iterator]
        )

        # Use a separator from the one above if this
        # at least the second spanner. Graphically this
        # appears as if underneath the group while it's
        # actually above but this merges into one line
        if (tspanner_iterator > 1) {
          rs %<>%
            c(style_list$css.tspanner.sep[tspanner_iterator - 1])
        }


        if (first_row) {
          rs %<>%
            c(top_row_style)
        }

        table_str %<>%
          sprintf(
            "%s\n\t<tr><td colspan='%d' style='%s'>%s</td></tr>",
            .,
            total_columns,
            prGetStyle(rs),
            tspanner[tspanner_iterator]
          )
        first_row <- FALSE
      }


      # Add the row group if any
      # and it's:
      # - first row
      # - the row belongs to the next row group
      rgroup_sep_style <- FALSE
      if (!is.null(rgroup) &&
        (row_nr == 1 ||
          row_nr > sum(n.rgroup[1:rgroup_iterator]))) {
        rgroup_iterator <- rgroup_iterator + 1

        rs <- c(rname_style,
          style_list$css.rgroup[rgroup_iterator],
          `background-color` = style_list$col.rgroup[rgroup_iterator]
        )

        # Use a separator from the one above if this
        # at least the second group. Graphically this
        # appears as if underneath the group while it's
        # actually above but this merges into one line
        if (rgroup_iterator > 1) {
          rs <- c(
            rs,
            style_list$css.rgroup.sep[rgroup_iterator - 1]
          )
        }

        # Only add if there is anything in the group
        if (is.na(rgroup[rgroup_iterator]) == FALSE &&
          rgroup[rgroup_iterator] != "") {
          if (first_row) {
            rs <- c(
              rs,
              top_row_style
            )
          }

          rgroup_str <- prGetRgroupLine(
            x = x,
            total_columns = total_columns,
            rgroup = rgroup,
            rgroup_iterator = rgroup_iterator,
            cspan = cspan.rgroup[rgroup_iterator],
            rnames = rnames,
            style = rs,
            cgroup_spacer_cells = cgroup_spacer_cells,
            style_list = style_list,
            prepped_row_css = prepped_cell_css[row_nr, ]
          )

          table_str %<>%
            paste(rgroup_str)

          first_row <- FALSE
        } else if (rgroup_iterator > 1 && style_list$css.rgroup.sep[rgroup_iterator - 1] != "") {
          # Add the separator if the rgroup wasn't added so that it's included in the regular cells
          rgroup_sep_style <- style_list$css.rgroup.sep[rgroup_iterator - 1]
        }
      }


      cell_style <- rs <- paste("background-color:", row_clrs[row_nr])
      if (first_row) {
        rs %<>%
          c(top_row_style)
        cell_style %<>%
          c(top_row_style)
      } else if (rgroup_sep_style != FALSE) {
        rs %<>% c(rgroup_sep_style)
      }
      first_row <- FALSE

      if (row_nr == nrow(x)) {
        cell_style %<>%
          c(bottom_row_style)
      }

      if (row_nr %in% total) {
        cell_style %<>%
          c(style_list$css.total[which(row_nr == total)])
      }

      if (prGetStyle(rs) == "") {
        table_str %<>%
          paste0("\n\t<tr>")
      } else {
        table_str %<>%
          sprintf(
            "%s\n\t<tr style='%s'>",
            .,
            prGetStyle(rs)
          )
      }

      if (!prSkipRownames(rnames)) {
        pdng <- style_list$padding.tspanner
        # Minor change from original function. If the group doesn't have
        # a group name then there shouldn't be any indentation
        if (!is.null(rgroup) &&
          rgroup_iterator > 0 &&
          is.na(rgroup[rgroup_iterator]) == FALSE &&
          rgroup[rgroup_iterator] != "") {
          pdng %<>%
            paste0(style_list$padding.rgroup)
        }

        # The padding doesn't work well with the Word import - well nothing really works well with word...
        # table_str <- sprintf("%s\n\t\t<td style='padding-left: .5em;'>%s</td>", table_str, rnames[row_nr])
        table_str %<>% paste(str_interp(
          "<td style='${STYLE}'>${PADDING}${NAME}</td>",
          list(
            STYLE = prGetStyle(c(rname_style, cell_style),
              align = prGetAlign(style_list$align, index = 1, style_list = style_list)
            ),
            PADDING = pdng,
            NAME = rnames[row_nr]
          )
        ),
        sep = "\n\t\t"
        )
      }

      cell_str <- prAddCells(
        rowcells = x[row_nr, ],
        cellcode = "td",
        style_list = style_list,
        style = cell_style,
        cgroup_spacer_cells = cgroup_spacer_cells,
        has_rn_col = !prSkipRownames(rnames) * 1,
        prepped_cell_css = prepped_cell_css[row_nr, ]
      )
      table_str %<>%
        paste0(cell_str, "\n\t</tr>")
    }
  }
  # Close body
  table_str %<>%
    paste0("\n\t</tbody>")

  if (!is.null(caption) &
    compatibility == "LibreOffice" &
    style_list$pos.caption %in% c("bottom", "below")) {
    table_str %<>%
      sprintf(
        "%s\n\t<tr><td colspan='%d' style='text-align: left;'>%s</td></tr>",
        .,
        total_columns,
        caption
      )
  }

  # Add footer
  if (!is.null(tfoot)) {
    table_str %<>%
      sprintf(
        "%s\n\t<tfoot><tr><td colspan='%d'>",
        .,
        total_columns
      )

    # Add the body
    table_str %<>%
      paste0("\n\t", txtMergeLines(tfoot))

    table_str %<>% paste0("</td></tr></tfoot>")
  }

  # Close table
  table_str %<>%
    paste0("\n</table>")

  # Fix indentation issue with pandoc v1.13 - can be overridden if you want to look at a pretty `cat()`
  if (!getOption("htmlTable.pretty_indentation", default = FALSE)) {
    table_str %<>% gsub("\t", "", .)
  }

  # HTML favors UTF-8 and thus the string should be encoded as utf8
  table_str <- enc2utf8(table_str)
  class(table_str) <- c("htmlTable", class(table_str))
  attr(table_str, "...") <- dots
  attr(table_str, "html") <- TRUE

  # Add html class if this is a table inside a notebook for inline output
  if (!getOption("htmlTable.skip_notebook", FALSE) && prIsNotebook()) {
    class(table_str) <- c("html", class(table_str))
  }

  return(table_str)
}

#' @importFrom methods setClass
setClass("htmlTable", contains = "character")