## state completely sucks and I hate it, but2
## we need a pdf device open to calculate the3
## print width of strings, and we can't be opening4
## a new one every time we want to5
font_dev_state <- new.env()6
font_dev_state$open <- FALSE7
font_dev_state$fontspec <- list()8
font_dev_state$spacewidth <- NA_real_9
font_dev_state$ismonospace <- NA10
font_dev_state$max_ratio <- NA_real_11
font_dev_state$dev_num <- NA_integer_12
font_dev_state$debug_active <- FALSE13 14 15
cwidth_inches_unsafe <- function(x) {16 1136x
convertWidth(unit(1, "strwidth", x), "inches", valueOnly = TRUE)17
}18 19
## returns whether it opened a new device or not20
#' Activate font state21
#'22
#' @param fontspec (`font_spec`)\cr a font_spec object specifying the font information to use for23
#' calculating string widths and heights, as returned by [font_spec()].24
#' @param silent (`logical(1)`)\cr If `FALSE`, the default, a warning will be25
#' emitted if this function switches away from an active graphics device.26
#'27
#' @details The font device state is an environment with28
#' four variables guaranteed to be set:29
#'30
#' \describe{31
#' \item{`open`}{(`logical(1)`)\cr whether a device is already open with font info}32
#' \item{`fontspec`}{(`font_spec`)\cr the font specification, if any, that is currently active (`list()` if none is).}33
#' \item{`spacewidth`}{(`numeric(1)`)\cr the width of the space character in the currently active font.}34
#' \item{`ismonospace`}{(`logical(1)`)\cr whether the specified font is monospaced.}35
#' }36
#'37
#' `open_font_dev` opens a pdf device with the specified font38
#' only if there is not one currently open with the same font.39
#' If a new device is opened, it caches `spacewidth` and40
#' `ismonospace` for use in `nchar_ttype`).41
#'42
#' `close_font_dev` closes any open font state device43
#' and clears the cached values.44
#'45
#' `debug_font_dev` and `undebug_font_dev` activate and deactivate, respectively,46
#' logging of where in the call stack font devices are being opened.47
#'48
#' @return49
#' - `open_font_dev` returns a logical value indicating whether a *new* pdf device was opened.50
#' - `close_font_dev`, `debug_font_dev` and `undebug_font_dev` return `NULL`.51
#'52
#' In all cases the value is returned invisibly.53
#'54
#' @examples55
#' open_font_dev(font_spec("Times"))56
#' nchar_ttype("Hiya there", font_spec("Times"))57
#' close_font_dev()58
#'59
#' @export60
open_font_dev <- function(fontspec, silent = FALSE) {61 82300x
if (is.null(fontspec)) {62 1x
return(invisible(FALSE))63 82298x
} else if (font_dev_is_open()) {64 81788x
if (identical(font_dev_state$fontspec, fontspec)) {65 81787x
if (!silent && dev.cur() != font_dev_state$dev_num) {66 1x
warning(67 1x
"formatters is switching to the font state graphics device ",68 1x
"to perform string width calculations. You may need to switch ",69 1x
"to your currently open graphics device, depending on whether ",70 1x
"the font device is closed and what other devices you have open."71
)72 1x
dev.set(font_dev_state$dev_num)73
}74 81787x
return(invisible(FALSE))75
} else {76 1x
close_font_dev()77
}78
}79 511x
if (font_dev_state$debug_active && !font_dev_is_open()) { ## call debug_font_dev beforehand to get debugging info to helplocate places which aren't receiving/using the state properly # nolint80
## dump the call stack any time we have cache misses81
## and have to open a completely new font state device82 1x
scalls <- sys.calls()83 1x
msg <- sapply(84 1x
scalls[2:length(scalls)],85 1x
function(sci) {86 42x
toret <- deparse(sci[[1]], nlines = 3)87 42x
if (substr(toret[1], 1, 8) == "function") {88 !
toret <- "anon function"89
}90 42x
toret91
}92
)93 1x
message(paste("\n***** START font dev debugging dump *****\n",94 1x
paste(msg, collapse = " -> "),95 1x
paste(capture.output(print(fontspec)), collapse = "\n"),96 1x
sep = "\n"97
))98
}99 511x
tmppdf <- tempfile(fileext = ".pdf")100 511x
pdf(tmppdf)101 511x
grid.newpage()102 511x
gp <- gpar_from_fspec(fontspec)103 511x
pushViewport(plotViewport(gp = gp))104 511x
spcwidth <- cwidth_inches_unsafe(" ")105 511x
assign("open", TRUE, envir = font_dev_state)106 511x
assign("fontspec", fontspec, envir = font_dev_state)107 511x
assign("spacewidth", spcwidth, envir = font_dev_state)108 511x
assign("ismonospace", spcwidth == cwidth_inches_unsafe("W"),109 511x
envir = font_dev_state110
)111 511x
assign("dev_num", dev.cur(),112 511x
envir = font_dev_state113
)114 511x
invisible(TRUE)115
}116 117
#' @rdname open_font_dev118
#' @export119
close_font_dev <- function() {120 511x
if (font_dev_state$open) {121 511x
dev.off(font_dev_state$dev_num)122 511x
assign("open", FALSE, envir = font_dev_state)123 511x
assign("fontspec", list(), envir = font_dev_state)124 511x
assign("spacewidth", NA_real_, envir = font_dev_state)125 511x
assign("ismonospace", NA, envir = font_dev_state)126 511x
assign("dev_num", NA_integer_, envir = font_dev_state)127
}128 511x
invisible(NULL)129
}130 131
#' @rdname open_font_dev132
#' @export133
debug_font_dev <- function() {134 1x
message("debugging font device swapping. call undebug_font_dev() to turn debugging back off.")135 1x
font_dev_state$debug_active <- TRUE136 1x
invisible(NULL)137
}138 139
#' @rdname open_font_dev140
#' @export141
undebug_font_dev <- function() {142 1x
message("no longer debugging font device swapping.")143 1x
font_dev_state$debug_active <- FALSE144 1x
invisible(NULL)145
}146 147 148
## can only be called when font_dev_state$open is TRUE149
get_space_width <- function() {150 25x
if (!font_dev_is_open()) {151 1x
stop(152 1x
"get_space_width called when font dev state is not open. ",153 1x
"This shouldn't happen, please contact the maintainers."154
)155
}156 24x
font_dev_state$spacewidth157
}158 159
.open_fdev_is_monospace <- function() {160 32671x
if (!font_dev_is_open()) {161 2x
stop(162 2x
".open_fdev_is_monospace called when font dev state is not open. ",163 2x
"This shouldn't happen, please contact the maintainers."164
)165
}166 32669x
font_dev_state$ismonospace167
}168 169
## safe wrapper around .open_fdev_is_monospace170
is_monospace <- function(fontspec = font_spec(font_family, font_size, lineheight),171
font_family = "Courier",172
font_size = 8,173
lineheight = 1) {174 32670x
if (is.null(fontspec)) {175 1x
return(TRUE)176
}177 32669x
new_dev <- open_font_dev(fontspec)178 32669x
if (new_dev) {179 31x
on.exit(close_font_dev())180
}181 32669x
.open_fdev_is_monospace()182
}183 184
## get_max_wratio <- function() {185
## if (!font_dev_state$open) {186
## stop(187
## "get_space_width called when font dev state is not open. ",188
## "This shouldn't happen, please contact the maintainers."189
## )190
## }191
## if (.open_fdev_is_monospace()) {192
## 1193
## } else {194
## font_dev_state$maxratio195
## }196
## }197 198
gpar_from_fspec <- function(fontspec) {199 517x
gpar(200 517x
fontfamily = fontspec$family,201 517x
fontsize = fontspec$size,202 517x
lineheight = fontspec$lineheight203
)204
}205 206 114995x
font_dev_is_open <- function() font_dev_state$open207 208
#' Default horizontal separator209
#'210
#' The default horizontal separator character which can be displayed in the current211
#' charset for use in rendering table-like objects.212
#'213
#' @param hsep_char (`string`)\cr character that will be set in the R environment214
#' options as the default horizontal separator. Must be a single character. Use215
#' `getOption("formatters_default_hsep")` to get its current value (`NULL` if not set).216
#'217
#' @return unicode 2014 (long dash for generating solid horizontal line) if in a218
#' locale that uses a UTF character set, otherwise an ASCII hyphen with a219
#' once-per-session warning.220
#'221
#' @examples222
#' default_hsep()223
#' set_default_hsep("o")224
#' default_hsep()225
#'226
#' @name default_horizontal_sep227
#' @export228
default_hsep <- function() {229
system_default_hsep <- getOption("formatters_default_hsep")230 231
if (is.null(system_default_hsep)) {232
if (any(grepl("^UTF", utils::localeToCharset()))) {233
hsep <- "\u2014"234
} else {235
if (interactive()) {236
warning(237
"Detected non-UTF charset. Falling back to '-' ",238
"as default header/body separator. This warning ",239
"will only be shown once per R session."240
) # nocov241
} # nocov242
hsep <- "-" # nocov243
}244
} else {245
hsep <- system_default_hsep246
}247
hsep248
}249 250
#' @name default_horizontal_sep251
#' @export252
set_default_hsep <- function(hsep_char) {253
checkmate::assert_character(hsep_char, n.chars = 1, len = 1, null.ok = TRUE)254
options("formatters_default_hsep" = hsep_char)255
}256 257
.calc_cell_widths <- function(mat, colwidths, col_gap) {258 369x
spans <- mat$spans259 369x
keep_mat <- mat$display260 369x
body <- mat$strings261 262 369x
nr <- nrow(body)263 264 369x
cell_widths_mat <- matrix(rep(colwidths, nr), nrow = nr, byrow = TRUE)265 369x
nc <- ncol(cell_widths_mat)266 267 369x
for (i in seq_len(nrow(body))) {268 6545x
if (any(!keep_mat[i, ])) { # any spans?269 6x
j <- 1270 6x
while (j <= nc) {271 10x
nj <- spans[i, j]272 10x
j <- if (nj > 1) {273 6x
js <- seq(j, j + nj - 1)274 6x
cell_widths_mat[i, js] <- sum(cell_widths_mat[i, js]) + col_gap * (nj - 1)275 6x
j + nj276
} else {277 4x
j + 1278
}279
}280
}281
}282 369x
cell_widths_mat283
}284 285
# Main function that does the wrapping286
do_cell_fnotes_wrap <- function(mat, widths, max_width, tf_wrap, fontspec, expand_newlines = TRUE) {287 213x
col_gap <- mf_colgap(mat)288 213x
ncchar <- sum(widths) + (length(widths) - as.integer(mf_has_rlabels(mat))) * col_gap289 213x
inset <- table_inset(mat)290 291
## Text wrapping checks292 213x
if (tf_wrap) {293 92x
if (is.null(max_width)) {294 24x
max_width <- getOption("width", 80L)295 68x
} else if (is.character(max_width) && identical(max_width, "auto")) {296 !
max_width <- ncchar + inset297
}298 92x
assert_number(max_width, lower = 0)299
}300 301
## Check for having the right number of widths302 213x
stopifnot(length(widths) == ncol(mat$strings))303 304
## format the to ASCII305 213x
cell_widths_mat <- .calc_cell_widths(mat, widths, col_gap)306 307
# Check that indentation is correct (it works only for body)308 213x
.check_indentation(mat, row_col_width = cell_widths_mat[, 1, drop = TRUE])309 210x
mod_ind_list <- .modify_indentation(mat, cell_widths_mat, do_what = "remove")310 210x
mfs <- mod_ind_list[["mfs"]]311 210x
cell_widths_mat <- mod_ind_list[["cell_widths_mat"]]312 313
# Main wrapper314 210x
mf_strings(mat) <- matrix(315 210x
unlist(mapply(wrap_string,316 210x
str = mfs,317 210x
width = cell_widths_mat,318 210x
collapse = "\n",319 210x
MoreArgs = list(fontspec = fontspec)320
)),321 210x
ncol = ncol(mfs)322
)323 324 210x
if (expand_newlines) {325
## XXXXX this is wrong and will break for listings cause we don't know when326
## we need has_topleft to be FALSE!!!!!!!!!!327 210x
mat <- mform_handle_newlines(mat)328 329
## this updates extents in rinfo AND nlines in ref_fnotes_df330
## mat already has fontspec on it so no need to pass that down331 210x
mat <- update_mf_nlines(mat, max_width = max_width)332 333
# Re-indenting334 210x
mf_strings(mat) <- .modify_indentation(mat, cell_widths_mat, do_what = "add")[["mfs"]]335 210x
.check_indentation(mat) # all went well336
}337 210x
mat338
}339 340
# Helper function to see if body indentation matches (minimum)341
# It sees if there is AT LEAST the indentation contained in rinfo342
.check_indentation <- function(mat, row_col_width = NULL) {343
# mf_nrheader(mat) # not useful344 424x
mf_nlh <- mf_nlheader(mat)345 424x
mf_lgrp <- mf_lgrouping(mat)346 424x
mf_str <- mf_strings(mat)347
# we base everything on the groupings -> unique indentation identifiers348 424x
if (!is.null(mf_rinfo(mat))) { # this happens in rare cases with rtables::rtable()349 424x
mf_ind <- c(rep(0, mf_nrheader(mat)), mf_rinfo(mat)$indent) # XXX to fix with topleft350
} else {351 !
mf_ind <- rep(0, mf_nrheader(mat))352
}353 424x
ind_std <- paste0(rep(" ", mat$indent_size), collapse = "")354 355
# Expected indent (-x negative numbers should not appear at this stage)356 424x
stopifnot(all(mf_ind >= 0))357 424x
real_indent <- vapply(mf_ind, function(ii) {358 7833x
paste0(rep(ind_std, ii), collapse = "")359 424x
}, character(1))360 361 424x
if (!is.null(row_col_width) && any(row_col_width > 0) && !is.null(mf_rinfo(mat))) { # third is rare case362
# Self consistency test for row_col_width (same groups should have same width)363
# This should not be necessary (nocov)364 213x
consistency_check <- vapply(unique(mf_lgrp), function(ii) {365 3935x
width_per_grp <- row_col_width[which(mf_lgrp == ii)]366 3935x
all(width_per_grp == width_per_grp[1])367 213x
}, logical(1))368 213x
stopifnot(all(consistency_check))369 370
# Taking only one width for each indentation grouping371 213x
unique_row_col_width <- row_col_width[match(unique(mf_lgrp), mf_lgrp)]372 373
# Exception for check: case with summarize_row_groups and (hence) content_rows374 213x
nchar_real_indent <- nchar(real_indent)375 213x
body_rows <- seq(mf_nrheader(mat) + 1, length(nchar_real_indent))376 213x
nchar_real_indent[body_rows] <- nchar_real_indent[body_rows] +377 213x
as.numeric(mf_rinfo(mat)$node_class != "ContentRow")378
# xxx I think all of the above is a bit buggy honestly (check ContentRows!!!)379 380 213x
if (any(nchar_real_indent > unique_row_col_width)) {381 2x
stop(382 2x
"Inserted width for row label column is not wide enough. ",383 2x
"We found the following rows that do not have at least indentation * ind_size + 1",384 2x
" characters to allow text to be shown after indentation: ",385 2x
paste0(which(nchar(real_indent) + 1 > unique_row_col_width), collapse = " ")386
)387
}388
}389 390
# Main detector391 422x
correct_indentation <- vapply(seq_along(mf_lgrp), function(xx) {392 8277x
grouping <- mf_lgrp[xx]393 8277x
if (nzchar(real_indent[grouping])) {394 33x
has_correct_indentation <- stringi::stri_detect(395 33x
mf_str[xx, 1],396 33x
regex = paste0("^", real_indent[grouping])397
)398 33x
return(has_correct_indentation || !nzchar(mf_str[xx, 1])) # "" is still an ok indentation399
}400
# Cases where no indent are true by definition401 8244x
TRUE402 422x
}, logical(1))403 404 422x
if (any(!correct_indentation)) {405 1x
stop(406 1x
"We discovered indentation mismatches between the matrix_form and the indentation",407 1x
" predefined in mf_rinfo. This should not happen. Contact the maintainer."408 1x
) # nocov409
}410
}411 412
# Helper function that takes out or adds the proper indentation413
.modify_indentation <- function(mat, cell_widths_mat, do_what = c("remove", "add")) {414
# Extract info415 420x
mfs <- mf_strings(mat) # we work on mfs416 420x
mf_nlh <- mf_nlheader(mat)417 420x
mf_l <- mf_lgrouping(mat)418 420x
if (!is.null(mf_rinfo(mat))) { # this happens in rare cases with rtables::rtable()419 420x
mf_ind <- c(rep(0, mf_nrheader(mat)), mf_rinfo(mat)$indent) # XXX to fix with topleft420
} else {421 !
mf_ind <- rep(0, mf_nrheader(mat))422
}423 420x
stopifnot(length(mf_ind) == length(unique(mf_l))) # Check for indentation and grouping424 420x
ind_std <- paste0(rep(" ", mat$indent_size), collapse = "") # standard size of indent 1425 426
# Create real indentation427 420x
real_indent <- sapply(mf_ind, function(ii) paste0(rep(ind_std, ii), collapse = ""))428 429
# Use groupings to add or remove proper indentation430 420x
lbl_row <- mfs[, 1, drop = TRUE]431 420x
for (ii in seq_along(lbl_row)) {432 8268x
grp <- mf_l[ii]433 8268x
if (nzchar(real_indent[grp])) {434
# Update also the widths!!435 29x
if (do_what[1] == "remove") {436 9x
cell_widths_mat[ii, 1] <- cell_widths_mat[ii, 1] - nchar(real_indent[grp])437 9x
mfs[ii, 1] <- stringi::stri_replace(lbl_row[ii], "", regex = paste0("^", real_indent[grp]))438 20x
} else if (do_what[1] == "add") {439 20x
mfs[ii, 1] <- paste0(real_indent[grp], lbl_row[ii])440
} else {441
stop("do_what needs to be remove or add.") # nocov442
}443
} else {444 8239x
mfs[ii, 1] <- lbl_row[ii]445
}446
}447
# Final return448 420x
return(list("mfs" = mfs, "cell_widths_mat" = cell_widths_mat))449
}450 451
## take a character vector and return whether the value is452
## a string version of a number or not453
is_number_str <- function(vec) {454 !
is.na(as.numeric(vec))455
}456 457
is_dec_align <- function(vec) {458
# "c" is not an alignment method we define in `formatters`,459
# but the reverse dependency package `tables` will need460 604x
sdiff <- setdiff(vec, c(list_valid_aligns(), "c"))461 604x
if (length(sdiff) > 0) {462 !
stop("Invalid text-alignment(s): ", paste(sdiff, collapse = ", "))463
}464 604x
grepl("dec", vec)465
}466 467 459x
any_dec_align <- function(vec) any(is_dec_align(vec))468 469
#' Decimal alignment470
#'471
#' Aligning decimal values of string matrix. Allowed alignments are: `dec_left`, `dec_right`,472
#' and `decimal`.473
#'474
#' @param string_mat (`character matrix`)\cr "string" matrix component of `MatrixPrintForm` object.475
#' @param align_mat (`character matrix`)\cr "aligns" matrix component of `MatrixPrintForm` object.476
#' Should contain either `dec_left`, `dec_right`, or `decimal` for values to be decimal aligned.477
#'478
#' @details Left and right decimal alignment (`dec_left` and `dec_right`) differ from center decimal479
#' alignment (`decimal`) only when there is padding present. This may occur if column widths are480
#' set wider via parameters `widths` in `toString` or `colwidths` in `paginate_*`. More commonly,481
#' it also occurs when column names are wider. Cell wrapping is not supported when decimal482
#' alignment is used.483
#'484
#' @return A processed string matrix of class `MatrixPrintForm` with decimal-aligned values.485
#'486
#' @seealso [toString()], [MatrixPrintForm()]487
#'488
#' @examples489
#' dfmf <- basic_matrix_form(mtcars[1:5, ])490
#' aligns <- mf_aligns(dfmf)491
#' aligns[, -c(1)] <- "dec_left"492
#' decimal_align(mf_strings(dfmf), aligns)493
#'494
#' @export495
decimal_align <- function(string_mat, align_mat) {496
## Evaluate if any values are to be decimal aligned497 45x
if (!any_dec_align(align_mat)) {498 !
return(string_mat)499
}500 45x
for (i in seq(1, ncol(string_mat))) {501
## Take a column and its decimal alignments502 145x
col_i <- as.character(string_mat[, i])503 145x
align_col_i <- is_dec_align(align_mat[, i])504 505
## !( A || B) -> !A && !B DeMorgan's Law506
## Are there any values to be decimal aligned? safe if507 145x
if (any(align_col_i) && any(!grepl("^[0-9]\\.", col_i))) {508
## Extract values not to be aligned (NAs, non-numbers,509
## doesn't say "decimal" in alignment matrix)510
## XXX FIXME because this happens after formatting, we can't tell the difference between511
## non-number strings which come from na_str+ NA value and strings which just aren't numbers.512
## this is a problem that should eventually be fixed.513 82x
nas <- vapply(col_i, is.na, FUN.VALUE = logical(1))514 82x
nonnum <- !grepl("[0-9]", col_i)515
## No grepl("[a-zA-Z]", col_i) because this excludes N=xx, e.g.516 82x
nonalign <- nas | nonnum | !align_col_i517 82x
col_ia <- col_i[!nonalign]518 519
## Do decimal alignment520 82x
if (length(col_ia) > 0) {521
# Special case: scientific notation522 82x
has_sc_not <- grepl("\\d+[e|E][\\+|\\-]\\d+", col_ia)523 82x
if (any(has_sc_not)) {524 1x
stop(525 1x
"Found values using scientific notation between the ones that",526 1x
" needs to be decimal aligned (aligns is decimal, dec_left or dec_right).",527 1x
" Please consider using format functions to get a complete decimal ",528 1x
"(e.g. formatC)."529
)530
}531 532
## Count the number of numbers in the string533 81x
matches <- gregexpr("\\d+\\.\\d+|\\d+", col_ia)534 81x
more_than_one <- vapply(matches, function(x) {535 697x
sum(attr(x, "match.length") > 0) > 1536 81x
}, logical(1))537
## Throw error in case any have more than 1 numbers538 81x
if (any(more_than_one)) {539 2x
stop(540 2x
"Decimal alignment is not supported for multiple values. ",541 2x
"Found the following string with multiple numbers ",542 2x
"(first 3 selected from column ", col_i[1], "): '",543 2x
paste0(col_ia[more_than_one][seq(1, 3)], collapse = "', '"),544
"'"545
)546
}547
## General split (only one match -> the first)548 79x
main_regexp <- regexpr("\\d+", col_ia)549 79x
left <- regmatches(col_ia, main_regexp, invert = FALSE)550 79x
right <- regmatches(col_ia, main_regexp, invert = TRUE)551 79x
right <- sapply(right, "[[", 2)552 79x
something_left <- sapply(strsplit(col_ia, "\\d+"), "[[", 1)553 79x
left <- paste0(something_left, left)554 79x
if (!checkmate::test_set_equal(paste0(left, right), col_ia)) {555 !
stop(556 !
"Split string list lost some piece along the way. This ",557 !
"should not have happened. Please contact the maintainer."558
)559
} # nocov560 79x
separator <- sapply(right, function(x) {561 645x
if (nzchar(x)) {562 349x
substr(x, 1, 1)563
} else {564 296x
c(" ")565
}566 79x
}, USE.NAMES = FALSE)567 79x
right <- sapply(right, function(x) {568 645x
if (nchar(x) > 1) {569 317x
substr(x, 2, nchar(x))570
} else {571 328x
c("")572
}573 79x
}, USE.NAMES = FALSE)574
## figure out whether we need space separators (at least one had a "." or not)575 79x
if (!any(grepl("[^[:space:]]", separator))) {576 26x
separator <- gsub("[[:space:]]*", "", separator)577
}578
## modify the piece with spaces579 79x
left_mod <- paste0(spaces(max(nchar(left), na.rm = TRUE) - nchar(left)), left)580 79x
right_mod <- paste0(right, spaces(max(nchar(right), na.rm = TRUE) - nchar(right)))581
# Put everything together582 79x
aligned <- paste(left_mod, separator, right_mod, sep = "")583 79x
string_mat[!nonalign, i] <- aligned584
}585
}586
}587 42x
string_mat588
}589 590
## this gives the conversion from number of spaces to number of characters591
## for use in, e.g., repping out divider lines.592
calc_str_adj <- function(str, fontspec) {593 160x
if (nchar(str) == 0) {594 !
return(0)595
}596 160x
nchar(str) / nchar_ttype(str, fontspec, raw = TRUE)597
}598 599
# toString ---------------------------------------------------------------------600
# main printing code for MatrixPrintForm601 602
#' @description603
#' All objects that are printed to console pass via `toString`. This function allows604
#' fundamental formatting specifications to be applied to final output, like column widths605
#' and relative wrapping (`width`), title and footer wrapping (`tf_wrap = TRUE` and606
#' `max_width`), and horizontal separator character (e.g. `hsep = "+"`).607
#'608
#' @inheritParams MatrixPrintForm609
#' @inheritParams open_font_dev610
#' @inheritParams format_value611
#' @param widths (`numeric` or `NULL`)\cr Proposed widths for the columns of `x`. The expected612
#' length of this numeric vector can be retrieved with `ncol(x) + 1` as the column of row names613
#' must also be considered.614
#' @param hsep (`string`)\cr character to repeat to create header/body separator line. If615
#' `NULL`, the object value will be used. If `" "`, an empty separator will be printed. See616
#' [default_hsep()] for more information.617
#' @param tf_wrap (`flag`)\cr whether the text for title, subtitles, and footnotes should be wrapped.618
#' @param max_width (`integer(1)`, `string` or `NULL`)\cr width that title and footer (including619
#' footnotes) materials should be word-wrapped to. If `NULL`, it is set to the current print width of the620
#' session (`getOption("width")`). If set to `"auto"`, the width of the table (plus any table inset) is621
#' used. Parameter is ignored if `tf_wrap = FALSE`.622
#' @param ttype_ok (`logical(1)`)\cr should truetype (non-monospace) fonts be623
#' allowed via `fontspec`. Defaults to `FALSE`. This parameter is primarily624
#' for internal testing and generally should not be set by end users.625
#'626
#' @details627
#' Manual insertion of newlines is not supported when `tf_wrap = TRUE` and will result in a warning and628
#' undefined wrapping behavior. Passing vectors of already split strings remains supported, however in this629
#' case each string is word-wrapped separately with the behavior described above.630
#'631
#' @return A character string containing the ASCII rendering of the table-like object represented by `x`.632
#'633
#' @seealso [wrap_string()]634
#'635
#' @examples636
#' mform <- basic_matrix_form(mtcars)637
#' cat(toString(mform))638
#'639
#' @rdname tostring640
#' @exportMethod toString641
setMethod("toString", "MatrixPrintForm", function(x,642
widths = NULL,643
tf_wrap = FALSE,644
max_width = NULL,645
col_gap = mf_colgap(x),646
hsep = NULL,647
fontspec = font_spec(),648
ttype_ok = FALSE,649
round_type = c("iec", "sas")) {650 163x
checkmate::assert_flag(tf_wrap)651 652
## we are going to use the pdf device and grid to understand the actual653
## print width of things given our font family and font size654 163x
new_dev <- open_font_dev(fontspec)655 163x
if (new_dev) {656 153x
on.exit(close_font_dev())657
}658 659 163x
if (!is_monospace(fontspec = fontspec) && !ttype_ok) {660 !
stop(661 !
"non-monospace font specified in toString call; this would result in cells contents not lining up exactly. ",662 !
"If you truly want this behavior please set ttype_ok = TRUE in the call to toString/export_as_txt/export_as_pdf"663
)664
}665 163x
mat <- matrix_form(x, indent_rownames = TRUE, fontspec = fontspec, round_type = round_type)666 667
# Check for \n in mat strings -> if there are any, matrix_form did not work668 163x
if (any(grepl("\n", mf_strings(mat)))) {669 !
stop(670 !
"Found newline characters (\\n) in string matrix produced by matrix_form. ",671 !
"This is not supported and implies missbehavior on the first parsing (in matrix_form). ",672 !
"Please contact the maintainer or file an issue."673 !
) # nocov674
}675 163x
if (any(grepl("\r", mf_strings(mat)))) {676 !
stop(677 !
"Found recursive special characters (\\r) in string matrix produced by matrix_form. ",678 !
"This special character is not supported and should be removed."679 !
) # nocov680
}681 682
# Check that expansion worked for header -> should not happen683 163x
if (!is.null(mf_rinfo(mat)) && # rare case of rtables::rtable()684 163x
(length(mf_lgrouping(mat)) != nrow(mf_strings(mat)) || # non-unique grouping test # nolint685 163x
mf_nrheader(mat) + nrow(mf_rinfo(mat)) != length(unique(mf_lgrouping(mat))))) { # nolint686 !
stop(687 !
"The sum of the expected nrows header and nrows of content table does ",688 !
"not match the number of rows in the string matrix. To our knowledge, ",689 !
"this is usually of a problem in solving newline characters (\\n) in the header. ",690 !
"Please contact the maintaner or file an issue."691 !
) # nocov692
}693 694 163x
inset <- table_inset(mat)695 696
# if cells are decimal aligned, run propose column widths697
# if the provided widths is less than proposed width, return an error698 163x
if (any_dec_align(mf_aligns(mat))) {699 22x
aligned <- propose_column_widths(x, fontspec = fontspec, round_type = round_type)700 701
# catch any columns that require widths more than what is provided702 20x
if (!is.null(widths)) {703 9x
how_wide <- sapply(seq_along(widths), function(i) c(widths[i] - aligned[i]))704 9x
too_wide <- how_wide < 0705 9x
if (any(too_wide)) {706 2x
desc_width <- paste(paste(707 2x
names(which(too_wide)),708 2x
paste0("(", how_wide[too_wide], ")")709 2x
), collapse = ", ")710 2x
stop(711 2x
"Inserted width(s) for column(s) ", desc_width,712 2x
" is(are) not wide enough for the desired alignment."713
)714
}715
}716
}717 718
# Column widths are fixed here719 159x
if (is.null(widths)) {720
# if mf does not have widths -> propose them721 133x
widths <- mf_col_widths(x) %||% propose_column_widths(x, fontspec = fontspec, round_type = round_type)722
} else {723 26x
mf_col_widths(x) <- widths724
}725 726
## Total number of characters for the table727
## col_gap (and table inset) are in number of spaces728
## so we're ok here even in the truetype case729 159x
ncchar <- sum(widths) + (length(widths) - 1) * col_gap730 731
## max_width for wrapping titles and footers (not related to ncchar if not indirectly)732 159x
max_width <- .handle_max_width(733 159x
tf_wrap = tf_wrap,734 159x
max_width = max_width,735 159x
colwidths = widths,736 159x
col_gap = col_gap,737 159x
inset = inset738
)739 740
# Main wrapper function for table core741 159x
mat <- do_cell_fnotes_wrap(mat, widths, max_width = max_width, tf_wrap = tf_wrap, fontspec = fontspec)742 743 156x
body <- mf_strings(mat)744 156x
aligns <- mf_aligns(mat)745 156x
keep_mat <- mf_display(mat)746
## spans <- mat$spans747 156x
mf_ri <- mf_rinfo(mat)748 156x
ref_fnotes <- mf_rfnotes(mat)749 156x
nl_header <- mf_nlheader(mat)750 751 156x
cell_widths_mat <- .calc_cell_widths(mat, widths, col_gap)752 753
# decimal alignment754 156x
if (any_dec_align(aligns)) {755 18x
body <- decimal_align(body, aligns)756
}757 758
# Content is a matrix of cells with the right amount of spaces759 156x
content <- matrix(760 156x
mapply(padstr, body, cell_widths_mat, aligns, MoreArgs = list(fontspec = fontspec)),761 156x
ncol = ncol(body)762
)763 156x
content[!keep_mat] <- NA764 765
# Define gap string and divisor string766 156x
gap_str <- strrep(" ", col_gap)767 156x
if (is.null(hsep)) {768 121x
hsep <- horizontal_sep(mat)769
}770 156x
adj_hsep <- calc_str_adj(hsep, fontspec)771 156x
div <- substr(strrep(hsep, ceiling(ncchar * adj_hsep)), 1, ceiling(ncchar * adj_hsep))772 156x
hsd <- header_section_div(mat)773 156x
if (!is.na(hsd)) {774 !
adj_hsd <- calc_str_adj(hsd, fontspec)775 !
hsd <- substr(strrep(hsd, ceiling(ncchar * adj_hsd)), 1, ceiling(ncchar * adj_hsd))776
} else {777 156x
hsd <- NULL # no divisor778
}779 780
# text head (paste w/o NA content header and gap string)781 156x
txt_head <- apply(head(content, nl_header), 1, .paste_no_na, collapse = gap_str)782 783
# txt body784 156x
sec_seps_df <- mf_ri[, c("abs_rownumber", "trailing_sep"), drop = FALSE]785 156x
if (!is.null(sec_seps_df) && any(!is.na(sec_seps_df$trailing_sep))) {786 2x
bdy_cont <- tail(content, -nl_header)787
## unfortunately we count "header rows" wrt line grouping so it788
## doesn't match the real (i.e. body) rows as is789 2x
row_grouping <- tail(mf_lgrouping(mat), -nl_header) - mf_nrheader(mat)790 2x
nrbody <- NROW(bdy_cont)791 2x
stopifnot(length(row_grouping) == nrbody)792
## all rows with non-NA section divs and the final row (regardless of NA status)793
## fixes #77794 2x
sec_seps_df <- sec_seps_df[unique(c(795 2x
which(!is.na(sec_seps_df$trailing_sep)),796 2x
NROW(sec_seps_df)797
)), ]798 2x
txt_body <- character()799 2x
sec_strt <- 1800 2x
section_rws <- sec_seps_df$abs_rownumber801 2x
for (i in seq_len(NROW(section_rws))) {802 6x
cur_rownum <- section_rws[i]803 6x
sec_end <- max(which(row_grouping == cur_rownum))804 6x
txt_body <- c(805 6x
txt_body,806 6x
apply(bdy_cont[seq(sec_strt, sec_end), , drop = FALSE],807 6x
1,808 6x
.paste_no_na,809 6x
collapse = gap_str810
),811
## don't print section dividers if they would be the last thing before the812
## footer divider813
## this also ensures an extraneous sec div won't be printed if we have non-sec-div814
## rows after the last sec div row (#77)815 6x
if (sec_end < nrbody) {816 4x
adj_i <- calc_str_adj(sec_seps_df$trailing_sep[i], fontspec)817 4x
substr(818 4x
strrep(sec_seps_df$trailing_sep[i], ceiling(ncchar * adj_i)), 1,819 4x
ceiling((ncchar - inset) * adj_i)820
)821
}822
)823 6x
sec_strt <- sec_end + 1824
}825
} else {826
# This is the usual default pasting827 154x
txt_body <- apply(tail(content, -nl_header), 1, .paste_no_na, collapse = gap_str)828
}829 830
# retrieving titles and footers831 156x
allts <- all_titles(mat)832 833 156x
ref_fnotes <- reorder_ref_fnotes(ref_fnotes)834
# Fix for ref_fnotes with \n characters XXX this does not count in the pagination835 156x
if (any(grepl("\\n", ref_fnotes))) {836 2x
ref_fnotes <- unlist(strsplit(ref_fnotes, "\n", fixed = TRUE))837
}838 839 156x
allfoots <- list(840 156x
"main_footer" = main_footer(mat),841 156x
"prov_footer" = prov_footer(mat),842 156x
"ref_footnotes" = ref_fnotes843
)844 156x
allfoots <- allfoots[!sapply(allfoots, is.null)]845 846
## Wrapping titles if they go beyond the horizontally allowed space847 156x
if (tf_wrap) {848 68x
new_line_warning(allts)849 68x
allts <- wrap_txt(allts, max_width, fontspec = fontspec)850
}851 156x
titles_txt <- if (any(nzchar(allts))) c(allts, "", .do_inset(div, inset)) else NULL852 853
# Wrapping footers if they go beyond the horizontally allowed space854 156x
if (tf_wrap) {855 68x
new_line_warning(allfoots)856 68x
allfoots$main_footer <- wrap_txt(allfoots$main_footer, max_width - inset, fontspec = fontspec)857 68x
allfoots$ref_footnotes <- wrap_txt(allfoots$ref_footnotes, max_width - inset, fontspec = fontspec)858
## no - inset here because the prov_footer is not inset859 68x
allfoots$prov_footer <- wrap_txt(allfoots$prov_footer, max_width, fontspec = fontspec)860
}861 862
# Final return863 156x
paste0(864 156x
paste(c(865 156x
titles_txt, # .do_inset(div, inset) happens if there are any titles866 156x
.do_inset(txt_head, inset),867 156x
.do_inset(div, inset),868 156x
.do_inset(hsd, inset), # header_section_div if present869 156x
.do_inset(txt_body, inset),870 156x
.footer_inset_helper(allfoots, div, inset)871 156x
), collapse = "\n"),872 156x
"\n"873
)874
})875 876
# Switcher for the 3 options for max_width (NULL, numeric, "auto"))877
.handle_max_width <- function(tf_wrap, max_width,878
cpp = NULL, # Defaults to getOption("width", 80L)879
# Things for auto880
inset = NULL, colwidths = NULL, col_gap = NULL) {881 237x
max_width <- if (!tf_wrap) {882 117x
if (!is.null(max_width)) {883 1x
warning("tf_wrap is FALSE - ignoring non-null max_width value.")884
}885 117x
NULL886 237x
} else if (tf_wrap) {887 120x
if (is.null(max_width)) {888 36x
if (is.null(cpp) || is.na(cpp)) {889 7x
getOption("width", 80L)890
} else {891 29x
cpp892
}893 84x
} else if (is.numeric(max_width)) {894 79x
max_width895 5x
} else if (is.character(max_width) && identical(max_width, "auto")) {896
# This should not happen, but just in case897 4x
if (any(sapply(list(inset, colwidths, col_gap), is.null))) {898 1x
stop("inset, colwidths, and col_gap must all be non-null when max_width is \"auto\".")899
}900 3x
inset + sum(colwidths) + (length(colwidths) - 1) * col_gap901
} else {902 1x
stop("max_width must be NULL, a numeric value, or \"auto\".")903
}904
}905 235x
return(max_width)906
}907 908
.do_inset <- function(x, inset) {909 1053x
if (inset == 0 || !any(nzchar(x))) {910 1034x
return(x)911
}912 19x
padding <- strrep(" ", inset)913 19x
if (is.character(x)) {914 19x
x <- paste0(padding, x)915 !
} else if (is(x, "matrix")) {916 !
x[, 1] <- .do_inset(x[, 1, drop = TRUE], inset)917
}918 19x
x919
}920 921
.inset_div <- function(txt, div, inset) {922 105x
c(.do_inset(div, inset), "", txt)923
}924 925
.footer_inset_helper <- function(footers_v, div, inset) {926 156x
div_done <- FALSE # nolint927 156x
fter <- footers_v$main_footer928 156x
prvf <- footers_v$prov_footer929 156x
rfn <- footers_v$ref_footnotes930 156x
footer_txt <- .do_inset(rfn, inset)931 156x
if (any(nzchar(footer_txt))) {932 14x
footer_txt <- .inset_div(footer_txt, div, inset)933
}934 156x
if (any(vapply(935 156x
footers_v, function(x) any(nzchar(x)),936 156x
TRUE937
))) {938 91x
if (any(nzchar(prvf))) {939 89x
provtxt <- c(940 89x
if (any(nzchar(fter))) "",941 89x
prvf942
)943
} else {944 2x
provtxt <- character()945
}946 91x
footer_txt <- c(947 91x
footer_txt,948 91x
.inset_div(949 91x
c(950 91x
.do_inset(fter, inset),951 91x
provtxt952
),953 91x
div,954 91x
inset955
)956
)957
}958 156x
footer_txt959
}960 961
reorder_ref_fnotes <- function(fns) {962 159x
ind <- gsub("\\{(.*)\\}.*", "\\1", fns)963 159x
ind_num <- suppressWarnings(as.numeric(ind))964 159x
is_num <- !is.na(ind_num)965 159x
is_asis <- ind == fns966 967 159x
if (all(is_num)) {968 143x
ord_num <- order(ind_num)969 143x
ord_char <- NULL970 143x
ord_other <- NULL971
} else {972 16x
ord_num <- order(ind_num[is_num])973 16x
ord_char <- order(ind[!is_num & !is_asis])974 16x
ord_other <- order(ind[is_asis])975
}976 159x
c(fns[is_num][ord_num], fns[!is_num & !is_asis][ord_char], ind[is_asis][ord_other])977
}978 979
new_line_warning <- function(str_v) {980 136x
if (any(unlist(sapply(str_v, grepl, pattern = "\n")))) {981 !
msg <- c(982 !
"Detected manual newlines when automatic title/footer word-wrapping is on.",983 !
"This is unsupported and will result in undefined behavior. Please either ",984 !
"utilize automatic word-wrapping with newline characters inserted, or ",985 !
"turn off automatic wrapping and wordwrap all contents manually by inserting ",986 !
"newlines."987
)988 !
warning(paste0(msg, collapse = ""))989
}990
}991 992
#' Wrap a string to a precise width993
#'994
#' Core wrapping functionality that preserves whitespace. Newline character `"\n"` is not supported995
#' by core functionality [stringi::stri_wrap()]. This is usually solved beforehand by [matrix_form()].996
#' If the width is smaller than any large word, these will be truncated after `width` characters. If997
#' the split leaves trailing groups of empty spaces, they will be dropped.998
#'999
#' @inheritParams open_font_dev1000
#' @param str (`string`, `character`, or `list`)\cr string to be wrapped. If it is a `vector` or1001
#' a `list`, it will be looped as a `list` and returned with `unlist(use.names = FALSE)`.1002
#' @param width (`numeric(1)`)\cr width, in characters, that the text should be wrapped to.1003
#' @param collapse (`string` or `NULL`)\cr collapse character used to separate segments of words that1004
#' have been split and should be pasted together. This is usually done internally with `"\n"` to update1005
#' the wrapping along with other internal values.1006
#'1007
#' @details Word wrapping happens similarly to [stringi::stri_wrap()] with the following difference: individual1008
#' words which are longer than `max_width` are broken up in a way that fits with other word wrapping.1009
#'1010
#' @return A string if `str` is one element and if `collapse = NULL`. Otherwise, a list of elements1011
#' (if `length(str) > 1`) that can contain strings or vectors of characters (if `collapse = NULL`).1012
#'1013
#' @examples1014
#' str <- list(1015
#' " , something really \\tnot very good", # \t needs to be escaped1016
#' " but I keep it12 "1017
#' )1018
#' wrap_string(str, 5, collapse = "\n")1019
#'1020
#' @export1021
wrap_string <- function(str, width, collapse = NULL, fontspec = font_spec()) {1022 36488x
if (length(str) > 1) {1023 114x
return(1024 114x
unlist(1025 114x
lapply(str, wrap_string, width = width, collapse = collapse, fontspec = fontspec),1026 114x
use.names = FALSE1027
)1028
)1029
}1030 36374x
str <- unlist(str, use.names = FALSE) # it happens is one list element1031 36374x
if (!length(str) || !nzchar(str) || is.na(str)) {1032 3872x
return(str)1033
}1034 32502x
checkmate::assert_character(str)1035 32502x
checkmate::assert_int(width, lower = 1)1036 1037 32502x
if (any(grepl("\\n", str))) {1038 !
stop(1039 !
"Found \\n in a string that was meant to be wrapped. This should not happen ",1040 !
"because matrix_form should take care of them before this step (toString, ",1041 !
"i.e. the printing machinery). Please contact the maintaner or file an issue."1042
)1043
}1044 1045 32502x
if (!is_monospace(fontspec)) {1046 3x
return(wrap_string_ttype(str, width, fontspec, collapse = collapse))1047
}1048 1049
# str can be also a vector or list. In this case simplify manages the output1050 32499x
ret <- .go_stri_wrap(str, width)1051 1052
# Check if it went fine1053 32499x
if (any(nchar_ttype(ret, fontspec) > width)) {1054 68x
which_exceeded <- which(nchar_ttype(ret, fontspec) > width)1055 1056
# Recursive for loop to take word interval1057 68x
while (length(which_exceeded) > 0) {1058 75x
we_i <- which_exceeded[1]1059
# Is there space for some part of the next word?1060 75x
char_threshold <- width * (2 / 3) + 0.01 # if too little space -> no previous word1061 75x
smart_condition <- nchar_ttype(ret[we_i - 1], fontspec) + 1 < char_threshold # +1 is for spaces1062 75x
if (we_i - 1 > 0 && smart_condition) {1063 6x
we_interval <- unique(c(we_i - 1, we_i))1064 6x
we_interval <- we_interval[1065 6x
(we_interval < (length(ret) + 1)) &1066 6x
(we_interval > 0)1067
]1068
} else {1069 69x
we_interval <- we_i1070
}1071
# Split words and collapse (needs unique afterwards)1072 75x
ret[we_interval] <- split_words_by(1073 75x
paste0(ret[we_interval], collapse = " "),1074 75x
width1075
)1076
# Taking out repetitions if there are more than one1077 75x
if (length(we_interval) > 1) {1078 6x
ret <- ret[-we_interval[-1]]1079 6x
we_interval <- we_interval[1]1080
}1081
# Paste together and rerun if it is not the same as original ret1082 75x
ret_collapse <- paste0(ret, collapse = " ")1083 1084
# Checking if we are stuck in a loop1085 75x
ori_wrapped_txt_v <- .go_stri_wrap(str, width)1086 75x
cur_wrapped_txt_v <- .go_stri_wrap(ret_collapse, width)1087 75x
broken_char_ori <- sum(nchar_ttype(ori_wrapped_txt_v, fontspec) > width) # how many issues there were1088 75x
broken_char_cur <- sum(nchar_ttype(cur_wrapped_txt_v, fontspec) > width) # how many issues there are1089 1090
# if still broken, we did not solve the current issue!1091 75x
if (setequal(ori_wrapped_txt_v, cur_wrapped_txt_v) || broken_char_cur >= broken_char_ori) {1092
# help function: Very rare case where the recursion is stuck in a loop1093 14x
ret_tmp <- force_split_words_by(ret[we_interval], width) # here we_interval is only one ind1094 14x
ret <- append(ret, ret_tmp, we_interval)[-we_interval]1095 14x
which_exceeded <- which(nchar_ttype(ret, fontspec) > width)1096
} else {1097 61x
return(wrap_string(str = ret_collapse, width = width, collapse = collapse))1098
}1099
}1100
}1101 1102 32438x
if (!is.null(collapse)) {1103 31987x
return(paste0(ret, collapse = collapse))1104
}1105 1106 451x
return(ret)1107
}1108 1109
.go_stri_wrap <- function(str, w) {1110 32649x
if (w < 1) {1111 !
return(str)1112
}1113 32649x
stringi::stri_wrap(str,1114 32649x
width = w,1115 32649x
normalize = FALSE, # keeps spaces1116 32649x
simplify = TRUE, # If FALSE makes it a list with str elements1117 32649x
indent = 0,1118 32649x
use_length = FALSE # incase the defaul changes, use actual char widths1119
)1120
}1121 1122
#' @rdname wrap_string_ttype1123
#' @export1124
split_word_ttype <- function(str, width, fontspec, min_ok_chars) {1125 11x
chrs <- strsplit(str, "")[[1]]1126 11x
nctt_chars <- nchar_ttype(chrs, fontspec, raw = TRUE)1127 11x
ok <- which(cumsum(nctt_chars) <= width)1128 11x
if (length(ok) < min_ok_chars || length(chrs) - length(ok) < min_ok_chars) {1129 3x
list(1130 3x
ok = character(),1131 3x
remainder = str1132
)1133
} else {1134 8x
list(1135 8x
ok = substr(str, 1, length(ok)),1136 8x
remainder = substr(str, length(ok) + 1, nchar(str))1137
)1138
}1139
}1140 1141
## need a separate path here because **the number of characters**1142
## in each part is no longer going to be constant the way it1143
## was for monospace1144
## this is much slower but still shouldn't be a bottleneck, if it is we'll1145
## have to do something else1146
#' wrap string given a Truetype font1147
#'1148
#' @inheritParams wrap_string1149
#' @param min_ok_chars (`numeric(1)`)\cr number of minimum characters that remain1150
#' on either side when a word is split.1151
#' @param wordbreak_ok (`logical(1)`)\cr should breaking within a word be allowed? If, `FALSE`,1152
#' attempts to wrap a string to a width narrower than its widest word will result1153
#' in an error.1154
#' @return `str`, broken up into a word-wrapped vector1155
#' @export1156
wrap_string_ttype <- function(str,1157
width,1158
fontspec,1159
collapse = NULL,1160
min_ok_chars = min(floor(nchar(str) / 2), 4, floor(width / 2)),1161
wordbreak_ok = TRUE) {1162 12x
newdev <- open_font_dev(fontspec)1163 11x
if (newdev) {1164 !
on.exit(close_font_dev())1165
}1166 1167 11x
rawspls <- strsplit(str, "[[:space:]](?=[^[:space:]])", perl = TRUE)[[1]] # preserve all but one space1168 11x
nctt <- nchar_ttype(rawspls, fontspec, raw = TRUE)1169 11x
pts <- which(cumsum(nctt) <= width)1170 11x
if (length(pts) == length(rawspls)) { ## no splitting needed1171 3x
return(str)1172 8x
} else if (length(pts) == 0) { ## no spaces, all one word, split it and keep going1173 7x
if (wordbreak_ok) {1174 7x
inner_res <- list()1175 7x
min_ok_inner <- min_ok_chars1176 7x
while (length(inner_res$ok) == 0) {1177 10x
inner_res <- split_word_ttype(rawspls[1], width, fontspec, min_ok_inner) # min_ok_chars)1178 10x
min_ok_inner <- floor(min_ok_inner / 2)1179
}1180 7x
done <- inner_res$ok1181 7x
remainder <- paste(c(inner_res$remainder, rawspls[-1]), collapse = " ")1182
} else {1183 !
stop(1184 !
"Unable to find word wrapping solution without breaking word: ",1185 !
rawspls[[1]], " [requires ", nchar_ttype(rawspls[[1]], fontspec), " spaces of width, out of ",1186 !
width, " available]."1187
)1188
}1189
} else { ## some words fit, and some words don't1190 1x
done_tmp <- paste(rawspls[pts], collapse = " ")1191 1x
tospl_tmp <- rawspls[length(pts) + 1]1192 1x
width_tmp <- width - sum(nctt[pts])1193 1x
if (wordbreak_ok && width_tmp / width > .33) {1194 1x
inner_res <- split_word_ttype(tospl_tmp, width_tmp, fontspec,1195 1x
min_ok_chars = min_ok_chars1196
)1197
} else {1198 !
inner_res <- list(done = "", remainder = tospl_tmp)1199
}1200 1x
done <- paste(c(rawspls[pts], inner_res$ok),1201 1x
collapse = " "1202
)1203 1x
remainder <- paste(1204 1x
c(1205 1x
inner_res$remainder,1206 1x
if (length(rawspls) > length(pts) + 1) tail(rawspls, -(length(pts) + 1))1207
),1208 1x
collapse = " "1209
)1210
}1211 8x
ret <- c(1212 8x
done,1213 8x
wrap_string_ttype(remainder, width, fontspec)1214
)1215 8x
if (!is.null(collapse)) {1216 !
ret <- paste(ret, collapse = collapse)1217
}1218 8x
ret1219
}1220 1221
# help function: Very rare case where the recursion is stuck in a loop1222
force_split_words_by <- function(ret, width) {1223 14x
which_exceeded <- which(nchar(ret) > width)1224 14x
ret_tmp <- NULL1225 14x
for (ii in seq_along(ret)) {1226 14x
if (ii %in% which_exceeded) {1227 14x
wrd_i <- ret[ii]1228 14x
init_v <- seq(1, nchar(wrd_i), by = width)1229 14x
end_v <- c(init_v[-1] - 1, nchar(wrd_i))1230 14x
str_v_tmp <- stringi::stri_sub(wrd_i, from = init_v, to = end_v)1231 14x
ret_tmp <- c(ret_tmp, str_v_tmp[!grepl("^\\s+$", str_v_tmp) & nzchar(str_v_tmp)])1232
} else {1233 !
ret_tmp <- c(ret_tmp, ret[ii])1234
}1235
}1236 14x
ret_tmp1237
}1238 1239
# Helper fnc to split the words and collapse them with space1240
split_words_by <- function(wrd, width) {1241 75x
vapply(wrd, function(wrd_i) {1242 75x
init_v <- seq(1, nchar(wrd_i), by = width)1243 75x
end_v <- c(init_v[-1] - 1, nchar(wrd_i))1244 75x
fin_str_v <- substring(wrd_i, init_v, end_v)1245 75x
is_only_spaces <- grepl("^\\s+$", fin_str_v)1246
# We pop only spaces at this point1247 75x
if (all(is_only_spaces)) {1248 !
fin_str_v <- fin_str_v[1] # keep only one width-sized empty1249
} else {1250 75x
fin_str_v <- fin_str_v[!is_only_spaces] # hybrid text + \s1251
}1252 1253
# Collapse the string1254 75x
paste0(fin_str_v, collapse = " ")1255 75x
}, character(1), USE.NAMES = FALSE)1256
}1257 1258
#' @describeIn wrap_string Deprecated function. Please use [wrap_string()] instead.1259
#'1260
#' @examples1261
#' wrap_txt(str, 5, collapse = NULL)1262
#'1263
#' @export1264
wrap_txt <- function(str, width, collapse = NULL, fontspec = font_spec()) {1265 396x
new_dev <- open_font_dev(fontspec)1266 396x
if (new_dev) {1267 2x
on.exit(close_font_dev())1268
}1269 1270 396x
unlist(wrap_string(str, width, collapse, fontspec = fontspec), use.names = FALSE)1271
}1272 1273
pad_vert_top <- function(x, len, default = "") {1274 5546x
c(x, rep(default, len - length(x)))1275
}1276 1277
pad_vert_bottom <- function(x, len, default = "") {1278 362x
c(rep(default, len - length(x)), x)1279
}1280 1281
pad_vec_to_len <- function(vec, len, cpadder = pad_vert_top, rlpadder = cpadder) {1282 741x
dat <- unlist(lapply(vec[-1], cpadder, len = len))1283 741x
dat <- c(rlpadder(vec[[1]], len = len), dat)1284 741x
matrix(dat, nrow = len)1285
}1286 1287
rep_vec_to_len <- function(vec, len, ...) {1288 698x
matrix(unlist(lapply(vec, rep, times = len)),1289 698x
nrow = len1290
)1291
}1292 1293
safe_strsplit <- function(x, split, ...) {1294 857x
ret <- strsplit(x, split, ...)1295 857x
lapply(ret, function(reti) if (length(reti) == 0) "" else reti)1296
}1297 1298
.expand_mat_rows_inner <- function(i, mat, row_nlines, expfun, ...) {1299 1439x
leni <- row_nlines[i]1300 1439x
rw <- mat[i, ]1301 1439x
if (is.character(rw)) {1302 857x
rw <- safe_strsplit(rw, "\n", fixed = TRUE)1303
}1304 1439x
expfun(rw, len = leni, ...)1305
}1306 1307
expand_mat_rows <- function(mat, row_nlines = apply(mat, 1, nlines), expfun = pad_vec_to_len, ...) {1308 280x
rinds <- seq_len(nrow(mat))1309 280x
exprows <- lapply(rinds, .expand_mat_rows_inner,1310 280x
mat = mat,1311 280x
row_nlines = row_nlines,1312 280x
expfun = expfun,1313
...1314
)1315 280x
do.call(rbind, exprows)1316
}1317 1318
#' Transform a vector of spans (with duplication) into a visibility vector1319
#'1320
#' @param spans (`numeric`)\cr a vector of spans, with each span value repeated1321
#' for the cells it covers.1322
#'1323
#' @details1324
#' The values of `spans` are assumed to be repeated such that each individual position covered by the1325
#' span has the repeated value.1326
#'1327
#' This means that each block of values in `spans` must be of a length at least equal to its value1328
#' (i.e. two 2s, three 3s, etc).1329
#'1330
#' This function correctly handles cases where two spans of the same size are next to each other;1331
#' i.e., a block of four 2s represents two large cells each of which spans two individual cells.1332
#'1333
#' @return A logical vector the same length as `spans` indicating whether the contents of a string vector1334
#' with those spans is valid.1335
#'1336
#' @note1337
#' Currently no checking or enforcement is done to verify that the vector of spans is valid according to1338
#' the specifications described in the Details section above.1339
#'1340
#' @examples1341
#' spans_to_viscell(c(2, 2, 2, 2, 1, 3, 3, 3))1342
#'1343
#' @export1344
spans_to_viscell <- function(spans) {1345 2x
if (!is.vector(spans)) {1346 !
spans <- as.vector(spans)1347
}1348 2x
myrle <- rle(spans)1349 2x
unlist(1350 2x
mapply(1351 2x
function(vl, ln) {1352 4x
rep(c(TRUE, rep(FALSE, vl - 1L)), times = ln / vl)1353
},1354 2x
SIMPLIFY = FALSE,1355 2x
vl = myrle$values,1356 2x
ln = myrle$lengths1357
),1358 2x
recursive = FALSE1359
)1360
}1361 1362
#' Propose column widths based on the `MatrixPrintForm` of an object1363
#'1364
#' Row names are also considered a column for the output.1365
#'1366
#' @inheritParams open_font_dev1367
#' @inheritParams format_value1368
#' @param x (`ANY`)\cr a `MatrixPrintForm` object, or an object with a `matrix_form` method.1369
#' @param indent_size (`numeric(1)`)\cr indent size, in characters. Ignored when `x` is already1370
#' a `MatrixPrintForm` object in favor of information there.1371
#'1372
#' @return A vector of column widths based on the content of `x` for use in printing and pagination.1373
#'1374
#' @examples1375
#' mf <- basic_matrix_form(mtcars)1376
#' propose_column_widths(mf)1377
#'1378
#' @export1379
propose_column_widths <- function(x,1380
indent_size = 2,1381
fontspec = font_spec(),1382
round_type = c("iec", "sas")) {1383 95x
new_dev <- open_font_dev(fontspec)1384 95x
if (new_dev) {1385 65x
on.exit(close_font_dev())1386
}1387 1388 95x
if (!is(x, "MatrixPrintForm")) {1389 !
x <- matrix_form(x, indent_rownames = TRUE, indent_size = indent_size, fontspec = fontspec, round_type = round_type)1390
}1391 95x
body <- mf_strings(x)1392 95x
spans <- mf_spans(x)1393 95x
aligns <- mf_aligns(x)1394 95x
display <- mf_display(x)1395 1396
# compute decimal alignment if asked in alignment matrix1397 95x
if (any_dec_align(aligns)) {1398 27x
body <- decimal_align(body, aligns)1399
}1400 1401
## chars <- nchar(body) #old monospace assumption1402
## we now use widths in terms of the printwidth of the space (" ")1403
## character. This collapses to the same thing in the monospace1404
## case but allows us to reasonably support truetype fonts1405 92x
chars <- nchar_ttype(body, fontspec)1406 1407
# first check column widths without colspan1408 92x
has_spans <- spans != 11409 92x
chars_ns <- chars1410 92x
chars_ns[has_spans] <- 01411 92x
widths <- apply(chars_ns, 2, max)1412 1413
# now check if the colspans require extra width1414 92x
if (any(has_spans)) {1415 1x
has_row_spans <- apply(has_spans, 1, any)1416 1417 1x
chars_sp <- chars[has_row_spans, , drop = FALSE]1418 1x
spans_sp <- spans[has_row_spans, , drop = FALSE]1419 1x
disp_sp <- display[has_row_spans, , drop = FALSE]1420 1421 1x
nc <- ncol(spans)1422 1x
for (i in seq_len(nrow(chars_sp))) {1423 1x
for (j in seq_len(nc)) {1424 2x
if (disp_sp[i, j] && spans_sp[i, j] != 1) {1425 1x
i_cols <- seq(j, j + spans_sp[i, j] - 1)1426 1427 1x
nchar_i <- chars_sp[i, j]1428 1x
cw_i <- widths[i_cols]1429 1x
available_width <- sum(cw_i)1430 1431 1x
if (nchar_i > available_width) {1432
# need to update widths to fit content with colspans1433
# spread width among columns1434 !
widths[i_cols] <- cw_i + spread_integer(nchar_i - available_width, length(cw_i))1435
}1436
}1437
}1438
}1439
}1440 92x
widths1441
}1442 1443
## "number of characters" width in terms of1444
## width of " " for the chosen font family1445 1446
## pdf device with font specification MUST already be open1447 1448
#' Calculate font-specific string width1449
#'1450
#' This function returns the width of each element `x`1451
#' *as a multiple of the width of the space character1452
#' for in declared font*, rounded up to the nearest1453
#' integer. This is used extensively in the text rendering1454
#' ([toString()]) and pagination machinery for1455
#' calculating word wrapping, default column widths,1456
#' lines per page, etc.1457
#'1458
#' @param x (`character`)\cr the string(s) to calculate width(s) for.1459
#' @param fontspec (`font_spec` or `NULL`)\cr if non-NULL, the font to use for1460
#' the calculations (as returned by [font_spec()]). Defaults to "Courier",1461
#' which is a monospace font. If NULL, the width will be returned1462
#' in number of characters by calling `nchar` directly.1463
#' @param tol (`numeric(1)`)\cr the tolerance to use when determining1464
#' if a multiple needs to be rounded up to the next integer. See1465
#' Details.1466
#' @param raw (`logical(1)`)\cr whether unrounded widths should be returned. Defaults to `FALSE`.1467
#'1468
#' @details String width is defined in terms of spaces within1469
#' the specified font. For monospace fonts, this definition1470
#' collapses to the number of characters in the string1471
#' ([nchar()]), but for truetype fonts it does not.1472
#'1473
#' For `raw = FALSE`, non-integer values (the norm in a truetype1474
#' setting) for the number of spaces a string takes up is rounded1475
#' up, *unless the multiple is less than `tol` above the last integer1476
#' before it*. E.g., if `k - num_spaces < tol` for an integer1477
#' `k`, `k` is returned instead of `k+1`.1478
#'1479
#' @seealso [font_spec()]1480
#'1481
#' @examples1482
#' nchar_ttype("hi there!")1483
#'1484
#' nchar_ttype("hi there!", font_spec("Times"))1485
#'1486
#' @export1487
nchar_ttype <- function(x, fontspec = font_spec(), tol = sqrt(.Machine$double.eps), raw = FALSE) {1488
## escape hatch because sometimes we need to call, e.g. make_row_df1489
## but we dont' care about getting the word wrapping right and the1490
## performance penalty was KILLING us. Looking at you1491
## rtables::update_ref_indexing @.@1492 48730x
if (is.null(fontspec)) {1493 1x
return(nchar(x))1494
}1495 48729x
new_dev <- open_font_dev(fontspec)1496 48729x
if (new_dev) {1497 149x
on.exit(close_font_dev())1498
}1499 48729x
if (font_dev_state$ismonospace) { ## WAY faster if we can do it1500 48705x
return(nchar(x))1501
}1502 24x
space_width <- get_space_width()1503
## cwidth_inches_unsafe is ok here because if we don't1504
## have a successfully opened state (somehow), get_space_width1505
## above will error.1506 24x
num_inches_raw <- vapply(x, cwidth_inches_unsafe, 1.0)1507 24x
num_spaces_raw <- num_inches_raw / space_width1508 24x
if (!raw) {1509 1x
num_spaces_ceil <- ceiling(num_spaces_raw)1510
## we don't want to add one when the answer is e.g, 3.00000000000009531511 1x
within_tol <- which(num_spaces_raw + 1 - num_spaces_ceil <= tol)1512 1x
ret <- num_spaces_ceil1513 1x
if (length(within_tol) == 0L) {1514 1x
ret[within_tol] <- floor(num_spaces_raw[within_tol])1515
}1516
} else {1517 23x
ret <- num_spaces_raw1518
}1519 24x
if (!is.null(dim(x))) {1520 !
dim(ret) <- dim(x)1521
} else {1522 24x
names(ret) <- NULL1523
}1524 24x
ret1525
}1526 1527
#' Pad a string and align within string1528
#'1529
#' @inheritParams open_font_dev1530
#' @param x (`string`)\cr a string.1531
#' @param n (`integer(1)`)\cr number of characters in the output string. If `n < nchar(x)`, an error is thrown.1532
#' @param just (`string`)\cr text alignment justification to use. Defaults to `"center"`. Must be one of1533
#' `"center"`, `"right"`, `"left"`, `"dec_right"`, `"dec_left"`, or `"decimal"`.1534
#'1535
#' @return `x`, padded to be a string of length `n`.1536
#'1537
#' @examples1538
#' padstr("abc", 3)1539
#' padstr("abc", 4)1540
#' padstr("abc", 5)1541
#' padstr("abc", 5, "left")1542
#' padstr("abc", 5, "right")1543
#'1544
#' \dontrun{1545
#' # Expect error: "abc" has more than 1 characters1546
#' padstr("abc", 1)1547
#' }1548
#'1549
#' @export1550
padstr <- function(x, n, just = list_valid_aligns(), fontspec = font_spec()) {1551 15649x
just <- match.arg(just)1552 1553 1x
if (length(x) != 1) stop("length of x needs to be 1 and not", length(x))1554 1x
if (is.na(n) || !is.numeric(n) || n < 0) stop("n needs to be numeric and > 0")1555 1556 2x
if (is.na(x)) x <- "<NA>"1557 1558 15647x
nc <- nchar_ttype(x, fontspec)1559 !
if (n < nc) stop("\"", x, "\" has more than ", n, " characters")1560 1561 15647x
switch(just,1562
center = {1563 13742x
pad <- (n - nc) / 21564 13742x
paste0(spaces(floor(pad)), x, spaces(ceiling(pad)))1565
},1566 1754x
left = paste0(x, spaces(n - nc)),1567 10x
right = paste0(spaces(n - nc), x),1568
decimal = {1569 61x
pad <- (n - nc) / 21570 61x
paste0(spaces(floor(pad)), x, spaces(ceiling(pad)))1571
},1572 45x
dec_left = paste0(x, spaces(n - nc)),1573 35x
dec_right = paste0(spaces(n - nc), x)1574
)1575
}1576 1577
spaces <- function(n) {1578 29608x
strrep(" ", n)1579
}1580 1581
.paste_no_na <- function(x, ...) {1582 2408x
paste(na.omit(x), ...)1583
}1584 1585
#' Spread an integer to a given length1586
#'1587
#' @param x (`integer(1)`)\cr number to spread.1588
#' @param len (`integer(1)`)\cr number of times to repeat `x`.1589
#'1590
#' @return If `x` is a scalar whole number value (see [is.wholenumber()]), the value `x` is repeated `len` times.1591
#' Otherwise, an error is thrown.1592
#'1593
#' @examples1594
#' spread_integer(3, 1)1595
#' spread_integer(0, 3)1596
#' spread_integer(1, 3)1597
#' spread_integer(2, 3)1598
#' spread_integer(3, 3)1599
#' spread_integer(4, 3)1600
#' spread_integer(5, 3)1601
#' spread_integer(6, 3)1602
#' spread_integer(7, 3)1603
#'1604
#' @export1605
spread_integer <- function(x, len) {1606 2x
stopifnot(1607 2x
is.wholenumber(x), length(x) == 1, x >= 0,1608 2x
is.wholenumber(len), length(len) == 1, len >= 0,1609 2x
!(len == 0 && x > 0)1610
)1611 1612 1x
if (len == 0) {1613 !
integer(0)1614
} else {1615 1x
y <- rep(floor(x / len), len)1616 1x
i <- 11617 1x
while (sum(y) < x) {1618 1x
y[i] <- y[i] + 11619 1x
if (i == len) {1620 !
i <- 11621
} else {1622 1x
i <- i + 11623
}1624
}1625 1x
y1626
}1627
}1628 1629
#' Check if a value is a whole number1630
#'1631
#' @param x (`numeric(1)`)\cr a numeric value.1632
#' @param tol (`numeric(1)`)\cr a precision tolerance.1633
#'1634
#' @return `TRUE` if `x` is within `tol` of zero, `FALSE` otherwise.1635
#'1636
#' @examples1637
#' is.wholenumber(5)1638
#' is.wholenumber(5.00000000000000001)1639
#' is.wholenumber(.5)1640
#'1641
#' @export1642
is.wholenumber <- function(x, tol = .Machine$double.eps^0.5) {1643 3x
abs(x - round(x)) < tol1644
}
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4