diff --git a/R/export.R b/R/export.R index ca5eae4a99..33f6d0a375 100644 --- a/R/export.R +++ b/R/export.R @@ -725,15 +725,19 @@ as_latex <- function(data) { latex_packages <- NULL } + table_width_bookends <- derive_table_width_bookends(data = data) + # Compose the LaTeX table knitr::asis_output( paste0( + table_width_bookends[1L], table_start, heading_component, columns_component, body_component, table_end, footer_component, + table_width_bookends[2L], collapse = "" ), meta = latex_packages diff --git a/R/utils_render_latex.R b/R/utils_render_latex.R index 0ae6ed157d..d4120d4e85 100644 --- a/R/utils_render_latex.R +++ b/R/utils_render_latex.R @@ -183,6 +183,45 @@ create_table_start_l <- function(data) { ">{\\raggedright\\arraybackslash}" ) + # Check if column width was set using gt::pct and + # convert to Latex friendly terminology (i.e., + # '14.7%' becomes '0.147\\linewidth') + if (grepl('^[[:digit:].]+%$', col_widths[i])) { + + table_width <- dt_options_get_value(data = data, option = 'table_width') + + col_pct <- as.numeric(gsub('%$', '', col_widths[i])) / 100 + + if (table_width == 'auto') { + + # Table width not specified, use all available space + col_scalar <- col_pct + tab_unit <- '\\linewidth' + + } else if (endsWith(table_width, suffix = '%')) { + + # If table width is expressed as a percentage, adjust the scaler + col_scalar <- col_pct * as.numeric(gsub('%', '', table_width)) / 100 + tab_unit <- '\\linewidth' + + } else { + + # When table width is expressed in units, convert to points + col_scalar <- col_pct * convert_to_px(table_width) * 0.75 # 0.75 converts pixels to points + tab_unit <- 'pt' + + } + + col_widths[i] <- + paste0( + "\\dimexpr ", + col_scalar, + tab_unit, + "-2\\tabcolsep-1.5\\arrayrulewidth" + ) + + } + col_defs_i <- paste0(align, "p{", col_widths[i], "}") } else { @@ -205,11 +244,18 @@ create_table_start_l <- function(data) { paste0(col_defs[seq_along(stub_layout)], "|") } + # If a table width is specified, add an extra column + # space to fill in enough space to match the width + extra_sep <- '' + if (dt_options_get_value(data = data, option = 'table_width') != 'auto') + extra_sep <- '@{\\extracolsep{\\fill}}' + # Generate setup statements for table including default left # alignments and vertical lines for any stub columns paste0( longtable_post_length, "\\begin{longtable}{", + extra_sep, paste(col_defs, collapse = ""), "}\n", collapse = "" @@ -1103,3 +1149,70 @@ split_row_content <- function(x) { split(row_content, ceiling(seq_along(row_content) / ncol(x))) } + +derive_table_width_bookends <- function(data) { + + table_width <- dt_options_get_value(data = data, 'table_width') + + # Bookends are not required if a table width is not specified + if (table_width == 'auto') { + + bookends <- c('', '') + + } else if (endsWith(table_width, "%")) { + + tw <- as.numeric(gsub('%', '', table_width)) + + side_width <- + ((100 - tw) / 200) %>% + format(scientific = FALSE, trim = TRUE) + + bookends <- + c( + paste0( + "\\newlength\\holdLTleft", + "\\newlength\\holdLTright", + "\\setlength\\holdLTleft{\\LTleft}\\relax", + "\\setlength\\holdLTright{\\LTright}\\relax", + sprintf( + '\\setlength\\LTleft{%s\\linewidth}\n\\setlength\\LTright{%s\\linewidth}', + side_width, + side_width + ), + collapse = "\n" + ), + "\\setlength\\LTleft{\\holdLTleft}\n\\setlength\\LTright{\\holdLTright}" + ) + + } else { + + width_in_pt <- 0.75 * convert_to_px(table_width) + + halfwidth_in_pt <- format(width_in_pt / 2, scientific = FALSE, trim = TRUE) + + bookends <- + c( + paste0( + "\\newlength\\holdLTleft", + "\\newlength\\holdLTright", + "\\setlength\\holdLTleft{\\LTleft}\\relax", + "\\setlength\\holdLTright{\\LTright}\\relax", + sprintf( + "\\setlength\\LTleft{\\dimexpr(0.5\\linewidth - %spt)}\n\\setlength\\LTright{\\dimexpr(0.5\\linewidth - %spt)}", + halfwidth_in_pt, + halfwidth_in_pt + ), + collapse = '\n' + ), + paste0( + "\\setlength\\LTleft{\\holdLTleft}", + "\\setlength\\LTright{\\holdLTright}", + collapse = "\n" + ) + ) + + } + + bookends + +} diff --git a/tests/testthat/test-as_latex.R b/tests/testthat/test-as_latex.R new file mode 100644 index 0000000000..17e74a2380 --- /dev/null +++ b/tests/testthat/test-as_latex.R @@ -0,0 +1,50 @@ +test_that("Table width correctly output in LaTeX", { + + gt_latex_width_1 <- + gt(exibble) %>% + tab_options(table.width = pct(90)) %>% + as_latex() + + start_pt <- regexpr("begin\\{longtable", gt_latex_width_1) + + expect_gt(start_pt, 0) # Verifies the long table command appears in the text + + end_pt <- regexpr("end\\{longtable", gt_latex_width_1) + + expect_gt(end_pt, 0) + + # Verify that the holdLTleft and holdLTright variables are defined and set + latex_prefix <- substr(gt_latex_width_1, 1L, start_pt) + + expect_match(latex_prefix, "\\\\newlength\\\\holdLTleft") + + expect_match(latex_prefix, "\\\\newlength\\\\holdLTright") + + expect_match(latex_prefix, "\\\\setlength\\\\holdLTleft\\{\\\\LTleft\\}\\\\relax") + + expect_match(latex_prefix, "\\\\setlength\\\\holdLTright\\{\\\\LTright\\}\\\\relax") + + # Verify that LTleft and LTright are correctly specified + expect_match(latex_prefix, "\\\\setlength\\\\LTleft\\{0.05\\\\linewidth\\}") + + expect_match(latex_prefix, "\\\\setlength\\\\LTright\\{0.05\\\\linewidth\\}") + + # Verify that after the longtable environment, the LTleft and LT right are + # changed back to their previous values + latex_suffix <- substr(gt_latex_width_1, end_pt, nchar(gt_latex_width_1)) + + expect_match(latex_suffix, "\\\\setlength\\\\LTleft\\{\\\\holdLTleft\\}") + + expect_match(latex_suffix, "\\\\setlength\\\\LTright\\{\\\\holdLTright\\}") + + # Test specification of a table width in pixels + gt_latex_width_2 <- + gt(exibble) %>% + tab_options(table.width = '600px') %>% + as_latex() + + expect_match(gt_latex_width_2, "\\\\setlength\\\\LTleft\\{\\\\dimexpr\\(0.5\\\\linewidth - 225pt\\)\\}") + + expect_match(gt_latex_width_2, "\\\\setlength\\\\LTright\\{\\\\dimexpr\\(0.5\\\\linewidth - 225pt\\)\\}") + +}) diff --git a/tests/testthat/test-cols_width.R b/tests/testthat/test-cols_width.R index f96bb06276..43e830f156 100644 --- a/tests/testthat/test-cols_width.R +++ b/tests/testthat/test-cols_width.R @@ -897,3 +897,113 @@ test_that("The function `cols_width()` works correctly with a complex table", { ) %>% expect_true() }) + +test_that("The function `cols_width()` correctly specifies LaTeX table when column widths are specified by user as percentages", { + + # Check that specific suggested packages are available + check_suggests() + + # Create a `tbl_latex` object with `gt()` and size + # all columns in percentages + tbl_latex <- + gt(tbl) %>% + cols_width( + col_1 ~ pct(50), + col_2 ~ pct(30), + col_3 ~ pct(20), + col_4 ~ pct(10) + ) + + pct_string <- function(x, unit = '\\\\linewidth') { + + prefix <- '>\\{\\\\(raggedright|raggedleft|centering)\\\\arraybackslash\\}' + + sprintf( + '%sp\\{\\\\dimexpr %s%s-2\\\\tabcolsep-1.5\\\\arrayrulewidth\\}', + prefix, + format(x, scientific = FALSE, trim = TRUE), + unit + ) + + } + + build_longtable_regex <- function(...) { + + paste0( + c( + "\\\\begin\\{longtable\\}\\{", + "(@\\{\\\\extracolsep\\{\\\\fill\\}\\})*", + c(...), + "\\}\\n" + ), + collapse = '' + ) + + } + + latex_col_regex <- + paste0( + c( + "\\\\begin\\{longtable\\}\\{", + "(@\\{\\\\extracolsep\\{\\\\fill\\}\\})*", + # '>\\{\\\\ragged[[:alpha:]]+\\\\arraybackslash' + sprintf(">\\{\\\\(raggedright|raggedleft|centering)\\\\arraybackslash\\}p\\{\\\\dimexpr 0\\.%d\\\\linewidth-2\\\\tabcolsep-1.5\\\\arrayrulewidth}", + c(5L, 3L, 2L, 1L)), + "\\}\\n" + ), + collapse = '' + ) + + # Expect that all column widths are expressed as percentage of \linewidth + c(0.5, 0.3, 0.2, 0.1) %>% + pct_string() %>% + build_longtable_regex() %>% + grepl(as_latex(tbl_latex)) %>% + expect_true() + + + # Check that LaTeX is correctly generated when only some + # column widths are specified as percentages + tbl_latex_partial <- + gt(tbl) %>% + cols_width( + col_1 ~ pct(30), + col_3 ~ pct(20) + ) + + c( + pct_string(0.3), + 'r', + pct_string(0.2), + 'r' + ) %>% + build_longtable_regex() %>% + grepl(as_latex(tbl_latex_partial)) %>% + expect_true() + + # Check that LaTeX longtable command is correctly generated + # when table_width is specified by the user as a percentage + tbl_latex_tw_pct <- + tbl_latex %>% + tab_options(table.width = pct(70)) + + (0.7 * c(0.5, 0.3, 0.2, 0.1)) %>% + pct_string() %>% + build_longtable_regex() %>% + grepl(as_latex(tbl_latex_tw_pct)) %>% + expect_true() + + # Check that LaTeX longtable command is correctly generated + # when table width is specified by user in pixels + tbl_latex_tw_px <- + tbl_latex %>% + tab_options(table.width = '400px') + + (400 * 0.75 * c(0.5, 0.3, 0.2, 0.1)) %>% + pct_string(unit = 'pt') %>% + build_longtable_regex() %>% + grepl(as_latex(tbl_latex_tw_px)) + + +}) +