1 |
## state completely sucks and I hate it, but |
|
2 |
## we need a pdf device open to calculate the |
|
3 |
## print width of strings, and we can't be opening |
|
4 |
## a new one every time we want to |
|
5 |
font_dev_state <- new.env() |
|
6 |
font_dev_state$open <- FALSE |
|
7 |
font_dev_state$fontspec <- list() |
|
8 |
font_dev_state$spacewidth <- NA_real_ |
|
9 |
font_dev_state$ismonospace <- NA |
|
10 |
font_dev_state$max_ratio <- NA_real_ |
|
11 |
font_dev_state$dev_num <- NA_integer_ |
|
12 |
font_dev_state$debug_active <- FALSE |
|
13 | ||
14 | ||
15 |
cwidth_inches_unsafe <- function(x) { |
|
16 | 1118x |
convertWidth(unit(1, "strwidth", x), "inches", valueOnly = TRUE) |
17 |
} |
|
18 | ||
19 |
## returns whether it opened a new device or not |
|
20 |
#' Activate font state |
|
21 |
#' |
|
22 |
#' @param fontspec (`font_spec`)\cr a font_spec object specifying the font information to use for |
|
23 |
#' calculating string widths and heights, as returned by [font_spec()]. |
|
24 |
#' @param silent (`logical(1)`)\cr If `FALSE`, the default, a warning will be |
|
25 |
#' emitted if this function switches away from an active graphics device. |
|
26 |
#' |
|
27 |
#' @details The font device state is an environment with |
|
28 |
#' 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 font |
|
38 |
#' only if there is not one currently open with the same font. |
|
39 |
#' If a new device is opened, it caches `spacewidth` and |
|
40 |
#' `ismonospace` for use in `nchar_ttype`). |
|
41 |
#' |
|
42 |
#' `close_font_dev` closes any open font state device |
|
43 |
#' 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 |
#' @return |
|
49 |
#' - `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 |
#' @examples |
|
55 |
#' open_font_dev(font_spec("Times")) |
|
56 |
#' nchar_ttype("Hiya there", font_spec("Times")) |
|
57 |
#' close_font_dev() |
|
58 |
#' |
|
59 |
#' @export |
|
60 |
open_font_dev <- function(fontspec, silent = FALSE) { |
|
61 | 82190x |
if (is.null(fontspec)) { |
62 | 1x |
return(invisible(FALSE)) |
63 | 82188x |
} else if (font_dev_is_open()) { |
64 | 81687x |
if (identical(font_dev_state$fontspec, fontspec)) { |
65 | 81686x |
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 | 81686x |
return(invisible(FALSE)) |
75 |
} else { |
|
76 | 1x |
close_font_dev() |
77 |
} |
|
78 |
} |
|
79 | 502x |
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 # nolint |
80 |
## dump the call stack any time we have cache misses |
|
81 |
## and have to open a completely new font state device |
|
82 | 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 |
toret |
91 |
} |
|
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 | 502x |
tmppdf <- tempfile(fileext = ".pdf") |
100 | 502x |
pdf(tmppdf) |
101 | 502x |
grid.newpage() |
102 | 502x |
gp <- gpar_from_fspec(fontspec) |
103 | 502x |
pushViewport(plotViewport(gp = gp)) |
104 | 502x |
spcwidth <- cwidth_inches_unsafe(" ") |
105 | 502x |
assign("open", TRUE, envir = font_dev_state) |
106 | 502x |
assign("fontspec", fontspec, envir = font_dev_state) |
107 | 502x |
assign("spacewidth", spcwidth, envir = font_dev_state) |
108 | 502x |
assign("ismonospace", spcwidth == cwidth_inches_unsafe("W"), |
109 | 502x |
envir = font_dev_state |
110 |
) |
|
111 | 502x |
assign("dev_num", dev.cur(), |
112 | 502x |
envir = font_dev_state |
113 |
) |
|
114 | 502x |
invisible(TRUE) |
115 |
} |
|
116 | ||
117 |
#' @rdname open_font_dev |
|
118 |
#' @export |
|
119 |
close_font_dev <- function() { |
|
120 | 502x |
if (font_dev_state$open) { |
121 | 502x |
dev.off(font_dev_state$dev_num) |
122 | 502x |
assign("open", FALSE, envir = font_dev_state) |
123 | 502x |
assign("fontspec", list(), envir = font_dev_state) |
124 | 502x |
assign("spacewidth", NA_real_, envir = font_dev_state) |
125 | 502x |
assign("ismonospace", NA, envir = font_dev_state) |
126 | 502x |
assign("dev_num", NA_integer_, envir = font_dev_state) |
127 |
} |
|
128 | 502x |
invisible(NULL) |
129 |
} |
|
130 | ||
131 |
#' @rdname open_font_dev |
|
132 |
#' @export |
|
133 |
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 <- TRUE |
136 | 1x |
invisible(NULL) |
137 |
} |
|
138 | ||
139 |
#' @rdname open_font_dev |
|
140 |
#' @export |
|
141 |
undebug_font_dev <- function() { |
|
142 | 1x |
message("no longer debugging font device swapping.") |
143 | 1x |
font_dev_state$debug_active <- FALSE |
144 | 1x |
invisible(NULL) |
145 |
} |
|
146 | ||
147 | ||
148 |
## can only be called when font_dev_state$open is TRUE |
|
149 |
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$spacewidth |
157 |
} |
|
158 | ||
159 |
.open_fdev_is_monospace <- function() { |
|
160 | 32643x |
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 | 32641x |
font_dev_state$ismonospace |
167 |
} |
|
168 | ||
169 |
## safe wrapper around .open_fdev_is_monospace |
|
170 |
is_monospace <- function(fontspec = font_spec(font_family, font_size, lineheight), |
|
171 |
font_family = "Courier", |
|
172 |
font_size = 8, |
|
173 |
lineheight = 1) { |
|
174 | 32642x |
if (is.null(fontspec)) { |
175 | 1x |
return(TRUE) |
176 |
} |
|
177 | 32641x |
new_dev <- open_font_dev(fontspec) |
178 | 32641x |
if (new_dev) { |
179 | 31x |
on.exit(close_font_dev()) |
180 |
} |
|
181 | 32641x |
.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 |
## 1 |
|
193 |
## } else { |
|
194 |
## font_dev_state$maxratio |
|
195 |
## } |
|
196 |
## } |
|
197 | ||
198 |
gpar_from_fspec <- function(fontspec) { |
|
199 | 508x |
gpar( |
200 | 508x |
fontfamily = fontspec$family, |
201 | 508x |
fontsize = fontspec$size, |
202 | 508x |
lineheight = fontspec$lineheight |
203 |
) |
|
204 |
} |
|
205 | ||
206 | 114857x |
font_dev_is_open <- function() font_dev_state$open |
207 | ||
208 |
#' Default horizontal separator |
|
209 |
#' |
|
210 |
#' The default horizontal separator character which can be displayed in the current |
|
211 |
#' charset for use in rendering table-like objects. |
|
212 |
#' |
|
213 |
#' @param hsep_char (`string`)\cr character that will be set in the R environment |
|
214 |
#' options as the default horizontal separator. Must be a single character. Use |
|
215 |
#' `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 a |
|
218 |
#' locale that uses a UTF character set, otherwise an ASCII hyphen with a |
|
219 |
#' once-per-session warning. |
|
220 |
#' |
|
221 |
#' @examples |
|
222 |
#' default_hsep() |
|
223 |
#' set_default_hsep("o") |
|
224 |
#' default_hsep() |
|
225 |
#' |
|
226 |
#' @name default_horizontal_sep |
|
227 |
#' @export |
|
228 |
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 |
) # nocov |
|
241 |
} # nocov |
|
242 |
hsep <- "-" # nocov |
|
243 |
} |
|
244 |
} else { |
|
245 |
hsep <- system_default_hsep |
|
246 |
} |
|
247 |
hsep |
|
248 |
} |
|
249 | ||
250 |
#' @name default_horizontal_sep |
|
251 |
#' @export |
|
252 |
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 | 363x |
spans <- mat$spans |
259 | 363x |
keep_mat <- mat$display |
260 | 363x |
body <- mat$strings |
261 | ||
262 | 363x |
nr <- nrow(body) |
263 | ||
264 | 363x |
cell_widths_mat <- matrix(rep(colwidths, nr), nrow = nr, byrow = TRUE) |
265 | 363x |
nc <- ncol(cell_widths_mat) |
266 | ||
267 | 363x |
for (i in seq_len(nrow(body))) { |
268 | 6517x |
if (any(!keep_mat[i, ])) { # any spans? |
269 | 6x |
j <- 1 |
270 | 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 + nj |
276 |
} else { |
|
277 | 4x |
j + 1 |
278 |
} |
|
279 |
} |
|
280 |
} |
|
281 |
} |
|
282 | 363x |
cell_widths_mat |
283 |
} |
|
284 | ||
285 |
# Main function that does the wrapping |
|
286 |
do_cell_fnotes_wrap <- function(mat, widths, max_width, tf_wrap, fontspec, expand_newlines = TRUE) { |
|
287 | 210x |
col_gap <- mf_colgap(mat) |
288 | 210x |
ncchar <- sum(widths) + (length(widths) - as.integer(mf_has_rlabels(mat))) * col_gap |
289 | 210x |
inset <- table_inset(mat) |
290 | ||
291 |
## Text wrapping checks |
|
292 | 210x |
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 + inset |
297 |
} |
|
298 | 92x |
assert_number(max_width, lower = 0) |
299 |
} |
|
300 | ||
301 |
## Check for having the right number of widths |
|
302 | 210x |
stopifnot(length(widths) == ncol(mat$strings)) |
303 | ||
304 |
## format the to ASCII |
|
305 | 210x |
cell_widths_mat <- .calc_cell_widths(mat, widths, col_gap) |
306 | ||
307 |
# Check that indentation is correct (it works only for body) |
|
308 | 210x |
.check_indentation(mat, row_col_width = cell_widths_mat[, 1, drop = TRUE]) |
309 | 207x |
mod_ind_list <- .modify_indentation(mat, cell_widths_mat, do_what = "remove") |
310 | 207x |
mfs <- mod_ind_list[["mfs"]] |
311 | 207x |
cell_widths_mat <- mod_ind_list[["cell_widths_mat"]] |
312 | ||
313 |
# Main wrapper |
|
314 | 207x |
mf_strings(mat) <- matrix( |
315 | 207x |
unlist(mapply(wrap_string, |
316 | 207x |
str = mfs, |
317 | 207x |
width = cell_widths_mat, |
318 | 207x |
collapse = "\n", |
319 | 207x |
MoreArgs = list(fontspec = fontspec) |
320 |
)), |
|
321 | 207x |
ncol = ncol(mfs) |
322 |
) |
|
323 | ||
324 | 207x |
if (expand_newlines) { |
325 |
## XXXXX this is wrong and will break for listings cause we don't know when |
|
326 |
## we need has_topleft to be FALSE!!!!!!!!!! |
|
327 | 207x |
mat <- mform_handle_newlines(mat) |
328 | ||
329 |
## this updates extents in rinfo AND nlines in ref_fnotes_df |
|
330 |
## mat already has fontspec on it so no need to pass that down |
|
331 | 207x |
mat <- update_mf_nlines(mat, max_width = max_width) |
332 | ||
333 |
# Re-indenting |
|
334 | 207x |
mf_strings(mat) <- .modify_indentation(mat, cell_widths_mat, do_what = "add")[["mfs"]] |
335 | 207x |
.check_indentation(mat) # all went well |
336 |
} |
|
337 | 207x |
mat |
338 |
} |
|
339 | ||
340 |
# Helper function to see if body indentation matches (minimum) |
|
341 |
# It sees if there is AT LEAST the indentation contained in rinfo |
|
342 |
.check_indentation <- function(mat, row_col_width = NULL) { |
|
343 |
# mf_nrheader(mat) # not useful |
|
344 | 418x |
mf_nlh <- mf_nlheader(mat) |
345 | 418x |
mf_lgrp <- mf_lgrouping(mat) |
346 | 418x |
mf_str <- mf_strings(mat) |
347 |
# we base everything on the groupings -> unique indentation identifiers |
|
348 | 418x |
if (!is.null(mf_rinfo(mat))) { # this happens in rare cases with rtables::rtable() |
349 | 418x |
mf_ind <- c(rep(0, mf_nrheader(mat)), mf_rinfo(mat)$indent) # XXX to fix with topleft |
350 |
} else { |
|
351 | ! |
mf_ind <- rep(0, mf_nrheader(mat)) |
352 |
} |
|
353 | 418x |
ind_std <- paste0(rep(" ", mat$indent_size), collapse = "") |
354 | ||
355 |
# Expected indent (-x negative numbers should not appear at this stage) |
|
356 | 418x |
stopifnot(all(mf_ind >= 0)) |
357 | 418x |
real_indent <- vapply(mf_ind, function(ii) { |
358 | 7821x |
paste0(rep(ind_std, ii), collapse = "") |
359 | 418x |
}, character(1)) |
360 | ||
361 | 418x |
if (!is.null(row_col_width) && any(row_col_width > 0) && !is.null(mf_rinfo(mat))) { # third is rare case |
362 |
# Self consistency test for row_col_width (same groups should have same width) |
|
363 |
# This should not be necessary (nocov) |
|
364 | 210x |
consistency_check <- vapply(unique(mf_lgrp), function(ii) { |
365 | 3929x |
width_per_grp <- row_col_width[which(mf_lgrp == ii)] |
366 | 3929x |
all(width_per_grp == width_per_grp[1]) |
367 | 210x |
}, logical(1)) |
368 | 210x |
stopifnot(all(consistency_check)) |
369 | ||
370 |
# Taking only one width for each indentation grouping |
|
371 | 210x |
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_rows |
|
374 | 210x |
nchar_real_indent <- nchar(real_indent) |
375 | 210x |
body_rows <- seq(mf_nrheader(mat) + 1, length(nchar_real_indent)) |
376 | 210x |
nchar_real_indent[body_rows] <- nchar_real_indent[body_rows] + |
377 | 210x |
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 | 210x |
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 detector |
|
391 | 416x |
correct_indentation <- vapply(seq_along(mf_lgrp), function(xx) { |
392 | 8249x |
grouping <- mf_lgrp[xx] |
393 | 8249x |
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 indentation |
399 |
} |
|
400 |
# Cases where no indent are true by definition |
|
401 | 8216x |
return(TRUE) |
402 | 416x |
}, logical(1)) |
403 | ||
404 | 416x |
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 |
) # nocov |
409 |
} |
|
410 |
} |
|
411 | ||
412 |
# Helper function that takes out or adds the proper indentation |
|
413 |
.modify_indentation <- function(mat, cell_widths_mat, do_what = c("remove", "add")) { |
|
414 |
# Extract info |
|
415 | 414x |
mfs <- mf_strings(mat) # we work on mfs |
416 | 414x |
mf_nlh <- mf_nlheader(mat) |
417 | 414x |
mf_l <- mf_lgrouping(mat) |
418 | 414x |
if (!is.null(mf_rinfo(mat))) { # this happens in rare cases with rtables::rtable() |
419 | 414x |
mf_ind <- c(rep(0, mf_nrheader(mat)), mf_rinfo(mat)$indent) # XXX to fix with topleft |
420 |
} else { |
|
421 | ! |
mf_ind <- rep(0, mf_nrheader(mat)) |
422 |
} |
|
423 | 414x |
stopifnot(length(mf_ind) == length(unique(mf_l))) # Check for indentation and grouping |
424 | 414x |
ind_std <- paste0(rep(" ", mat$indent_size), collapse = "") # standard size of indent 1 |
425 | ||
426 |
# Create real indentation |
|
427 | 414x |
real_indent <- sapply(mf_ind, function(ii) paste0(rep(ind_std, ii), collapse = "")) |
428 | ||
429 |
# Use groupings to add or remove proper indentation |
|
430 | 414x |
lbl_row <- mfs[, 1, drop = TRUE] |
431 | 414x |
for (ii in seq_along(lbl_row)) { |
432 | 8240x |
grp <- mf_l[ii] |
433 | 8240x |
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.") # nocov |
|
442 |
} |
|
443 |
} else { |
|
444 | 8211x |
mfs[ii, 1] <- lbl_row[ii] |
445 |
} |
|
446 |
} |
|
447 |
# Final return |
|
448 | 414x |
return(list("mfs" = mfs, "cell_widths_mat" = cell_widths_mat)) |
449 |
} |
|
450 | ||
451 |
## take a character vector and return whether the value is |
|
452 |
## a string version of a number or not |
|
453 |
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 need |
|
460 | 595x |
sdiff <- setdiff(vec, c(list_valid_aligns(), "c")) |
461 | 595x |
if (length(sdiff) > 0) { |
462 | ! |
stop("Invalid text-alignment(s): ", paste(sdiff, collapse = ", ")) |
463 |
} |
|
464 | 595x |
grepl("dec", vec) |
465 |
} |
|
466 | ||
467 | 450x |
any_dec_align <- function(vec) any(is_dec_align(vec)) |
468 | ||
469 |
#' Decimal alignment |
|
470 |
#' |
|
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 decimal |
|
479 |
#' alignment (`decimal`) only when there is padding present. This may occur if column widths are |
|
480 |
#' 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 decimal |
|
482 |
#' alignment is used. |
|
483 |
#' |
|
484 |
#' @return A processed string matrix of class `MatrixPrintForm` with decimal-aligned values. |
|
485 |
#' |
|
486 |
#' @seealso [toString()], [MatrixPrintForm()] |
|
487 |
#' |
|
488 |
#' @examples |
|
489 |
#' 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 |
#' @export |
|
495 |
decimal_align <- function(string_mat, align_mat) { |
|
496 |
## Evaluate if any values are to be decimal aligned |
|
497 | 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 alignments |
|
502 | 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 Law |
|
506 |
## Are there any values to be decimal aligned? safe if |
|
507 | 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 between |
|
511 |
## 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_i |
517 | 82x |
col_ia <- col_i[!nonalign] |
518 | ||
519 |
## Do decimal alignment |
|
520 | 82x |
if (length(col_ia) > 0) { |
521 |
# Special case: scientific notation |
|
522 | 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 string |
|
533 | 81x |
matches <- gregexpr("\\d+\\.\\d+|\\d+", col_ia) |
534 | 81x |
more_than_one <- vapply(matches, function(x) { |
535 | 692x |
sum(attr(x, "match.length") > 0) > 1 |
536 | 81x |
}, logical(1)) |
537 |
## Throw error in case any have more than 1 numbers |
|
538 | 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 |
} # nocov |
|
560 | 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 spaces |
|
579 | 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 together |
|
582 | 79x |
aligned <- paste(left_mod, separator, right_mod, sep = "") |
583 | 79x |
string_mat[!nonalign, i] <- aligned |
584 |
} |
|
585 |
} |
|
586 |
} |
|
587 | 42x |
string_mat |
588 |
} |
|
589 | ||
590 |
## this gives the conversion from number of spaces to number of characters |
|
591 |
## for use in, e.g., repping out divider lines. |
|
592 |
calc_str_adj <- function(str, fontspec) { |
|
593 | 157x |
if (nchar(str) == 0) { |
594 | ! |
return(0) |
595 |
} |
|
596 | 157x |
nchar(str) / nchar_ttype(str, fontspec, raw = TRUE) |
597 |
} |
|
598 | ||
599 |
# toString --------------------------------------------------------------------- |
|
600 |
# main printing code for MatrixPrintForm |
|
601 | ||
602 |
#' @description |
|
603 |
#' All objects that are printed to console pass via `toString`. This function allows |
|
604 |
#' fundamental formatting specifications to be applied to final output, like column widths |
|
605 |
#' and relative wrapping (`width`), title and footer wrapping (`tf_wrap = TRUE` and |
|
606 |
#' `max_width`), and horizontal separator character (e.g. `hsep = "+"`). |
|
607 |
#' |
|
608 |
#' @inheritParams MatrixPrintForm |
|
609 |
#' @inheritParams open_font_dev |
|
610 |
#' @param widths (`numeric` or `NULL`)\cr Proposed widths for the columns of `x`. The expected |
|
611 |
#' length of this numeric vector can be retrieved with `ncol(x) + 1` as the column of row names |
|
612 |
#' must also be considered. |
|
613 |
#' @param hsep (`string`)\cr character to repeat to create header/body separator line. If |
|
614 |
#' `NULL`, the object value will be used. If `" "`, an empty separator will be printed. See |
|
615 |
#' [default_hsep()] for more information. |
|
616 |
#' @param tf_wrap (`flag`)\cr whether the text for title, subtitles, and footnotes should be wrapped. |
|
617 |
#' @param max_width (`integer(1)`, `string` or `NULL`)\cr width that title and footer (including |
|
618 |
#' footnotes) materials should be word-wrapped to. If `NULL`, it is set to the current print width of the |
|
619 |
#' session (`getOption("width")`). If set to `"auto"`, the width of the table (plus any table inset) is |
|
620 |
#' used. Parameter is ignored if `tf_wrap = FALSE`. |
|
621 |
#' @param ttype_ok (`logical(1)`)\cr should truetype (non-monospace) fonts be |
|
622 |
#' allowed via `fontspec`. Defaults to `FALSE`. This parameter is primarily |
|
623 |
#' for internal testing and generally should not be set by end users. |
|
624 |
#' |
|
625 |
#' @details |
|
626 |
#' Manual insertion of newlines is not supported when `tf_wrap = TRUE` and will result in a warning and |
|
627 |
#' undefined wrapping behavior. Passing vectors of already split strings remains supported, however in this |
|
628 |
#' case each string is word-wrapped separately with the behavior described above. |
|
629 |
#' |
|
630 |
#' @return A character string containing the ASCII rendering of the table-like object represented by `x`. |
|
631 |
#' |
|
632 |
#' @seealso [wrap_string()] |
|
633 |
#' |
|
634 |
#' @examples |
|
635 |
#' mform <- basic_matrix_form(mtcars) |
|
636 |
#' cat(toString(mform)) |
|
637 |
#' |
|
638 |
#' @rdname tostring |
|
639 |
#' @exportMethod toString |
|
640 |
setMethod("toString", "MatrixPrintForm", function(x, |
|
641 |
widths = NULL, |
|
642 |
tf_wrap = FALSE, |
|
643 |
max_width = NULL, |
|
644 |
col_gap = mf_colgap(x), |
|
645 |
hsep = NULL, |
|
646 |
fontspec = font_spec(), |
|
647 |
ttype_ok = FALSE) { |
|
648 | 160x |
checkmate::assert_flag(tf_wrap) |
649 | ||
650 |
## we are going to use the pdf device and grid to understand the actual |
|
651 |
## print width of things given our font family and font size |
|
652 | 160x |
new_dev <- open_font_dev(fontspec) |
653 | 160x |
if (new_dev) { |
654 | 150x |
on.exit(close_font_dev()) |
655 |
} |
|
656 | ||
657 | 160x |
if (!is_monospace(fontspec = fontspec) && !ttype_ok) { |
658 | ! |
stop( |
659 | ! |
"non-monospace font specified in toString call; this would result in cells contents not lining up exactly. ", |
660 | ! |
"If you truly want this behavior please set ttype_ok = TRUE in the call to toString/export_as_txt/export_as_pdf" |
661 |
) |
|
662 |
} |
|
663 | 160x |
mat <- matrix_form(x, indent_rownames = TRUE, fontspec = fontspec) |
664 | ||
665 |
# Check for \n in mat strings -> if there are any, matrix_form did not work |
|
666 | 160x |
if (any(grepl("\n", mf_strings(mat)))) { |
667 | ! |
stop( |
668 | ! |
"Found newline characters (\\n) in string matrix produced by matrix_form. ", |
669 | ! |
"This is not supported and implies missbehavior on the first parsing (in matrix_form). ", |
670 | ! |
"Please contact the maintainer or file an issue." |
671 | ! |
) # nocov |
672 |
} |
|
673 | 160x |
if (any(grepl("\r", mf_strings(mat)))) { |
674 | ! |
stop( |
675 | ! |
"Found recursive special characters (\\r) in string matrix produced by matrix_form. ", |
676 | ! |
"This special character is not supported and should be removed." |
677 | ! |
) # nocov |
678 |
} |
|
679 | ||
680 |
# Check that expansion worked for header -> should not happen |
|
681 | 160x |
if (!is.null(mf_rinfo(mat)) && # rare case of rtables::rtable() |
682 | 160x |
(length(mf_lgrouping(mat)) != nrow(mf_strings(mat)) || # non-unique grouping test # nolint |
683 | 160x |
mf_nrheader(mat) + nrow(mf_rinfo(mat)) != length(unique(mf_lgrouping(mat))))) { # nolint |
684 | ! |
stop( |
685 | ! |
"The sum of the expected nrows header and nrows of content table does ", |
686 | ! |
"not match the number of rows in the string matrix. To our knowledge, ", |
687 | ! |
"this is usually of a problem in solving newline characters (\\n) in the header. ", |
688 | ! |
"Please contact the maintaner or file an issue." |
689 | ! |
) # nocov |
690 |
} |
|
691 | ||
692 | 160x |
inset <- table_inset(mat) |
693 | ||
694 |
# if cells are decimal aligned, run propose column widths |
|
695 |
# if the provided widths is less than proposed width, return an error |
|
696 | 160x |
if (any_dec_align(mf_aligns(mat))) { |
697 | 22x |
aligned <- propose_column_widths(x, fontspec = fontspec) |
698 | ||
699 |
# catch any columns that require widths more than what is provided |
|
700 | 20x |
if (!is.null(widths)) { |
701 | 9x |
how_wide <- sapply(seq_along(widths), function(i) c(widths[i] - aligned[i])) |
702 | 9x |
too_wide <- how_wide < 0 |
703 | 9x |
if (any(too_wide)) { |
704 | 2x |
desc_width <- paste(paste( |
705 | 2x |
names(which(too_wide)), |
706 | 2x |
paste0("(", how_wide[too_wide], ")") |
707 | 2x |
), collapse = ", ") |
708 | 2x |
stop( |
709 | 2x |
"Inserted width(s) for column(s) ", desc_width, |
710 | 2x |
" is(are) not wide enough for the desired alignment." |
711 |
) |
|
712 |
} |
|
713 |
} |
|
714 |
} |
|
715 | ||
716 |
# Column widths are fixed here |
|
717 | 156x |
if (is.null(widths)) { |
718 |
# if mf does not have widths -> propose them |
|
719 | 130x |
widths <- mf_col_widths(x) %||% propose_column_widths(x, fontspec = fontspec) |
720 |
} else { |
|
721 | 26x |
mf_col_widths(x) <- widths |
722 |
} |
|
723 | ||
724 |
## Total number of characters for the table |
|
725 |
## col_gap (and table inset) are in number of spaces |
|
726 |
## so we're ok here even in the truetype case |
|
727 | 156x |
ncchar <- sum(widths) + (length(widths) - 1) * col_gap |
728 | ||
729 |
## max_width for wrapping titles and footers (not related to ncchar if not indirectly) |
|
730 | 156x |
max_width <- .handle_max_width( |
731 | 156x |
tf_wrap = tf_wrap, |
732 | 156x |
max_width = max_width, |
733 | 156x |
colwidths = widths, |
734 | 156x |
col_gap = col_gap, |
735 | 156x |
inset = inset |
736 |
) |
|
737 | ||
738 |
# Main wrapper function for table core |
|
739 | 156x |
mat <- do_cell_fnotes_wrap(mat, widths, max_width = max_width, tf_wrap = tf_wrap, fontspec = fontspec) |
740 | ||
741 | 153x |
body <- mf_strings(mat) |
742 | 153x |
aligns <- mf_aligns(mat) |
743 | 153x |
keep_mat <- mf_display(mat) |
744 |
## spans <- mat$spans |
|
745 | 153x |
mf_ri <- mf_rinfo(mat) |
746 | 153x |
ref_fnotes <- mf_rfnotes(mat) |
747 | 153x |
nl_header <- mf_nlheader(mat) |
748 | ||
749 | 153x |
cell_widths_mat <- .calc_cell_widths(mat, widths, col_gap) |
750 | ||
751 |
# decimal alignment |
|
752 | 153x |
if (any_dec_align(aligns)) { |
753 | 18x |
body <- decimal_align(body, aligns) |
754 |
} |
|
755 | ||
756 |
# Content is a matrix of cells with the right amount of spaces |
|
757 | 153x |
content <- matrix( |
758 | 153x |
mapply(padstr, body, cell_widths_mat, aligns, MoreArgs = list(fontspec = fontspec)), |
759 | 153x |
ncol = ncol(body) |
760 |
) |
|
761 | 153x |
content[!keep_mat] <- NA |
762 | ||
763 |
# Define gap string and divisor string |
|
764 | 153x |
gap_str <- strrep(" ", col_gap) |
765 | 153x |
if (is.null(hsep)) { |
766 | 121x |
hsep <- horizontal_sep(mat) |
767 |
} |
|
768 | 153x |
adj_hsep <- calc_str_adj(hsep, fontspec) |
769 | 153x |
div <- substr(strrep(hsep, ceiling(ncchar * adj_hsep)), 1, ceiling(ncchar * adj_hsep)) |
770 | 153x |
hsd <- header_section_div(mat) |
771 | 153x |
if (!is.na(hsd)) { |
772 | ! |
adj_hsd <- calc_str_adj(hsd, fontspec) |
773 | ! |
hsd <- substr(strrep(hsd, ceiling(ncchar * adj_hsd)), 1, ceiling(ncchar * adj_hsd)) |
774 |
} else { |
|
775 | 153x |
hsd <- NULL # no divisor |
776 |
} |
|
777 | ||
778 |
# text head (paste w/o NA content header and gap string) |
|
779 | 153x |
txt_head <- apply(head(content, nl_header), 1, .paste_no_na, collapse = gap_str) |
780 | ||
781 |
# txt body |
|
782 | 153x |
sec_seps_df <- mf_ri[, c("abs_rownumber", "trailing_sep"), drop = FALSE] |
783 | 153x |
if (!is.null(sec_seps_df) && any(!is.na(sec_seps_df$trailing_sep))) { |
784 | 2x |
bdy_cont <- tail(content, -nl_header) |
785 |
## unfortunately we count "header rows" wrt line grouping so it |
|
786 |
## doesn't match the real (i.e. body) rows as is |
|
787 | 2x |
row_grouping <- tail(mf_lgrouping(mat), -nl_header) - mf_nrheader(mat) |
788 | 2x |
nrbody <- NROW(bdy_cont) |
789 | 2x |
stopifnot(length(row_grouping) == nrbody) |
790 |
## all rows with non-NA section divs and the final row (regardless of NA status) |
|
791 |
## fixes #77 |
|
792 | 2x |
sec_seps_df <- sec_seps_df[unique(c( |
793 | 2x |
which(!is.na(sec_seps_df$trailing_sep)), |
794 | 2x |
NROW(sec_seps_df) |
795 |
)), ] |
|
796 | 2x |
txt_body <- character() |
797 | 2x |
sec_strt <- 1 |
798 | 2x |
section_rws <- sec_seps_df$abs_rownumber |
799 | 2x |
for (i in seq_len(NROW(section_rws))) { |
800 | 6x |
cur_rownum <- section_rws[i] |
801 | 6x |
sec_end <- max(which(row_grouping == cur_rownum)) |
802 | 6x |
txt_body <- c( |
803 | 6x |
txt_body, |
804 | 6x |
apply(bdy_cont[seq(sec_strt, sec_end), , drop = FALSE], |
805 | 6x |
1, |
806 | 6x |
.paste_no_na, |
807 | 6x |
collapse = gap_str |
808 |
), |
|
809 |
## don't print section dividers if they would be the last thing before the |
|
810 |
## footer divider |
|
811 |
## this also ensures an extraneous sec div won't be printed if we have non-sec-div |
|
812 |
## rows after the last sec div row (#77) |
|
813 | 6x |
if (sec_end < nrbody) { |
814 | 4x |
adj_i <- calc_str_adj(sec_seps_df$trailing_sep[i], fontspec) |
815 | 4x |
substr( |
816 | 4x |
strrep(sec_seps_df$trailing_sep[i], ceiling(ncchar * adj_i)), 1, |
817 | 4x |
ceiling((ncchar - inset) * adj_i) |
818 |
) |
|
819 |
} |
|
820 |
) |
|
821 | 6x |
sec_strt <- sec_end + 1 |
822 |
} |
|
823 |
} else { |
|
824 |
# This is the usual default pasting |
|
825 | 151x |
txt_body <- apply(tail(content, -nl_header), 1, .paste_no_na, collapse = gap_str) |
826 |
} |
|
827 | ||
828 |
# retrieving titles and footers |
|
829 | 153x |
allts <- all_titles(mat) |
830 | ||
831 | 153x |
ref_fnotes <- reorder_ref_fnotes(ref_fnotes) |
832 |
# Fix for ref_fnotes with \n characters XXX this does not count in the pagination |
|
833 | 153x |
if (any(grepl("\\n", ref_fnotes))) { |
834 | 2x |
ref_fnotes <- unlist(strsplit(ref_fnotes, "\n", fixed = TRUE)) |
835 |
} |
|
836 | ||
837 | 153x |
allfoots <- list( |
838 | 153x |
"main_footer" = main_footer(mat), |
839 | 153x |
"prov_footer" = prov_footer(mat), |
840 | 153x |
"ref_footnotes" = ref_fnotes |
841 |
) |
|
842 | 153x |
allfoots <- allfoots[!sapply(allfoots, is.null)] |
843 | ||
844 |
## Wrapping titles if they go beyond the horizontally allowed space |
|
845 | 153x |
if (tf_wrap) { |
846 | 68x |
new_line_warning(allts) |
847 | 68x |
allts <- wrap_txt(allts, max_width, fontspec = fontspec) |
848 |
} |
|
849 | 153x |
titles_txt <- if (any(nzchar(allts))) c(allts, "", .do_inset(div, inset)) else NULL |
850 | ||
851 |
# Wrapping footers if they go beyond the horizontally allowed space |
|
852 | 153x |
if (tf_wrap) { |
853 | 68x |
new_line_warning(allfoots) |
854 | 68x |
allfoots$main_footer <- wrap_txt(allfoots$main_footer, max_width - inset, fontspec = fontspec) |
855 | 68x |
allfoots$ref_footnotes <- wrap_txt(allfoots$ref_footnotes, max_width - inset, fontspec = fontspec) |
856 |
## no - inset here because the prov_footer is not inset |
|
857 | 68x |
allfoots$prov_footer <- wrap_txt(allfoots$prov_footer, max_width, fontspec = fontspec) |
858 |
} |
|
859 | ||
860 |
# Final return |
|
861 | 153x |
paste0( |
862 | 153x |
paste(c( |
863 | 153x |
titles_txt, # .do_inset(div, inset) happens if there are any titles |
864 | 153x |
.do_inset(txt_head, inset), |
865 | 153x |
.do_inset(div, inset), |
866 | 153x |
.do_inset(hsd, inset), # header_section_div if present |
867 | 153x |
.do_inset(txt_body, inset), |
868 | 153x |
.footer_inset_helper(allfoots, div, inset) |
869 | 153x |
), collapse = "\n"), |
870 | 153x |
"\n" |
871 |
) |
|
872 |
}) |
|
873 | ||
874 |
# Switcher for the 3 options for max_width (NULL, numeric, "auto")) |
|
875 |
.handle_max_width <- function(tf_wrap, max_width, |
|
876 |
cpp = NULL, # Defaults to getOption("width", 80L) |
|
877 |
# Things for auto |
|
878 |
inset = NULL, colwidths = NULL, col_gap = NULL) { |
|
879 | 234x |
max_width <- if (!tf_wrap) { |
880 | 114x |
if (!is.null(max_width)) { |
881 | 1x |
warning("tf_wrap is FALSE - ignoring non-null max_width value.") |
882 |
} |
|
883 | 114x |
NULL |
884 | 234x |
} else if (tf_wrap) { |
885 | 120x |
if (is.null(max_width)) { |
886 | 36x |
if (is.null(cpp) || is.na(cpp)) { |
887 | 7x |
getOption("width", 80L) |
888 |
} else { |
|
889 | 29x |
cpp |
890 |
} |
|
891 | 84x |
} else if (is.numeric(max_width)) { |
892 | 79x |
max_width |
893 | 5x |
} else if (is.character(max_width) && identical(max_width, "auto")) { |
894 |
# This should not happen, but just in case |
|
895 | 4x |
if (any(sapply(list(inset, colwidths, col_gap), is.null))) { |
896 | 1x |
stop("inset, colwidths, and col_gap must all be non-null when max_width is \"auto\".") |
897 |
} |
|
898 | 3x |
inset + sum(colwidths) + (length(colwidths) - 1) * col_gap |
899 |
} else { |
|
900 | 1x |
stop("max_width must be NULL, a numeric value, or \"auto\".") |
901 |
} |
|
902 |
} |
|
903 | 232x |
return(max_width) |
904 |
} |
|
905 | ||
906 |
.do_inset <- function(x, inset) { |
|
907 | 1038x |
if (inset == 0 || !any(nzchar(x))) { |
908 | 1019x |
return(x) |
909 |
} |
|
910 | 19x |
padding <- strrep(" ", inset) |
911 | 19x |
if (is.character(x)) { |
912 | 19x |
x <- paste0(padding, x) |
913 | ! |
} else if (is(x, "matrix")) { |
914 | ! |
x[, 1] <- .do_inset(x[, 1, drop = TRUE], inset) |
915 |
} |
|
916 | 19x |
x |
917 |
} |
|
918 | ||
919 |
.inset_div <- function(txt, div, inset) { |
|
920 | 105x |
c(.do_inset(div, inset), "", txt) |
921 |
} |
|
922 | ||
923 |
.footer_inset_helper <- function(footers_v, div, inset) { |
|
924 | 153x |
div_done <- FALSE # nolint |
925 | 153x |
fter <- footers_v$main_footer |
926 | 153x |
prvf <- footers_v$prov_footer |
927 | 153x |
rfn <- footers_v$ref_footnotes |
928 | 153x |
footer_txt <- .do_inset(rfn, inset) |
929 | 153x |
if (any(nzchar(footer_txt))) { |
930 | 14x |
footer_txt <- .inset_div(footer_txt, div, inset) |
931 |
} |
|
932 | 153x |
if (any(vapply( |
933 | 153x |
footers_v, function(x) any(nzchar(x)), |
934 | 153x |
TRUE |
935 |
))) { |
|
936 | 91x |
if (any(nzchar(prvf))) { |
937 | 89x |
provtxt <- c( |
938 | 89x |
if (any(nzchar(fter))) "", |
939 | 89x |
prvf |
940 |
) |
|
941 |
} else { |
|
942 | 2x |
provtxt <- character() |
943 |
} |
|
944 | 91x |
footer_txt <- c( |
945 | 91x |
footer_txt, |
946 | 91x |
.inset_div( |
947 | 91x |
c( |
948 | 91x |
.do_inset(fter, inset), |
949 | 91x |
provtxt |
950 |
), |
|
951 | 91x |
div, |
952 | 91x |
inset |
953 |
) |
|
954 |
) |
|
955 |
} |
|
956 | 153x |
footer_txt |
957 |
} |
|
958 | ||
959 |
reorder_ref_fnotes <- function(fns) { |
|
960 | 156x |
ind <- gsub("\\{(.*)\\}.*", "\\1", fns) |
961 | 156x |
ind_num <- suppressWarnings(as.numeric(ind)) |
962 | 156x |
is_num <- !is.na(ind_num) |
963 | 156x |
is_asis <- ind == fns |
964 | ||
965 | 156x |
if (all(is_num)) { |
966 | 140x |
ord_num <- order(ind_num) |
967 | 140x |
ord_char <- NULL |
968 | 140x |
ord_other <- NULL |
969 |
} else { |
|
970 | 16x |
ord_num <- order(ind_num[is_num]) |
971 | 16x |
ord_char <- order(ind[!is_num & !is_asis]) |
972 | 16x |
ord_other <- order(ind[is_asis]) |
973 |
} |
|
974 | 156x |
c(fns[is_num][ord_num], fns[!is_num & !is_asis][ord_char], ind[is_asis][ord_other]) |
975 |
} |
|
976 | ||
977 |
new_line_warning <- function(str_v) { |
|
978 | 136x |
if (any(unlist(sapply(str_v, grepl, pattern = "\n")))) { |
979 | ! |
msg <- c( |
980 | ! |
"Detected manual newlines when automatic title/footer word-wrapping is on.", |
981 | ! |
"This is unsupported and will result in undefined behavior. Please either ", |
982 | ! |
"utilize automatic word-wrapping with newline characters inserted, or ", |
983 | ! |
"turn off automatic wrapping and wordwrap all contents manually by inserting ", |
984 | ! |
"newlines." |
985 |
) |
|
986 | ! |
warning(paste0(msg, collapse = "")) |
987 |
} |
|
988 |
} |
|
989 | ||
990 |
#' Wrap a string to a precise width |
|
991 |
#' |
|
992 |
#' Core wrapping functionality that preserves whitespace. Newline character `"\n"` is not supported |
|
993 |
#' by core functionality [stringi::stri_wrap()]. This is usually solved beforehand by [matrix_form()]. |
|
994 |
#' If the width is smaller than any large word, these will be truncated after `width` characters. If |
|
995 |
#' the split leaves trailing groups of empty spaces, they will be dropped. |
|
996 |
#' |
|
997 |
#' @inheritParams open_font_dev |
|
998 |
#' @param str (`string`, `character`, or `list`)\cr string to be wrapped. If it is a `vector` or |
|
999 |
#' a `list`, it will be looped as a `list` and returned with `unlist(use.names = FALSE)`. |
|
1000 |
#' @param width (`numeric(1)`)\cr width, in characters, that the text should be wrapped to. |
|
1001 |
#' @param collapse (`string` or `NULL`)\cr collapse character used to separate segments of words that |
|
1002 |
#' have been split and should be pasted together. This is usually done internally with `"\n"` to update |
|
1003 |
#' the wrapping along with other internal values. |
|
1004 |
#' |
|
1005 |
#' @details Word wrapping happens similarly to [stringi::stri_wrap()] with the following difference: individual |
|
1006 |
#' words which are longer than `max_width` are broken up in a way that fits with other word wrapping. |
|
1007 |
#' |
|
1008 |
#' @return A string if `str` is one element and if `collapse = NULL`. Otherwise, a list of elements |
|
1009 |
#' (if `length(str) > 1`) that can contain strings or vectors of characters (if `collapse = NULL`). |
|
1010 |
#' |
|
1011 |
#' @examples |
|
1012 |
#' str <- list( |
|
1013 |
#' " , something really \\tnot very good", # \t needs to be escaped |
|
1014 |
#' " but I keep it12 " |
|
1015 |
#' ) |
|
1016 |
#' wrap_string(str, 5, collapse = "\n") |
|
1017 |
#' |
|
1018 |
#' @export |
|
1019 |
wrap_string <- function(str, width, collapse = NULL, fontspec = font_spec()) { |
|
1020 | 36446x |
if (length(str) > 1) { |
1021 | 114x |
return( |
1022 | 114x |
unlist( |
1023 | 114x |
lapply(str, wrap_string, width = width, collapse = collapse, fontspec = fontspec), |
1024 | 114x |
use.names = FALSE |
1025 |
) |
|
1026 |
) |
|
1027 |
} |
|
1028 | 36332x |
str <- unlist(str, use.names = FALSE) # it happens is one list element |
1029 | 36332x |
if (!length(str) || !nzchar(str) || is.na(str)) { |
1030 | 3855x |
return(str) |
1031 |
} |
|
1032 | 32477x |
checkmate::assert_character(str) |
1033 | 32477x |
checkmate::assert_int(width, lower = 1) |
1034 | ||
1035 | 32477x |
if (any(grepl("\\n", str))) { |
1036 | ! |
stop( |
1037 | ! |
"Found \\n in a string that was meant to be wrapped. This should not happen ", |
1038 | ! |
"because matrix_form should take care of them before this step (toString, ", |
1039 | ! |
"i.e. the printing machinery). Please contact the maintaner or file an issue." |
1040 |
) |
|
1041 |
} |
|
1042 | ||
1043 | 32477x |
if (!is_monospace(fontspec)) { |
1044 | 3x |
return(wrap_string_ttype(str, width, fontspec, collapse = collapse)) |
1045 |
} |
|
1046 | ||
1047 |
# str can be also a vector or list. In this case simplify manages the output |
|
1048 | 32474x |
ret <- .go_stri_wrap(str, width) |
1049 | ||
1050 |
# Check if it went fine |
|
1051 | 32474x |
if (any(nchar_ttype(ret, fontspec) > width)) { |
1052 | 68x |
which_exceeded <- which(nchar_ttype(ret, fontspec) > width) |
1053 | ||
1054 |
# Recursive for loop to take word interval |
|
1055 | 68x |
while (length(which_exceeded) > 0) { |
1056 | 75x |
we_i <- which_exceeded[1] |
1057 |
# Is there space for some part of the next word? |
|
1058 | 75x |
char_threshold <- width * (2 / 3) + 0.01 # if too little space -> no previous word |
1059 | 75x |
smart_condition <- nchar_ttype(ret[we_i - 1], fontspec) + 1 < char_threshold # +1 is for spaces |
1060 | 75x |
if (we_i - 1 > 0 && smart_condition) { |
1061 | 6x |
we_interval <- unique(c(we_i - 1, we_i)) |
1062 | 6x |
we_interval <- we_interval[ |
1063 | 6x |
(we_interval < (length(ret) + 1)) & |
1064 | 6x |
(we_interval > 0) |
1065 |
] |
|
1066 |
} else { |
|
1067 | 69x |
we_interval <- we_i |
1068 |
} |
|
1069 |
# Split words and collapse (needs unique afterwards) |
|
1070 | 75x |
ret[we_interval] <- split_words_by( |
1071 | 75x |
paste0(ret[we_interval], collapse = " "), |
1072 | 75x |
width |
1073 |
) |
|
1074 |
# Taking out repetitions if there are more than one |
|
1075 | 75x |
if (length(we_interval) > 1) { |
1076 | 6x |
ret <- ret[-we_interval[-1]] |
1077 | 6x |
we_interval <- we_interval[1] |
1078 |
} |
|
1079 |
# Paste together and rerun if it is not the same as original ret |
|
1080 | 75x |
ret_collapse <- paste0(ret, collapse = " ") |
1081 | ||
1082 |
# Checking if we are stuck in a loop |
|
1083 | 75x |
ori_wrapped_txt_v <- .go_stri_wrap(str, width) |
1084 | 75x |
cur_wrapped_txt_v <- .go_stri_wrap(ret_collapse, width) |
1085 | 75x |
broken_char_ori <- sum(nchar_ttype(ori_wrapped_txt_v, fontspec) > width) # how many issues there were |
1086 | 75x |
broken_char_cur <- sum(nchar_ttype(cur_wrapped_txt_v, fontspec) > width) # how many issues there are |
1087 | ||
1088 |
# if still broken, we did not solve the current issue! |
|
1089 | 75x |
if (setequal(ori_wrapped_txt_v, cur_wrapped_txt_v) || broken_char_cur >= broken_char_ori) { |
1090 |
# help function: Very rare case where the recursion is stuck in a loop |
|
1091 | 14x |
ret_tmp <- force_split_words_by(ret[we_interval], width) # here we_interval is only one ind |
1092 | 14x |
ret <- append(ret, ret_tmp, we_interval)[-we_interval] |
1093 | 14x |
which_exceeded <- which(nchar_ttype(ret, fontspec) > width) |
1094 |
} else { |
|
1095 | 61x |
return(wrap_string(str = ret_collapse, width = width, collapse = collapse)) |
1096 |
} |
|
1097 |
} |
|
1098 |
} |
|
1099 | ||
1100 | 32413x |
if (!is.null(collapse)) { |
1101 | 31962x |
return(paste0(ret, collapse = collapse)) |
1102 |
} |
|
1103 | ||
1104 | 451x |
return(ret) |
1105 |
} |
|
1106 | ||
1107 |
.go_stri_wrap <- function(str, w) { |
|
1108 | 32624x |
if (w < 1) { |
1109 | ! |
return(str) |
1110 |
} |
|
1111 | 32624x |
stringi::stri_wrap(str, |
1112 | 32624x |
width = w, |
1113 | 32624x |
normalize = FALSE, # keeps spaces |
1114 | 32624x |
simplify = TRUE, # If FALSE makes it a list with str elements |
1115 | 32624x |
indent = 0, |
1116 | 32624x |
use_length = FALSE # incase the defaul changes, use actual char widths |
1117 |
) |
|
1118 |
} |
|
1119 | ||
1120 |
#' @rdname wrap_string_ttype |
|
1121 |
#' @export |
|
1122 |
split_word_ttype <- function(str, width, fontspec, min_ok_chars) { |
|
1123 | 11x |
chrs <- strsplit(str, "")[[1]] |
1124 | 11x |
nctt_chars <- nchar_ttype(chrs, fontspec, raw = TRUE) |
1125 | 11x |
ok <- which(cumsum(nctt_chars) <= width) |
1126 | 11x |
if (length(ok) < min_ok_chars || length(chrs) - length(ok) < min_ok_chars) { |
1127 | 3x |
list( |
1128 | 3x |
ok = character(), |
1129 | 3x |
remainder = str |
1130 |
) |
|
1131 |
} else { |
|
1132 | 8x |
list( |
1133 | 8x |
ok = substr(str, 1, length(ok)), |
1134 | 8x |
remainder = substr(str, length(ok) + 1, nchar(str)) |
1135 |
) |
|
1136 |
} |
|
1137 |
} |
|
1138 | ||
1139 |
## need a separate path here because **the number of characters** |
|
1140 |
## in each part is no longer going to be constant the way it |
|
1141 |
## was for monospace |
|
1142 |
## this is much slower but still shouldn't be a bottleneck, if it is we'll |
|
1143 |
## have to do something else |
|
1144 |
#' wrap string given a Truetype font |
|
1145 |
#' |
|
1146 |
#' @inheritParams wrap_string |
|
1147 |
#' @param min_ok_chars (`numeric(1)`)\cr number of minimum characters that remain |
|
1148 |
#' on either side when a word is split. |
|
1149 |
#' @param wordbreak_ok (`logical(1)`)\cr should breaking within a word be allowed? If, `FALSE`, |
|
1150 |
#' attempts to wrap a string to a width narrower than its widest word will result |
|
1151 |
#' in an error. |
|
1152 |
#' @return `str`, broken up into a word-wrapped vector |
|
1153 |
#' @export |
|
1154 |
wrap_string_ttype <- function(str, |
|
1155 |
width, |
|
1156 |
fontspec, |
|
1157 |
collapse = NULL, |
|
1158 |
min_ok_chars = min(floor(nchar(str) / 2), 4, floor(width / 2)), |
|
1159 |
wordbreak_ok = TRUE) { |
|
1160 | 12x |
newdev <- open_font_dev(fontspec) |
1161 | 11x |
if (newdev) { |
1162 | ! |
on.exit(close_font_dev()) |
1163 |
} |
|
1164 | ||
1165 | 11x |
rawspls <- strsplit(str, "[[:space:]](?=[^[:space:]])", perl = TRUE)[[1]] # preserve all but one space |
1166 | 11x |
nctt <- nchar_ttype(rawspls, fontspec, raw = TRUE) |
1167 | 11x |
pts <- which(cumsum(nctt) <= width) |
1168 | 11x |
if (length(pts) == length(rawspls)) { ## no splitting needed |
1169 | 3x |
return(str) |
1170 | 8x |
} else if (length(pts) == 0) { ## no spaces, all one word, split it and keep going |
1171 | 7x |
if (wordbreak_ok) { |
1172 | 7x |
inner_res <- list() |
1173 | 7x |
min_ok_inner <- min_ok_chars |
1174 | 7x |
while (length(inner_res$ok) == 0) { |
1175 | 10x |
inner_res <- split_word_ttype(rawspls[1], width, fontspec, min_ok_inner) # min_ok_chars) |
1176 | 10x |
min_ok_inner <- floor(min_ok_inner / 2) |
1177 |
} |
|
1178 | 7x |
done <- inner_res$ok |
1179 | 7x |
remainder <- paste(c(inner_res$remainder, rawspls[-1]), collapse = " ") |
1180 |
} else { |
|
1181 | ! |
stop( |
1182 | ! |
"Unable to find word wrapping solution without breaking word: ", |
1183 | ! |
rawspls[[1]], " [requires ", nchar_ttype(rawspls[[1]], fontspec), " spaces of width, out of ", |
1184 | ! |
width, " available]." |
1185 |
) |
|
1186 |
} |
|
1187 |
} else { ## some words fit, and some words don't |
|
1188 | 1x |
done_tmp <- paste(rawspls[pts], collapse = " ") |
1189 | 1x |
tospl_tmp <- rawspls[length(pts) + 1] |
1190 | 1x |
width_tmp <- width - sum(nctt[pts]) |
1191 | 1x |
if (wordbreak_ok && width_tmp / width > .33) { |
1192 | 1x |
inner_res <- split_word_ttype(tospl_tmp, width_tmp, fontspec, |
1193 | 1x |
min_ok_chars = min_ok_chars |
1194 |
) |
|
1195 |
} else { |
|
1196 | ! |
inner_res <- list(done = "", remainder = tospl_tmp) |
1197 |
} |
|
1198 | 1x |
done <- paste(c(rawspls[pts], inner_res$ok), |
1199 | 1x |
collapse = " " |
1200 |
) |
|
1201 | 1x |
remainder <- paste( |
1202 | 1x |
c( |
1203 | 1x |
inner_res$remainder, |
1204 | 1x |
if (length(rawspls) > length(pts) + 1) tail(rawspls, -(length(pts) + 1)) |
1205 |
), |
|
1206 | 1x |
collapse = " " |
1207 |
) |
|
1208 |
} |
|
1209 | 8x |
ret <- c( |
1210 | 8x |
done, |
1211 | 8x |
wrap_string_ttype(remainder, width, fontspec) |
1212 |
) |
|
1213 | 8x |
if (!is.null(collapse)) { |
1214 | ! |
ret <- paste(ret, collapse = collapse) |
1215 |
} |
|
1216 | 8x |
ret |
1217 |
} |
|
1218 | ||
1219 |
# help function: Very rare case where the recursion is stuck in a loop |
|
1220 |
force_split_words_by <- function(ret, width) { |
|
1221 | 14x |
which_exceeded <- which(nchar(ret) > width) |
1222 | 14x |
ret_tmp <- NULL |
1223 | 14x |
for (ii in seq_along(ret)) { |
1224 | 14x |
if (ii %in% which_exceeded) { |
1225 | 14x |
wrd_i <- ret[ii] |
1226 | 14x |
init_v <- seq(1, nchar(wrd_i), by = width) |
1227 | 14x |
end_v <- c(init_v[-1] - 1, nchar(wrd_i)) |
1228 | 14x |
str_v_tmp <- stringi::stri_sub(wrd_i, from = init_v, to = end_v) |
1229 | 14x |
ret_tmp <- c(ret_tmp, str_v_tmp[!grepl("^\\s+$", str_v_tmp) & nzchar(str_v_tmp)]) |
1230 |
} else { |
|
1231 | ! |
ret_tmp <- c(ret_tmp, ret[ii]) |
1232 |
} |
|
1233 |
} |
|
1234 | 14x |
ret_tmp |
1235 |
} |
|
1236 | ||
1237 |
# Helper fnc to split the words and collapse them with space |
|
1238 |
split_words_by <- function(wrd, width) { |
|
1239 | 75x |
vapply(wrd, function(wrd_i) { |
1240 | 75x |
init_v <- seq(1, nchar(wrd_i), by = width) |
1241 | 75x |
end_v <- c(init_v[-1] - 1, nchar(wrd_i)) |
1242 | 75x |
fin_str_v <- substring(wrd_i, init_v, end_v) |
1243 | 75x |
is_only_spaces <- grepl("^\\s+$", fin_str_v) |
1244 |
# We pop only spaces at this point |
|
1245 | 75x |
if (all(is_only_spaces)) { |
1246 | ! |
fin_str_v <- fin_str_v[1] # keep only one width-sized empty |
1247 |
} else { |
|
1248 | 75x |
fin_str_v <- fin_str_v[!is_only_spaces] # hybrid text + \s |
1249 |
} |
|
1250 | ||
1251 |
# Collapse the string |
|
1252 | 75x |
paste0(fin_str_v, collapse = " ") |
1253 | 75x |
}, character(1), USE.NAMES = FALSE) |
1254 |
} |
|
1255 | ||
1256 |
#' @describeIn wrap_string Deprecated function. Please use [wrap_string()] instead. |
|
1257 |
#' |
|
1258 |
#' @examples |
|
1259 |
#' wrap_txt(str, 5, collapse = NULL) |
|
1260 |
#' |
|
1261 |
#' @export |
|
1262 |
wrap_txt <- function(str, width, collapse = NULL, fontspec = font_spec()) { |
|
1263 | 396x |
new_dev <- open_font_dev(fontspec) |
1264 | 396x |
if (new_dev) { |
1265 | 2x |
on.exit(close_font_dev()) |
1266 |
} |
|
1267 | ||
1268 | 396x |
unlist(wrap_string(str, width, collapse, fontspec = fontspec), use.names = FALSE) |
1269 |
} |
|
1270 | ||
1271 |
pad_vert_top <- function(x, len, default = "") { |
|
1272 | 5510x |
c(x, rep(default, len - length(x))) |
1273 |
} |
|
1274 | ||
1275 |
pad_vert_bottom <- function(x, len, default = "") { |
|
1276 | 326x |
c(rep(default, len - length(x)), x) |
1277 |
} |
|
1278 | ||
1279 |
pad_vec_to_len <- function(vec, len, cpadder = pad_vert_top, rlpadder = cpadder) { |
|
1280 | 711x |
dat <- unlist(lapply(vec[-1], cpadder, len = len)) |
1281 | 711x |
dat <- c(rlpadder(vec[[1]], len = len), dat) |
1282 | 711x |
matrix(dat, nrow = len) |
1283 |
} |
|
1284 | ||
1285 |
rep_vec_to_len <- function(vec, len, ...) { |
|
1286 | 674x |
matrix(unlist(lapply(vec, rep, times = len)), |
1287 | 674x |
nrow = len |
1288 |
) |
|
1289 |
} |
|
1290 | ||
1291 |
safe_strsplit <- function(x, split, ...) { |
|
1292 | 948x |
ret <- strsplit(x, split, ...) |
1293 | 948x |
lapply(ret, function(reti) if (length(reti) == 0) "" else reti) |
1294 |
} |
|
1295 | ||
1296 |
.expand_mat_rows_inner <- function(i, mat, row_nlines, expfun, ...) { |
|
1297 | 1385x |
leni <- row_nlines[i] |
1298 | 1385x |
rw <- mat[i, ] |
1299 | 1385x |
if (is.character(rw)) { |
1300 | 948x |
rw <- safe_strsplit(rw, "\n", fixed = TRUE) |
1301 |
} |
|
1302 | 1385x |
expfun(rw, len = leni, ...) |
1303 |
} |
|
1304 | ||
1305 |
expand_mat_rows <- function(mat, row_nlines = apply(mat, 1, nlines), expfun = pad_vec_to_len, ...) { |
|
1306 | 238x |
rinds <- seq_len(nrow(mat)) |
1307 | 238x |
exprows <- lapply(rinds, .expand_mat_rows_inner, |
1308 | 238x |
mat = mat, |
1309 | 238x |
row_nlines = row_nlines, |
1310 | 238x |
expfun = expfun, |
1311 |
... |
|
1312 |
) |
|
1313 | 238x |
do.call(rbind, exprows) |
1314 |
} |
|
1315 | ||
1316 |
#' Transform a vector of spans (with duplication) into a visibility vector |
|
1317 |
#' |
|
1318 |
#' @param spans (`numeric`)\cr a vector of spans, with each span value repeated |
|
1319 |
#' for the cells it covers. |
|
1320 |
#' |
|
1321 |
#' @details |
|
1322 |
#' The values of `spans` are assumed to be repeated such that each individual position covered by the |
|
1323 |
#' span has the repeated value. |
|
1324 |
#' |
|
1325 |
#' This means that each block of values in `spans` must be of a length at least equal to its value |
|
1326 |
#' (i.e. two 2s, three 3s, etc). |
|
1327 |
#' |
|
1328 |
#' This function correctly handles cases where two spans of the same size are next to each other; |
|
1329 |
#' i.e., a block of four 2s represents two large cells each of which spans two individual cells. |
|
1330 |
#' |
|
1331 |
#' @return A logical vector the same length as `spans` indicating whether the contents of a string vector |
|
1332 |
#' with those spans is valid. |
|
1333 |
#' |
|
1334 |
#' @note |
|
1335 |
#' Currently no checking or enforcement is done to verify that the vector of spans is valid according to |
|
1336 |
#' the specifications described in the Details section above. |
|
1337 |
#' |
|
1338 |
#' @examples |
|
1339 |
#' spans_to_viscell(c(2, 2, 2, 2, 1, 3, 3, 3)) |
|
1340 |
#' |
|
1341 |
#' @export |
|
1342 |
spans_to_viscell <- function(spans) { |
|
1343 | 2x |
if (!is.vector(spans)) { |
1344 | ! |
spans <- as.vector(spans) |
1345 |
} |
|
1346 | 2x |
myrle <- rle(spans) |
1347 | 2x |
unlist( |
1348 | 2x |
mapply( |
1349 | 2x |
function(vl, ln) { |
1350 | 4x |
rep(c(TRUE, rep(FALSE, vl - 1L)), times = ln / vl) |
1351 |
}, |
|
1352 | 2x |
SIMPLIFY = FALSE, |
1353 | 2x |
vl = myrle$values, |
1354 | 2x |
ln = myrle$lengths |
1355 |
), |
|
1356 | 2x |
recursive = FALSE |
1357 |
) |
|
1358 |
} |
|
1359 | ||
1360 |
#' Propose column widths based on the `MatrixPrintForm` of an object |
|
1361 |
#' |
|
1362 |
#' Row names are also considered a column for the output. |
|
1363 |
#' |
|
1364 |
#' @inheritParams open_font_dev |
|
1365 |
#' @param x (`ANY`)\cr a `MatrixPrintForm` object, or an object with a `matrix_form` method. |
|
1366 |
#' @param indent_size (`numeric(1)`)\cr indent size, in characters. Ignored when `x` is already |
|
1367 |
#' a `MatrixPrintForm` object in favor of information there. |
|
1368 |
#' |
|
1369 |
#' @return A vector of column widths based on the content of `x` for use in printing and pagination. |
|
1370 |
#' |
|
1371 |
#' @examples |
|
1372 |
#' mf <- basic_matrix_form(mtcars) |
|
1373 |
#' propose_column_widths(mf) |
|
1374 |
#' |
|
1375 |
#' @export |
|
1376 |
propose_column_widths <- function(x, |
|
1377 |
indent_size = 2, |
|
1378 |
fontspec = font_spec()) { |
|
1379 | 92x |
new_dev <- open_font_dev(fontspec) |
1380 | 92x |
if (new_dev) { |
1381 | 62x |
on.exit(close_font_dev()) |
1382 |
} |
|
1383 | ||
1384 | 92x |
if (!is(x, "MatrixPrintForm")) { |
1385 | ! |
x <- matrix_form(x, indent_rownames = TRUE, indent_size = indent_size, fontspec = fontspec) |
1386 |
} |
|
1387 | 92x |
body <- mf_strings(x) |
1388 | 92x |
spans <- mf_spans(x) |
1389 | 92x |
aligns <- mf_aligns(x) |
1390 | 92x |
display <- mf_display(x) |
1391 | ||
1392 |
# compute decimal alignment if asked in alignment matrix |
|
1393 | 92x |
if (any_dec_align(aligns)) { |
1394 | 27x |
body <- decimal_align(body, aligns) |
1395 |
} |
|
1396 | ||
1397 |
## chars <- nchar(body) #old monospace assumption |
|
1398 |
## we now use widths in terms of the printwidth of the space (" ") |
|
1399 |
## character. This collapses to the same thing in the monospace |
|
1400 |
## case but allows us to reasonably support truetype fonts |
|
1401 | 89x |
chars <- nchar_ttype(body, fontspec) |
1402 | ||
1403 |
# first check column widths without colspan |
|
1404 | 89x |
has_spans <- spans != 1 |
1405 | 89x |
chars_ns <- chars |
1406 | 89x |
chars_ns[has_spans] <- 0 |
1407 | 89x |
widths <- apply(chars_ns, 2, max) |
1408 | ||
1409 |
# now check if the colspans require extra width |
|
1410 | 89x |
if (any(has_spans)) { |
1411 | 1x |
has_row_spans <- apply(has_spans, 1, any) |
1412 | ||
1413 | 1x |
chars_sp <- chars[has_row_spans, , drop = FALSE] |
1414 | 1x |
spans_sp <- spans[has_row_spans, , drop = FALSE] |
1415 | 1x |
disp_sp <- display[has_row_spans, , drop = FALSE] |
1416 | ||
1417 | 1x |
nc <- ncol(spans) |
1418 | 1x |
for (i in seq_len(nrow(chars_sp))) { |
1419 | 1x |
for (j in seq_len(nc)) { |
1420 | 2x |
if (disp_sp[i, j] && spans_sp[i, j] != 1) { |
1421 | 1x |
i_cols <- seq(j, j + spans_sp[i, j] - 1) |
1422 | ||
1423 | 1x |
nchar_i <- chars_sp[i, j] |
1424 | 1x |
cw_i <- widths[i_cols] |
1425 | 1x |
available_width <- sum(cw_i) |
1426 | ||
1427 | 1x |
if (nchar_i > available_width) { |
1428 |
# need to update widths to fit content with colspans |
|
1429 |
# spread width among columns |
|
1430 | ! |
widths[i_cols] <- cw_i + spread_integer(nchar_i - available_width, length(cw_i)) |
1431 |
} |
|
1432 |
} |
|
1433 |
} |
|
1434 |
} |
|
1435 |
} |
|
1436 | 89x |
widths |
1437 |
} |
|
1438 | ||
1439 |
## "number of characters" width in terms of |
|
1440 |
## width of " " for the chosen font family |
|
1441 | ||
1442 |
## pdf device with font specification MUST already be open |
|
1443 | ||
1444 |
#' Calculate font-specific string width |
|
1445 |
#' |
|
1446 |
#' This function returns the width of each element `x` |
|
1447 |
#' *as a multiple of the width of the space character |
|
1448 |
#' for in declared font*, rounded up to the nearest |
|
1449 |
#' integer. This is used extensively in the text rendering |
|
1450 |
#' ([toString()]) and pagination machinery for |
|
1451 |
#' calculating word wrapping, default column widths, |
|
1452 |
#' lines per page, etc. |
|
1453 |
#' |
|
1454 |
#' @param x (`character`)\cr the string(s) to calculate width(s) for. |
|
1455 |
#' @param fontspec (`font_spec` or `NULL`)\cr if non-NULL, the font to use for |
|
1456 |
#' the calculations (as returned by [font_spec()]). Defaults to "Courier", |
|
1457 |
#' which is a monospace font. If NULL, the width will be returned |
|
1458 |
#' in number of characters by calling `nchar` directly. |
|
1459 |
#' @param tol (`numeric(1)`)\cr the tolerance to use when determining |
|
1460 |
#' if a multiple needs to be rounded up to the next integer. See |
|
1461 |
#' Details. |
|
1462 |
#' @param raw (`logical(1)`)\cr whether unrounded widths should be returned. Defaults to `FALSE`. |
|
1463 |
#' |
|
1464 |
#' @details String width is defined in terms of spaces within |
|
1465 |
#' the specified font. For monospace fonts, this definition |
|
1466 |
#' collapses to the number of characters in the string |
|
1467 |
#' ([nchar()]), but for truetype fonts it does not. |
|
1468 |
#' |
|
1469 |
#' For `raw = FALSE`, non-integer values (the norm in a truetype |
|
1470 |
#' setting) for the number of spaces a string takes up is rounded |
|
1471 |
#' up, *unless the multiple is less than `tol` above the last integer |
|
1472 |
#' before it*. E.g., if `k - num_spaces < tol` for an integer |
|
1473 |
#' `k`, `k` is returned instead of `k+1`. |
|
1474 |
#' |
|
1475 |
#' @seealso [font_spec()] |
|
1476 |
#' |
|
1477 |
#' @examples |
|
1478 |
#' nchar_ttype("hi there!") |
|
1479 |
#' |
|
1480 |
#' nchar_ttype("hi there!", font_spec("Times")) |
|
1481 |
#' |
|
1482 |
#' @export |
|
1483 |
nchar_ttype <- function(x, fontspec = font_spec(), tol = sqrt(.Machine$double.eps), raw = FALSE) { |
|
1484 |
## escape hatch because sometimes we need to call, e.g. make_row_df |
|
1485 |
## but we dont' care about getting the word wrapping right and the |
|
1486 |
## performance penalty was KILLING us. Looking at you |
|
1487 |
## rtables::update_ref_indexing @.@ |
|
1488 | 48657x |
if (is.null(fontspec)) { |
1489 | 1x |
return(nchar(x)) |
1490 |
} |
|
1491 | 48656x |
new_dev <- open_font_dev(fontspec) |
1492 | 48656x |
if (new_dev) { |
1493 | 149x |
on.exit(close_font_dev()) |
1494 |
} |
|
1495 | 48656x |
if (font_dev_state$ismonospace) { ## WAY faster if we can do it |
1496 | 48632x |
return(nchar(x)) |
1497 |
} |
|
1498 | 24x |
space_width <- get_space_width() |
1499 |
## cwidth_inches_unsafe is ok here because if we don't |
|
1500 |
## have a successfully opened state (somehow), get_space_width |
|
1501 |
## above will error. |
|
1502 | 24x |
num_inches_raw <- vapply(x, cwidth_inches_unsafe, 1.0) |
1503 | 24x |
num_spaces_raw <- num_inches_raw / space_width |
1504 | 24x |
if (!raw) { |
1505 | 1x |
num_spaces_ceil <- ceiling(num_spaces_raw) |
1506 |
## we don't want to add one when the answer is e.g, 3.0000000000000953 |
|
1507 | 1x |
within_tol <- which(num_spaces_raw + 1 - num_spaces_ceil <= tol) |
1508 | 1x |
ret <- num_spaces_ceil |
1509 | 1x |
if (length(within_tol) == 0L) { |
1510 | 1x |
ret[within_tol] <- floor(num_spaces_raw[within_tol]) |
1511 |
} |
|
1512 |
} else { |
|
1513 | 23x |
ret <- num_spaces_raw |
1514 |
} |
|
1515 | 24x |
if (!is.null(dim(x))) { |
1516 | ! |
dim(ret) <- dim(x) |
1517 |
} else { |
|
1518 | 24x |
names(ret) <- NULL |
1519 |
} |
|
1520 | 24x |
ret |
1521 |
} |
|
1522 | ||
1523 |
#' Pad a string and align within string |
|
1524 |
#' |
|
1525 |
#' @inheritParams open_font_dev |
|
1526 |
#' @param x (`string`)\cr a string. |
|
1527 |
#' @param n (`integer(1)`)\cr number of characters in the output string. If `n < nchar(x)`, an error is thrown. |
|
1528 |
#' @param just (`string`)\cr text alignment justification to use. Defaults to `"center"`. Must be one of |
|
1529 |
#' `"center"`, `"right"`, `"left"`, `"dec_right"`, `"dec_left"`, or `"decimal"`. |
|
1530 |
#' |
|
1531 |
#' @return `x`, padded to be a string of length `n`. |
|
1532 |
#' |
|
1533 |
#' @examples |
|
1534 |
#' padstr("abc", 3) |
|
1535 |
#' padstr("abc", 4) |
|
1536 |
#' padstr("abc", 5) |
|
1537 |
#' padstr("abc", 5, "left") |
|
1538 |
#' padstr("abc", 5, "right") |
|
1539 |
#' |
|
1540 |
#' \dontrun{ |
|
1541 |
#' # Expect error: "abc" has more than 1 characters |
|
1542 |
#' padstr("abc", 1) |
|
1543 |
#' } |
|
1544 |
#' |
|
1545 |
#' @export |
|
1546 |
padstr <- function(x, n, just = list_valid_aligns(), fontspec = font_spec()) { |
|
1547 | 15607x |
just <- match.arg(just) |
1548 | ||
1549 | 1x |
if (length(x) != 1) stop("length of x needs to be 1 and not", length(x)) |
1550 | 1x |
if (is.na(n) || !is.numeric(n) || n < 0) stop("n needs to be numeric and > 0") |
1551 | ||
1552 | 2x |
if (is.na(x)) x <- "<NA>" |
1553 | ||
1554 | 15605x |
nc <- nchar_ttype(x, fontspec) |
1555 | ! |
if (n < nc) stop("\"", x, "\" has more than ", n, " characters") |
1556 | ||
1557 | 15605x |
switch(just, |
1558 |
center = { |
|
1559 | 13706x |
pad <- (n - nc) / 2 |
1560 | 13706x |
paste0(spaces(floor(pad)), x, spaces(ceiling(pad))) |
1561 |
}, |
|
1562 | 1748x |
left = paste0(x, spaces(n - nc)), |
1563 | 10x |
right = paste0(spaces(n - nc), x), |
1564 |
decimal = { |
|
1565 | 61x |
pad <- (n - nc) / 2 |
1566 | 61x |
paste0(spaces(floor(pad)), x, spaces(ceiling(pad))) |
1567 |
}, |
|
1568 | 45x |
dec_left = paste0(x, spaces(n - nc)), |
1569 | 35x |
dec_right = paste0(spaces(n - nc), x) |
1570 |
) |
|
1571 |
} |
|
1572 | ||
1573 |
spaces <- function(n) { |
|
1574 | 29530x |
strrep(" ", n) |
1575 |
} |
|
1576 | ||
1577 |
.paste_no_na <- function(x, ...) { |
|
1578 | 2394x |
paste(na.omit(x), ...) |
1579 |
} |
|
1580 | ||
1581 |
#' Spread an integer to a given length |
|
1582 |
#' |
|
1583 |
#' @param x (`integer(1)`)\cr number to spread. |
|
1584 |
#' @param len (`integer(1)`)\cr number of times to repeat `x`. |
|
1585 |
#' |
|
1586 |
#' @return If `x` is a scalar whole number value (see [is.wholenumber()]), the value `x` is repeated `len` times. |
|
1587 |
#' Otherwise, an error is thrown. |
|
1588 |
#' |
|
1589 |
#' @examples |
|
1590 |
#' spread_integer(3, 1) |
|
1591 |
#' spread_integer(0, 3) |
|
1592 |
#' spread_integer(1, 3) |
|
1593 |
#' spread_integer(2, 3) |
|
1594 |
#' spread_integer(3, 3) |
|
1595 |
#' spread_integer(4, 3) |
|
1596 |
#' spread_integer(5, 3) |
|
1597 |
#' spread_integer(6, 3) |
|
1598 |
#' spread_integer(7, 3) |
|
1599 |
#' |
|
1600 |
#' @export |
|
1601 |
spread_integer <- function(x, len) { |
|
1602 | 2x |
stopifnot( |
1603 | 2x |
is.wholenumber(x), length(x) == 1, x >= 0, |
1604 | 2x |
is.wholenumber(len), length(len) == 1, len >= 0, |
1605 | 2x |
!(len == 0 && x > 0) |
1606 |
) |
|
1607 | ||
1608 | 1x |
if (len == 0) { |
1609 | ! |
integer(0) |
1610 |
} else { |
|
1611 | 1x |
y <- rep(floor(x / len), len) |
1612 | 1x |
i <- 1 |
1613 | 1x |
while (sum(y) < x) { |
1614 | 1x |
y[i] <- y[i] + 1 |
1615 | 1x |
if (i == len) { |
1616 | ! |
i <- 1 |
1617 |
} else { |
|
1618 | 1x |
i <- i + 1 |
1619 |
} |
|
1620 |
} |
|
1621 | 1x |
y |
1622 |
} |
|
1623 |
} |
|
1624 | ||
1625 |
#' Check if a value is a whole number |
|
1626 |
#' |
|
1627 |
#' @param x (`numeric(1)`)\cr a numeric value. |
|
1628 |
#' @param tol (`numeric(1)`)\cr a precision tolerance. |
|
1629 |
#' |
|
1630 |
#' @return `TRUE` if `x` is within `tol` of zero, `FALSE` otherwise. |
|
1631 |
#' |
|
1632 |
#' @examples |
|
1633 |
#' is.wholenumber(5) |
|
1634 |
#' is.wholenumber(5.00000000000000001) |
|
1635 |
#' is.wholenumber(.5) |
|
1636 |
#' |
|
1637 |
#' @export |
|
1638 |
is.wholenumber <- function(x, tol = .Machine$double.eps^0.5) { |
|
1639 | 3x |
abs(x - round(x)) < tol |
1640 |
} |
1 |
## #' Page Dimensions |
|
2 |
## #' |
|
3 |
## #' Dimensions for mapping page dimensions to text dimensions |
|
4 |
## #' @references https://www.ietf.org/rfc/rfc0678.txt |
|
5 |
## #' @export |
|
6 |
## #' @rdname pagedims |
|
7 |
## lpi_vert <- 6 |
|
8 |
## #' @export |
|
9 |
## #' @rdname pagedims |
|
10 |
## cpi_horiz <- 10 |
|
11 |
## #' @export |
|
12 |
## #' @rdname pagedims |
|
13 |
## horiz_margin_chars <- 13 |
|
14 |
## #' @export |
|
15 |
## #' @rdname pagedims |
|
16 |
## horiz_margin_inches <- horiz_margin_chars / cpi_horiz |
|
17 |
## #' @export |
|
18 |
## #' @rdname pagedims |
|
19 |
## vert_margin_lines <- 6 |
|
20 |
## #' @export |
|
21 |
## #' @rdname pagedims |
|
22 |
## vert_margin_inches <- vert_margin_lines / lpi_vert |
|
23 | ||
24 |
## #' Physical Page dimensions to chars x lines |
|
25 |
## #' |
|
26 |
## #' Calculate number of lines long and characters wide a page size is, |
|
27 |
## #' after excluding margins |
|
28 |
## #' @export |
|
29 |
## #' @examples |
|
30 |
## #' phys_page_to_lc() |
|
31 |
## phys_page_to_lc <- function(width = 8.5, len = 11, |
|
32 |
## h_margin = horiz_margin_inches, |
|
33 |
## v_margin = vert_margin_inches) { |
|
34 |
## lgl_width <- width - h_margin |
|
35 |
## lgl_len <- len - v_margin |
|
36 |
## c(chars_wide = floor(lgl_width * cpi_horiz), |
|
37 |
## lines_long = floor(lgl_len * lpi_vert)) |
|
38 |
## } |
|
39 | ||
40 |
#' Pagination |
|
41 |
#' |
|
42 |
#' @section Pagination Algorithm: |
|
43 |
#' |
|
44 |
#' Pagination is performed independently in the vertical and horizontal |
|
45 |
#' directions based solely on a *pagination data frame*, which includes the |
|
46 |
#' following information for each row/column: |
|
47 |
#' |
|
48 |
#' - Number of lines/characters rendering the row will take **after |
|
49 |
#' word-wrapping** (`self_extent`) |
|
50 |
#' - The indices (`reprint_inds`) and number of lines (`par_extent`) |
|
51 |
#' of the rows which act as **context** for the row |
|
52 |
#' - The row's number of siblings and position within its siblings |
|
53 |
#' |
|
54 |
#' Given `lpp` (`cpp`) is already adjusted for rendered elements which |
|
55 |
#' are not rows/columns and a data frame of pagination information, |
|
56 |
#' pagination is performed via the following algorithm with `start = 1`. |
|
57 |
#' |
|
58 |
#' Core Pagination Algorithm: |
|
59 |
#' |
|
60 |
#' 1. Initial guess for pagination position is `start + lpp` (`start + cpp`) |
|
61 |
#' 2. While the guess is not a valid pagination position, and `guess > start`, |
|
62 |
#' decrement guess and repeat. |
|
63 |
#' - An error is thrown if all possible pagination positions between |
|
64 |
#' `start` and `start + lpp` (`start + cpp`) would be `< start` |
|
65 |
#' after decrementing |
|
66 |
#' 3. Retain pagination index |
|
67 |
#' 4. If pagination point was less than `NROW(tt)` (`ncol(tt)`), set |
|
68 |
#' `start` to `pos + 1`, and repeat steps (1) - (4). |
|
69 |
#' |
|
70 |
#' Validating Pagination Position: |
|
71 |
#' |
|
72 |
#' Given an (already adjusted) `lpp` or `cpp` value, a pagination is invalid if: |
|
73 |
#' |
|
74 |
#' - The rows/columns on the page would take more than (adjusted) `lpp` lines/`cpp` |
|
75 |
#' characters to render **including**: |
|
76 |
#' - word-wrapping |
|
77 |
#' - (vertical only) context repetition |
|
78 |
#' - (vertical only) footnote messages and/or section divider lines |
|
79 |
#' take up too many lines after rendering rows |
|
80 |
#' - (vertical only) row is a label or content (row-group summary) row |
|
81 |
#' - (vertical only) row at the pagination point has siblings, and |
|
82 |
#' it has less than `min_siblings` preceding or following siblings |
|
83 |
#' - pagination would occur within a sub-table listed in `nosplitin` |
|
84 |
#' |
|
85 |
#' @name pagination_algo |
|
86 |
NULL |
|
87 | ||
88 |
#' Create a row of a pagination data frame |
|
89 |
#' |
|
90 |
#' @inheritParams open_font_dev |
|
91 |
#' @param nm (`string`)\cr name. |
|
92 |
#' @param lab (`string`)\cr label. |
|
93 |
#' @param rnum (`numeric(1)`)\cr absolute row number. |
|
94 |
#' @param pth (`character` or `NULL`)\cr path within larger table. |
|
95 |
#' @param sibpos (`integer(1)`)\cr position among sibling rows. |
|
96 |
#' @param nsibs (`integer(1)`)\cr number of siblings (including self). |
|
97 |
#' @param extent (`numeric(1)`)\cr number of lines required to print the row. |
|
98 |
#' @param colwidths (`numeric`)\cr column widths. |
|
99 |
#' @param repext (`integer(1)`)\cr number of lines required to reprint all context for this row if it appears directly |
|
100 |
#' after pagination. |
|
101 |
#' @param repind (`integer`)\cr vector of row numbers to be reprinted if this row appears directly after pagination. |
|
102 |
#' @param indent (`integer`)\cr indent. |
|
103 |
#' @param rclass (`string`)\cr class of row object. |
|
104 |
#' @param nrowrefs (`integer(1)`)\cr number of row referential footnotes for this row. |
|
105 |
#' @param ncellrefs (`integer(1)`)\cr number of cell referential footnotes for the cells in this row. |
|
106 |
#' @param nreflines (`integer(1)`)\cr total number of lines required by all referential footnotes. |
|
107 |
#' @param force_page (`flag`)\cr currently ignored. |
|
108 |
#' @param page_title (`flag`)\cr currently ignored. |
|
109 |
#' @param trailing_sep (`string`)\cr the string to use as a separator below this row during printing. |
|
110 |
#' If `NA_character_`, no separator is used. |
|
111 |
#' @param row (`ANY`)\cr object representing the row, which is used for default values of `nm`, `lab`, |
|
112 |
#' `extent`, and `rclass` if provided. Must have methods for `obj_name`, `obj_label`, and `nlines`, to retrieve |
|
113 |
#' default values of `nm`, `lab`, and `extent`, respectively. |
|
114 |
#' |
|
115 |
#' @return A single row `data.frame` with the appropriate columns for a pagination info data frame. |
|
116 |
#' |
|
117 |
#' @export |
|
118 |
pagdfrow <- function(row, |
|
119 |
nm = obj_name(row), |
|
120 |
lab = obj_label(row), |
|
121 |
rnum, |
|
122 |
pth, |
|
123 |
sibpos = NA_integer_, |
|
124 |
nsibs = NA_integer_, |
|
125 |
extent = nlines(row, colwidths, fontspec = fontspec), |
|
126 |
colwidths = NULL, |
|
127 |
repext = 0L, |
|
128 |
repind = integer(), |
|
129 |
indent = 0L, |
|
130 |
rclass = class(row), |
|
131 |
nrowrefs = 0L, |
|
132 |
ncellrefs = 0L, |
|
133 |
nreflines = 0L, |
|
134 |
# ref_df = .make_ref_df(NULL, NULL), |
|
135 |
force_page = FALSE, |
|
136 |
page_title = NA_character_, |
|
137 |
trailing_sep = NA_character_, |
|
138 |
fontspec) { |
|
139 | 1430x |
data.frame( |
140 | 1430x |
label = lab, |
141 | 1430x |
name = nm, |
142 | 1430x |
abs_rownumber = rnum, |
143 | 1430x |
path = I(list(pth)), |
144 | 1430x |
pos_in_siblings = sibpos, |
145 | 1430x |
n_siblings = nsibs, |
146 | 1430x |
self_extent = extent, |
147 | 1430x |
par_extent = repext, |
148 | 1430x |
reprint_inds = I(rep(list(unlist(repind)), length.out = length(nm))), |
149 | 1430x |
node_class = rclass, |
150 | 1430x |
indent = max(0L, indent), |
151 | 1430x |
nrowrefs = nrowrefs, |
152 | 1430x |
ncellrefs = ncellrefs, |
153 | 1430x |
nreflines = nreflines, |
154 |
# ref_info_df = I(list(ref_df)), |
|
155 | 1430x |
force_page = force_page, |
156 | 1430x |
page_title = page_title, |
157 | 1430x |
trailing_sep = trailing_sep, |
158 | 1430x |
stringsAsFactors = FALSE, |
159 | 1430x |
row.names = NULL, |
160 | 1430x |
check.names = FALSE, |
161 | 1430x |
fix.empty.names = FALSE |
162 |
) |
|
163 |
} |
|
164 | ||
165 |
calc_ref_nlines_df <- function(pagdf) { |
|
166 |
## XXX XXX XXX this is dangerous and wrong!!! |
|
167 | 628x |
if (is.null(pagdf$ref_info_df) && sum(pagdf$nreflines) == 0) { |
168 | 221x |
return(ref_df_row()[0, ]) |
169 |
} |
|
170 | 407x |
refdf <- do.call(rbind.data.frame, pagdf$ref_info_df) |
171 | 407x |
if (NROW(refdf) == 0) { |
172 | 375x |
return(ref_df_row()[0, ]) |
173 |
} |
|
174 | 32x |
unqsyms <- !duplicated(refdf$symbol) |
175 | 32x |
refdf[unqsyms, , drop = FALSE] |
176 |
} |
|
177 | ||
178 |
build_fail_msg <- function(row, lines, raw_rowlines, |
|
179 |
allowed_lines, lpp, decoration_lines, |
|
180 |
start, guess, rep_ext, n_reprint, |
|
181 |
reflines, n_refs, sectlines) { |
|
182 | 254x |
if (row) { |
183 | 104x |
spacetype <- "lines" |
184 | 104x |
spacetype_abr <- "lns" |
185 | 104x |
structtype_abr <- "rws" |
186 | 104x |
sprintf( |
187 | 104x |
paste0( |
188 | 104x |
" FAIL: rows selected for pagination require %d %s while only %d are available from ", |
189 | 104x |
"lpp = %d and %d header/footers lines.\n", |
190 | 104x |
" details: [raw: %d %s (%d %s), rep. context: %d %s (%d %s), ", |
191 | 104x |
"refs: %d %s (%d) sect. divs: %d %s]." |
192 |
), |
|
193 | 104x |
lines, |
194 | 104x |
spacetype, |
195 | 104x |
allowed_lines, |
196 | 104x |
lpp, |
197 | 104x |
decoration_lines, # header + footers |
198 | 104x |
raw_rowlines, |
199 | 104x |
spacetype_abr, |
200 | 104x |
guess - start + 1, # because it includes both start and guess |
201 | 104x |
structtype_abr, |
202 | 104x |
rep_ext, |
203 | 104x |
spacetype_abr, |
204 | 104x |
n_reprint, |
205 | 104x |
structtype_abr, |
206 | 104x |
reflines, |
207 | 104x |
spacetype_abr, |
208 | 104x |
n_refs, |
209 | 104x |
sectlines, |
210 | 104x |
spacetype_abr |
211 |
) |
|
212 |
} else { ## !row |
|
213 | 150x |
spacetype <- "chars" |
214 | 150x |
spacetype_abr <- "chars" |
215 | 150x |
structtype_abr <- "cols" |
216 | 150x |
raw_ncol <- guess - start + 1 |
217 | 150x |
tot_ncol <- raw_ncol + n_reprint |
218 | 150x |
rep_ext <- rep_ext |
219 | 150x |
sprintf( |
220 | 150x |
paste0( |
221 | 150x |
" FAIL: selected %d columns require %d %s, while only %d are available. \n", |
222 | 150x |
" details: [raw: %d %s (%d %s), rep. cols: %d %s (%d %s), tot. colgap: %d %s]." |
223 |
), |
|
224 | 150x |
guess - start + 1, |
225 | 150x |
lines + rep_ext + sectlines, |
226 | 150x |
spacetype, |
227 | 150x |
lpp, |
228 | 150x |
lines, |
229 | 150x |
spacetype_abr, |
230 | 150x |
raw_ncol, |
231 | 150x |
structtype_abr, |
232 | 150x |
rep_ext, |
233 | 150x |
spacetype_abr, |
234 | 150x |
n_reprint, |
235 | 150x |
structtype_abr, |
236 | 150x |
sectlines, |
237 | 150x |
spacetype |
238 |
) |
|
239 |
} |
|
240 |
} |
|
241 | ||
242 |
valid_pag <- function(pagdf, |
|
243 |
guess, |
|
244 |
start, |
|
245 |
rlpp, |
|
246 |
lpp, # for informational purposes only |
|
247 |
context_lpp, # for informational purposes only (headers/footers) |
|
248 |
min_sibs, |
|
249 |
nosplit = NULL, |
|
250 |
div_height = 1L, |
|
251 |
verbose = FALSE, |
|
252 |
row = TRUE, |
|
253 |
have_col_fnotes = FALSE, |
|
254 |
col_gap, |
|
255 |
has_rowlabels) { |
|
256 |
# FALSE output from this function means that another guess is taken till success or failure |
|
257 | 628x |
rw <- pagdf[guess, ] |
258 | ||
259 | 628x |
if (verbose) { |
260 | 382x |
message( |
261 | 382x |
"-> Attempting pagination between ", start, " and ", guess, " ", |
262 | 382x |
paste(ifelse(row, "row", "column")) |
263 |
) |
|
264 |
} |
|
265 | ||
266 |
# Fix for counting the right number of lines when there is wrapping on a keycols |
|
267 | 628x |
if (.is_listing_mf(pagdf) && !is.null(pagdf$self_extent_page_break)) { |
268 | 28x |
pagdf$self_extent[start] <- pagdf$self_extent_page_break[start] |
269 |
} |
|
270 | ||
271 | 628x |
raw_rowlines <- sum(pagdf[start:guess, "self_extent"] - pagdf[start:guess, "nreflines"]) |
272 | ||
273 | 628x |
refdf_ii <- calc_ref_nlines_df(pagdf[start:guess, ]) |
274 | 628x |
reflines <- if (row) sum(refdf_ii$nlines, 0L) else 0L |
275 | 628x |
if (reflines > 0 && !have_col_fnotes) { |
276 | 32x |
reflines <- reflines + div_height + 1L |
277 |
} |
|
278 | ||
279 | ||
280 | 628x |
rowlines <- raw_rowlines + reflines ## sum(pagdf[start:guess, "self_extent"]) - reflines |
281 |
## self extent includes reflines |
|
282 |
## self extent does ***not*** currently include trailing sep for rows |
|
283 |
## self extent does ***not*** currently include col_gap for columns |
|
284 |
## we don't include the trailing_sep for guess because if we paginate here it won't be printed |
|
285 | 628x |
ncols <- 0L |
286 | 628x |
if (row) { |
287 | 257x |
sectlines <- if (start == guess) 0L else sum(!is.na(pagdf[start:(guess - 1), "trailing_sep"])) |
288 |
} else { ## columns |
|
289 | 371x |
ncols <- guess - start + 1 + length(pagdf$reprint_inds[[start]]) ## +1 because its inclusive, 5-6 is 2 columns |
290 | 371x |
sectlines <- col_gap * (ncols - as.integer(!has_rowlabels)) ## -1 if no row labels |
291 |
} |
|
292 | 628x |
lines <- rowlines + sectlines |
293 | 628x |
rep_ext <- pagdf$par_extent[start] |
294 | 628x |
if (lines > rlpp) { |
295 | 382x |
if (verbose) { |
296 | 254x |
structtype <- ifelse(row, "rows", "columns") |
297 | 254x |
structtype_abr <- ifelse(row, "rows", "cols") |
298 | 254x |
spacetype <- ifelse(row, "lines", "chars") |
299 | 254x |
spacetype_abr <- ifelse(row, "lns", "chrs") |
300 | 254x |
msg <- build_fail_msg( |
301 | 254x |
row, lines, raw_rowlines, |
302 | 254x |
allowed_lines = rlpp, lpp = lpp, decoration_lines = context_lpp, |
303 | 254x |
start, guess, rep_ext, length(pagdf$reprint_inds[[start]]), |
304 | 254x |
reflines, NROW(refdf_ii), sectlines |
305 |
) |
|
306 | 254x |
message(msg) |
307 |
} |
|
308 | 382x |
return(FALSE) |
309 |
} |
|
310 | ||
311 |
# Special cases: is it a label or content row? |
|
312 | 246x |
if (rw[["node_class"]] %in% c("LabelRow", "ContentRow")) { |
313 |
# check if it has children; if no children then valid |
|
314 | 7x |
has_children <- rw$abs_rownumber %in% unlist(pagdf$reprint_inds) |
315 | 7x |
if (rw$abs_rownumber == nrow(pagdf)) { |
316 | 1x |
if (verbose) { |
317 | 1x |
message(" EXCEPTION: last row is a label or content row but in lpp") |
318 |
} |
|
319 | 6x |
} else if (!any(has_children)) { |
320 | 6x |
if (verbose) { |
321 | 6x |
message( |
322 | 6x |
" EXCEPTION: last row is a label or content row\n", |
323 | 6x |
"but does not have rows and row groups depending on it" |
324 |
) |
|
325 |
} |
|
326 |
} else { |
|
327 | ! |
if (verbose) { |
328 | ! |
message(" FAIL: last row is a label or content row") |
329 |
} |
|
330 | ! |
return(FALSE) |
331 |
} |
|
332 |
} |
|
333 | ||
334 |
# Siblings handling |
|
335 | 246x |
sibpos <- rw[["pos_in_siblings"]] |
336 | 246x |
nsib <- rw[["n_siblings"]] |
337 |
# okpos <- min(min_sibs + 1, rw[["n_siblings"]]) |
|
338 | 246x |
if (sibpos != nsib) { |
339 | 99x |
retfalse <- FALSE |
340 | 99x |
if (sibpos < min_sibs + 1) { |
341 | 25x |
retfalse <- TRUE |
342 | 25x |
if (verbose) { |
343 | 25x |
message( |
344 | 25x |
" FAIL: last row had only ", sibpos - 1, |
345 | 25x |
" preceding siblings, needed ", min_sibs |
346 |
) |
|
347 |
} |
|
348 | 74x |
} else if (nsib - sibpos < min_sibs + 1) { |
349 | 7x |
retfalse <- TRUE |
350 | 7x |
if (verbose) { |
351 | 4x |
message( |
352 | 4x |
" FAIL: last row had only ", nsib - sibpos - 1, |
353 | 4x |
" following siblings, needed ", min_sibs |
354 |
) |
|
355 |
} |
|
356 |
} |
|
357 | 99x |
if (retfalse) { |
358 | 32x |
return(FALSE) |
359 |
} |
|
360 |
} |
|
361 | 214x |
if (guess < nrow(pagdf) && length(nosplit > 0)) { |
362 |
## paths end at the leaf name which is *always* different |
|
363 | 16x |
curpth <- head(unlist(rw$path), -1) |
364 | 16x |
nxtpth <- head(unlist(pagdf$path[[guess + 1]]), -1) |
365 | ||
366 | 16x |
inplay <- nosplit[(nosplit %in% intersect(curpth, nxtpth))] |
367 | 16x |
if (length(inplay) > 0) { |
368 | 16x |
ok_split <- vapply(inplay, function(var) { |
369 | 16x |
!identical(curpth[match(var, curpth) + 1], nxtpth[match(var, nxtpth) + 1]) |
370 | 16x |
}, TRUE) |
371 | ||
372 | 16x |
curvals <- curpth[match(inplay, curpth) + 1] |
373 | 16x |
nxtvals <- nxtpth[match(inplay, nxtpth) + 1] |
374 | 16x |
if (!all(ok_split)) { |
375 | 16x |
if (verbose) { |
376 | 16x |
message( |
377 | 16x |
" FAIL: nosplit variable [", |
378 | 16x |
inplay[min(which(!ok_split))], "] would be constant [", |
379 | 16x |
curvals, "] across this pagebreak." |
380 |
) |
|
381 |
} |
|
382 | 16x |
return(FALSE) |
383 |
} |
|
384 |
} |
|
385 |
} |
|
386 | ||
387 |
# Usual output when found |
|
388 | 198x |
if (verbose) { |
389 | 83x |
message(" OK [", lines + rep_ext, if (row) " lines]" else " chars]") |
390 |
} |
|
391 | 198x |
TRUE |
392 |
} |
|
393 | ||
394 |
find_pag <- function(pagdf, |
|
395 |
current_page, |
|
396 |
start, |
|
397 |
guess, |
|
398 |
rlpp, |
|
399 |
lpp_or_cpp, |
|
400 |
context_lpp_or_cpp, |
|
401 |
min_siblings, |
|
402 |
nosplitin = character(), |
|
403 |
verbose = FALSE, |
|
404 |
row = TRUE, |
|
405 |
have_col_fnotes = FALSE, |
|
406 |
div_height = 1L, |
|
407 |
do_error = FALSE, |
|
408 |
col_gap, |
|
409 |
has_rowlabels) { |
|
410 | 204x |
if (verbose) { |
411 | 89x |
if (row) { |
412 | 44x |
message("--------- ROW-WISE: Checking possible pagination for page ", current_page) |
413 |
} else { |
|
414 | 45x |
message("========= COLUMN-WISE: Checking possible pagination for page ", current_page) |
415 |
} |
|
416 |
} |
|
417 | ||
418 | 204x |
origuess <- guess |
419 | 204x |
while (guess >= start && !valid_pag( |
420 | 204x |
pagdf, guess, |
421 | 204x |
start = start, |
422 | 204x |
rlpp = rlpp, lpp = lpp_or_cpp, context_lpp = context_lpp_or_cpp, # only lpp goes to row pagination |
423 | 204x |
min_sibs = min_siblings, |
424 | 204x |
nosplit = nosplitin, verbose, row = row, |
425 | 204x |
have_col_fnotes = have_col_fnotes, |
426 | 204x |
div_height = div_height, |
427 | 204x |
col_gap = col_gap, |
428 | 204x |
has_rowlabels = has_rowlabels |
429 |
)) { |
|
430 | 430x |
guess <- guess - 1 |
431 |
} |
|
432 | 204x |
if (guess < start) { |
433 |
# Repeat pagination process to see what went wrong with verbose on |
|
434 | 6x |
if (isFALSE(do_error) && isFALSE(verbose)) { |
435 | ! |
find_pag( |
436 | ! |
pagdf = pagdf, |
437 | ! |
current_page = current_page, |
438 | ! |
start = start, |
439 | ! |
guess = origuess, |
440 | ! |
rlpp = rlpp, lpp_or_cpp = lpp_or_cpp, context_lpp_or_cpp = context_lpp_or_cpp, |
441 | ! |
min_siblings = min_siblings, |
442 | ! |
nosplitin = nosplitin, |
443 | ! |
verbose = TRUE, |
444 | ! |
row = row, |
445 | ! |
have_col_fnotes = have_col_fnotes, |
446 | ! |
div_height = div_height, |
447 | ! |
do_error = TRUE, # only used to avoid loop |
448 | ! |
col_gap = col_gap, |
449 | ! |
has_rowlabels = has_rowlabels |
450 |
) |
|
451 |
} |
|
452 | 6x |
stop( |
453 | 6x |
"-------------------------------------- Error Summary ----------------------------------------\n", |
454 | 6x |
"Unable to find any valid pagination split for page ", current_page, " between ", |
455 | 6x |
ifelse(row, "rows ", "columns "), start, " and ", origuess, ". \n", |
456 | 6x |
"Inserted ", ifelse(row, "lpp (row-space, lines per page) ", "cpp (column-space, content per page) "), |
457 | 6x |
": ", lpp_or_cpp, "\n", |
458 | 6x |
"Context-relevant additional ", ifelse(row, "header/footers lines", "fixed column characters"), |
459 | 6x |
": ", context_lpp_or_cpp, "\n", |
460 | 6x |
ifelse(row, |
461 | 6x |
paste("Limit of allowed row lines per page:", rlpp, "\n"), |
462 | 6x |
paste("Check the minimum allowed column characters per page in the last FAIL(ed) attempt. \n") |
463 |
), |
|
464 | 6x |
"Note: take a look at the last FAIL(ed) attempt above to see what went wrong. It could be, for example, ", |
465 | 6x |
"that the inserted column width induces some wrapping, hence the inserted number of lines (lpp) is not enough." |
466 |
) |
|
467 |
} |
|
468 | 198x |
guess |
469 |
} |
|
470 | ||
471 |
#' Find pagination indices from pagination info data frame |
|
472 |
#' |
|
473 |
#' Pagination methods should typically call the `make_row_df` method |
|
474 |
#' for their object and then call this function on the resulting |
|
475 |
#' pagination info `data.frame`. |
|
476 |
#' |
|
477 |
#' @param pagdf (`data.frame`)\cr a pagination info `data.frame` as created by |
|
478 |
#' either `make_rows_df` or `make_cols_df`. |
|
479 |
#' @param rlpp (`numeric`)\cr maximum number of *row* lines per page (not including header materials), including |
|
480 |
#' (re)printed header and context rows. |
|
481 |
#' @param lpp_or_cpp (`numeric`)\cr total maximum number of *row* lines or content (column-wise characters) per page |
|
482 |
#' (including header materials and context rows). This is only for informative results with `verbose = TRUE`. |
|
483 |
#' It will print `NA` if not specified by the pagination machinery. |
|
484 |
#' @param context_lpp_or_cpp (`numeric`)\cr total number of context *row* lines or content (column-wise characters) |
|
485 |
#' per page (including header materials). Uses `NA` if not specified by the pagination machinery and is only |
|
486 |
#' for informative results with `verbose = TRUE`. |
|
487 |
#' @param min_siblings (`numeric`)\cr minimum sibling rows which must appear on either side of pagination row for a |
|
488 |
#' mid-subtable split to be valid. Defaults to 2 for tables. It is automatically turned off (set to 0) for listings. |
|
489 |
#' @param nosplitin (`character`)\cr list of names of subtables where page breaks are not allowed, regardless of other |
|
490 |
#' considerations. Defaults to none. |
|
491 |
#' @param verbose (`flag`)\cr whether additional informative messages about the search for |
|
492 |
#' pagination breaks should be shown. Defaults to `FALSE`. |
|
493 |
#' @param row (`flag`)\cr whether pagination is happening in row space (`TRUE`, the default) or column |
|
494 |
#' space (`FALSE`). |
|
495 |
#' @param have_col_fnotes (`flag`)\cr whether the table-like object being rendered has column-associated |
|
496 |
#' referential footnotes. |
|
497 |
#' @param div_height (`numeric(1)`)\cr the height of the divider line when the associated object is rendered. |
|
498 |
#' Defaults to `1`. |
|
499 |
#' @param col_gap (`numeric(1)`)\cr width of gap between columns, in same units as extent in `pagdf` (spaces |
|
500 |
#' under a particular font specification). |
|
501 |
#' @param has_rowlabels (`logical(1)`)\cr whether the object being paginated has row labels. |
|
502 |
#' |
|
503 |
#' @details `pab_indices_inner` implements the core pagination algorithm (see below) |
|
504 |
#' for a single direction (vertical if `row = TRUE` (the default), horizontal otherwise) |
|
505 |
#' based on the pagination data frame and (already adjusted for non-body rows/columns) |
|
506 |
#' lines (or characters) per page. |
|
507 |
#' |
|
508 |
#' @inheritSection pagination_algo Pagination Algorithm |
|
509 |
#' |
|
510 |
#' @return A `list` containing a vector of row numbers, broken up by page. |
|
511 |
#' |
|
512 |
#' @examples |
|
513 |
#' mypgdf <- basic_pagdf(row.names(mtcars)) |
|
514 |
#' |
|
515 |
#' paginds <- pag_indices_inner(mypgdf, rlpp = 15, min_siblings = 0) |
|
516 |
#' lapply(paginds, function(x) mtcars[x, ]) |
|
517 |
#' |
|
518 |
#' @export |
|
519 |
pag_indices_inner <- function(pagdf, |
|
520 |
rlpp, |
|
521 |
lpp_or_cpp = NA_integer_, context_lpp_or_cpp = NA_integer_, # Context number of lines |
|
522 |
min_siblings, |
|
523 |
nosplitin = character(), |
|
524 |
verbose = FALSE, |
|
525 |
row = TRUE, |
|
526 |
have_col_fnotes = FALSE, |
|
527 |
div_height = 1L, |
|
528 |
col_gap = 3L, |
|
529 |
has_rowlabels) { |
|
530 | 95x |
start <- 1 |
531 | 95x |
current_page <- 1 |
532 | 95x |
nr <- nrow(pagdf) |
533 | 95x |
ret <- list() |
534 | 95x |
while (start <= nr) { |
535 | 205x |
adjrlpp <- rlpp - pagdf$par_extent[start] |
536 | 205x |
if (adjrlpp <= 0) { |
537 | 1x |
if (row) { |
538 | 1x |
stop("Lines of repeated context (plus header materials) larger than specified lines per page") |
539 |
} else { |
|
540 | ! |
stop("Width of row labels equal to or larger than specified characters per page.") |
541 |
} |
|
542 |
} |
|
543 | 204x |
guess <- min(nr, start + adjrlpp - 1) |
544 | 204x |
end <- find_pag( |
545 | 204x |
pagdf = pagdf, |
546 | 204x |
current_page = current_page, start = start, guess = guess, |
547 | 204x |
rlpp = adjrlpp, lpp_or_cpp = lpp_or_cpp, context_lpp_or_cpp = context_lpp_or_cpp, |
548 | 204x |
min_siblings = min_siblings, |
549 | 204x |
nosplitin = nosplitin, |
550 | 204x |
verbose = verbose, |
551 | 204x |
row = row, |
552 | 204x |
have_col_fnotes = have_col_fnotes, |
553 | 204x |
div_height = div_height, |
554 | 204x |
col_gap = col_gap, |
555 | 204x |
has_rowlabels = has_rowlabels |
556 |
) |
|
557 | 198x |
ret <- c(ret, list(c( |
558 | 198x |
pagdf$reprint_inds[[start]], |
559 | 198x |
start:end |
560 |
))) |
|
561 | 198x |
start <- end + 1 |
562 | 198x |
current_page <- current_page + 1 |
563 |
} |
|
564 | 88x |
ret |
565 |
} |
|
566 | ||
567 |
#' Find column indices for vertical pagination |
|
568 |
#' |
|
569 |
#' @inheritParams pag_indices_inner |
|
570 |
#' @inheritParams open_font_dev |
|
571 |
#' @param obj (`ANY`)\cr object to be paginated. Must have a [matrix_form()] method. |
|
572 |
#' @param cpp (`numeric(1)`)\cr number of characters per page (width). |
|
573 |
#' @param colwidths (`numeric`)\cr vector of column widths (in characters) for use in vertical pagination. |
|
574 |
#' @param rep_cols (`numeric(1)`)\cr number of *columns* (not including row labels) to be repeated on every page. |
|
575 |
#' Defaults to 0. |
|
576 |
#' |
|
577 |
#' @return A `list` partitioning the vector of column indices into subsets for 1 or more horizontally paginated pages. |
|
578 |
#' |
|
579 |
#' @examples |
|
580 |
#' mf <- basic_matrix_form(df = mtcars) |
|
581 |
#' colpaginds <- vert_pag_indices(mf, fontspec = font_spec()) |
|
582 |
#' lapply(colpaginds, function(j) mtcars[, j, drop = FALSE]) |
|
583 |
#' |
|
584 |
#' @export |
|
585 |
vert_pag_indices <- function(obj, |
|
586 |
cpp = 40, |
|
587 |
colwidths = NULL, |
|
588 |
verbose = FALSE, |
|
589 |
rep_cols = 0L, |
|
590 |
fontspec, |
|
591 |
nosplitin = character()) { |
|
592 | 45x |
if (is.list(nosplitin)) { |
593 | ! |
nosplitin <- nosplitin[["cols"]] |
594 |
} |
|
595 | 45x |
mf <- matrix_form(obj, indent_rownames = TRUE, fontspec = fontspec) |
596 | 45x |
clwds <- colwidths %||% propose_column_widths(mf, fontspec = fontspec) |
597 | 45x |
if (is.null(mf_cinfo(mf))) { ## like always, ugh. |
598 | ! |
mf <- mpf_infer_cinfo(mf, colwidths = clwds, rep_cols = rep_cols, fontspec = fontspec) |
599 |
} |
|
600 | ||
601 | 45x |
num_rep_cols(mf) <- rep_cols |
602 | ||
603 | 45x |
has_rlabs <- mf_has_rlabels(mf) |
604 | 45x |
rlabs_flag <- as.integer(has_rlabs) |
605 | 45x |
rlab_extent <- if (has_rlabs) clwds[1] else 0L |
606 | ||
607 |
# rep_extent <- pdf$par_extent[nrow(pdf)] |
|
608 | 45x |
rcpp <- cpp - table_inset(mf) - rlab_extent # rep_extent - table_inset(mf) - rlab_extent |
609 | 45x |
if (verbose) { |
610 | 14x |
message( |
611 | 14x |
"Adjusted characters per page: ", rcpp, |
612 | 14x |
" [original: ", cpp, |
613 | 14x |
", table inset: ", table_inset(mf), if (has_rlabs) paste0(", row labels: ", clwds[1]), |
614 |
"]" |
|
615 |
) |
|
616 |
} |
|
617 | 45x |
res <- pag_indices_inner(mf_cinfo(mf), |
618 | 45x |
rlpp = rcpp, lpp_or_cpp = cpp, context_lpp_or_cpp = cpp - rcpp, |
619 |
# cpp - sum(clwds[seq_len(rep_cols)]), |
|
620 | 45x |
verbose = verbose, |
621 | 45x |
min_siblings = 1, |
622 | 45x |
nosplitin = nosplitin, |
623 | 45x |
row = FALSE, |
624 | 45x |
col_gap = mf_colgap(mf), |
625 | 45x |
has_rowlabels = mf_has_rlabels(mf) |
626 |
) |
|
627 | 44x |
res |
628 |
} |
|
629 | ||
630 |
mpf_infer_cinfo <- function(mf, colwidths = NULL, rep_cols = num_rep_cols(mf), fontspec, colpaths = NULL) { |
|
631 | 55x |
if (!is.null(mf_cinfo(mf))) { |
632 | 5x |
return(mf_update_cinfo(mf, colwidths = colwidths)) |
633 |
} |
|
634 | 50x |
new_dev <- open_font_dev(fontspec) |
635 | 50x |
if (new_dev) { |
636 | 50x |
on.exit(close_font_dev()) |
637 |
} |
|
638 | 50x |
if (!is(rep_cols, "numeric") || is.na(rep_cols) || rep_cols < 0) { |
639 | ! |
stop("got invalid number of columns to be repeated: ", rep_cols) |
640 |
} |
|
641 | 50x |
clwds <- (colwidths %||% mf_col_widths(mf)) %||% propose_column_widths(mf, fontspec = fontspec) |
642 | 50x |
has_rlabs <- mf_has_rlabels(mf) |
643 | 50x |
rlabs_flag <- as.integer(has_rlabs) |
644 | 50x |
rlab_extent <- if (has_rlabs) clwds[1] else 0L |
645 | 50x |
sqstart <- rlabs_flag + 1L # rep_cols + 1L |
646 | ||
647 | 50x |
pdfrows <- lapply( |
648 | 50x |
(sqstart):ncol(mf$strings), |
649 | 50x |
function(i) { |
650 | 292x |
rownum <- i - rlabs_flag |
651 | 292x |
rep_inds <- seq_len(rep_cols)[seq_len(rep_cols) < rownum] |
652 | 292x |
rep_extent_i <- sum( |
653 | 292x |
0L, |
654 | 292x |
clwds[rlabs_flag + rep_inds] |
655 | 292x |
) ## colwidths |
656 | 292x |
pagdfrow( |
657 | 292x |
row = NA, |
658 | 292x |
nm = rownum, |
659 | 292x |
lab = rownum, |
660 | 292x |
rnum = rownum, |
661 | 292x |
pth = NA, |
662 | 292x |
extent = clwds[i], |
663 | 292x |
repext = rep_extent_i, # sum(clwds[rep_cols]) + mf$col_gap * max(0, (length(rep_cols) - 1)), |
664 | 292x |
repind = rep_inds, # rep_cols, |
665 | 292x |
rclass = "stuff", |
666 | 292x |
sibpos = 1 - 1, |
667 | 292x |
nsibs = 1 - 1, |
668 | 292x |
fontspec = fontspec |
669 |
) |
|
670 |
} |
|
671 |
) |
|
672 | 50x |
pdf <- do.call(rbind, pdfrows) |
673 | ||
674 | 50x |
refdf <- mf_fnote_df(mf) |
675 | 50x |
pdf <- splice_fnote_info_in(pdf, refdf, row = FALSE) |
676 | 50x |
if (!is.null(colpaths)) { |
677 | ! |
if (length(colpaths) != NROW(pdf)) { |
678 |
## nocov start |
|
679 |
stop( |
|
680 |
"Got non-null colpaths with length not equal to number of columns (", |
|
681 |
length(colpaths), |
|
682 |
"!=", |
|
683 |
NROW(pdf), |
|
684 |
") during MatrixPrintForm construction. Please contact the maintainers." |
|
685 |
) |
|
686 |
## nocov end |
|
687 |
} |
|
688 | ! |
pdf[["path"]] <- colpaths |
689 |
} |
|
690 | 50x |
mf_cinfo(mf) <- pdf |
691 | 50x |
mf |
692 |
} |
|
693 | ||
694 |
#' Basic/spoof pagination info data frame |
|
695 |
#' |
|
696 |
#' Returns a minimal pagination info `data.frame` (with no info on siblings, footnotes, etc.). |
|
697 |
#' |
|
698 |
#' @inheritParams test_matrix_form |
|
699 |
#' @inheritParams open_font_dev |
|
700 |
#' @param rnames (`character`)\cr vector of row names. |
|
701 |
#' @param labs (`character`)\cr vector of row labels. Defaults to `rnames`. |
|
702 |
#' @param rnums (`integer`)\cr vector of row numbers. Defaults to `seq_along(rnames)`. |
|
703 |
#' @param extents (`integer`)\cr number of lines each row requires to print. Defaults to 1 for all rows. |
|
704 |
#' @param rclass (`character`)\cr class(es) for the rows. Defaults to `"DataRow"`. |
|
705 |
#' @param paths (`list`)\cr list of paths to the rows. Defaults to `lapply(rnames, function(x) c(parent_path, x))`. |
|
706 |
#' |
|
707 |
#' @return A `data.frame` suitable for use in both the `MatrixPrintForm` constructor and the pagination machinery. |
|
708 |
#' |
|
709 |
#' @examples |
|
710 |
#' basic_pagdf(c("hi", "there")) |
|
711 |
#' |
|
712 |
#' @export |
|
713 |
basic_pagdf <- function(rnames, |
|
714 |
labs = rnames, |
|
715 |
rnums = seq_along(rnames), |
|
716 |
extents = 1L, |
|
717 |
rclass = "DataRow", |
|
718 |
parent_path = NULL, |
|
719 |
paths = lapply(rnames, function(x) c(parent_path, x)), |
|
720 |
fontspec = font_spec()) { |
|
721 | 49x |
rws <- mapply(pagdfrow, |
722 | 49x |
nm = rnames, lab = labs, extent = extents, |
723 | 49x |
rclass = rclass, rnum = rnums, pth = paths, |
724 | 49x |
MoreArgs = list(fontspec = fontspec), |
725 | 49x |
SIMPLIFY = FALSE, nsibs = 1, sibpos = 1 |
726 |
) |
|
727 | 49x |
res <- do.call(rbind.data.frame, rws) |
728 | 49x |
res$n_siblings <- nrow(res) |
729 | 49x |
res$pos_in_siblings <- seq_along(res$n_siblings) |
730 | ||
731 | 49x |
if (!all(rclass == "DataRow")) { |
732 |
# These things are used in the simple case of a split, hence having labels. |
|
733 |
# To improve and extend to other cases |
|
734 | 4x |
res$pos_in_siblings <- NA |
735 | 4x |
res$pos_in_siblings[rclass == "DataRow"] <- 1 |
736 | 4x |
res$par_extent[rclass == "DataRow"] <- 1 # the rest is 0 |
737 | 4x |
res$n_siblings <- res$pos_in_siblings |
738 | 4x |
res$reprint_inds[which(rclass == "DataRow")] <- res$abs_rownumber[which(rclass == "DataRow") - 1] |
739 |
} |
|
740 | 49x |
res |
741 |
} |
|
742 | ||
743 |
## write paginate() which operates **solely** on a MatrixPrintForm obj |
|
744 | ||
745 |
page_size_spec <- function(lpp, cpp, max_width, |
|
746 |
font_family, |
|
747 |
font_size, |
|
748 |
lineheight, |
|
749 |
fontspec = font_spec( |
|
750 |
font_family = font_family, |
|
751 |
font_size = font_size, |
|
752 |
lineheight = lineheight |
|
753 |
)) { |
|
754 | 50x |
structure(list( |
755 | 50x |
lpp = lpp, |
756 | 50x |
cpp = cpp, |
757 | 50x |
max_width = max_width, |
758 | 50x |
font_spec = fontspec |
759 | 50x |
), class = "page_size_spec") |
760 |
} |
|
761 | ||
762 |
get_font_spec <- function(obj) { |
|
763 | 48x |
if (!is(obj, "page_size_spec")) { |
764 | ! |
stop("get_font_spec is only currently defined for page_size_spec objects") |
765 |
} |
|
766 | 48x |
obj$font_spec |
767 |
} |
|
768 | ||
769 | 100x |
non_null_na <- function(x) !is.null(x) && is.na(x) |
770 | ||
771 |
calc_lcpp <- function(page_type = NULL, |
|
772 |
landscape = FALSE, |
|
773 |
pg_width = page_dim(page_type)[if (landscape) 2 else 1], |
|
774 |
pg_height = page_dim(page_type)[if (landscape) 1 else 2], |
|
775 |
fontspec = font_spec(), |
|
776 |
## font_family = "Courier", |
|
777 |
## font_size = 8, # grid parameters |
|
778 |
cpp = NA_integer_, |
|
779 |
lpp = NA_integer_, |
|
780 |
tf_wrap = TRUE, |
|
781 |
max_width = NULL, |
|
782 |
## lineheight = 1, |
|
783 |
margins = c(bottom = .5, left = .75, top = .5, right = .75), |
|
784 |
colwidths, |
|
785 |
col_gap, |
|
786 |
inset) { |
|
787 | 50x |
pg_lcpp <- page_lcpp( |
788 | 50x |
page_type = page_type, |
789 | 50x |
landscape = landscape, |
790 |
## font_family = font_family, |
|
791 |
## font_size = font_size, |
|
792 |
## lineheight = lineheight, |
|
793 | 50x |
fontspec = fontspec, |
794 | 50x |
margins = margins, |
795 | 50x |
pg_width = pg_width, |
796 | 50x |
pg_height = pg_height |
797 |
) |
|
798 | ||
799 | 50x |
if (non_null_na(lpp)) { |
800 | 29x |
lpp <- pg_lcpp$lpp |
801 |
} |
|
802 | 50x |
if (non_null_na(cpp)) { |
803 | 22x |
cpp <- pg_lcpp$cpp |
804 |
} |
|
805 | 50x |
stopifnot(!is.na(cpp)) |
806 | ||
807 | 50x |
max_width <- .handle_max_width(tf_wrap, max_width, cpp, colwidths, col_gap, inset) |
808 | ||
809 | 50x |
page_size_spec( |
810 | 50x |
lpp = lpp, cpp = cpp, max_width = max_width, |
811 |
## font_family = font_family, |
|
812 |
## font_size = font_size, |
|
813 |
## lineheight = lineheight |
|
814 | 50x |
fontspec = fontspec |
815 |
) |
|
816 |
} |
|
817 | ||
818 |
calc_rlpp <- function(pg_size_spec, mf, colwidths, tf_wrap, verbose) { |
|
819 | 48x |
lpp <- pg_size_spec$lpp |
820 | 48x |
max_width <- pg_size_spec$max_width |
821 | 48x |
fontspec <- get_font_spec(pg_size_spec) |
822 | 48x |
dh <- divider_height(mf) |
823 | 48x |
if (any(nzchar(all_titles(mf)))) { |
824 |
## +1 is for blank line between subtitles and divider |
|
825 |
## dh is for divider line **between subtitles and column labels** |
|
826 |
## other divider line is accounted for in cinfo_lines |
|
827 | 24x |
if (!tf_wrap) { |
828 | 12x |
tlines <- length(all_titles(mf)) |
829 |
} else { |
|
830 | 12x |
tlines <- sum(nlines(all_titles(mf), colwidths = colwidths, max_width = max_width, fontspec = fontspec)) |
831 |
} |
|
832 | 24x |
tlines <- tlines + dh + 1L |
833 |
} else { |
|
834 | 24x |
tlines <- 0 |
835 |
} |
|
836 | ||
837 |
## dh for divider line between column labels and table body |
|
838 | 48x |
cinfo_lines <- mf_nlheader(mf) + dh |
839 | ||
840 | 48x |
if (verbose) { |
841 | 17x |
message( |
842 | 17x |
"Determining lines required for header content: ", |
843 | 17x |
tlines, " title and ", cinfo_lines, " table header lines" |
844 |
) |
|
845 |
} |
|
846 | ||
847 | 48x |
refdf <- mf_fnote_df(mf) |
848 | 48x |
cfn_df <- refdf[is.na(refdf$row) & !is.na(refdf$col), ] |
849 | ||
850 | 48x |
flines <- 0L |
851 | 48x |
mnfoot <- main_footer(mf) |
852 | 48x |
havemn <- length(mnfoot) && any(nzchar(mnfoot)) |
853 | 48x |
if (havemn) { |
854 | 25x |
flines <- nlines( |
855 | 25x |
mnfoot, |
856 | 25x |
colwidths = colwidths, |
857 | 25x |
max_width = max_width - table_inset(mf), |
858 | 25x |
fontspec = fontspec |
859 |
) |
|
860 |
} |
|
861 | 48x |
prfoot <- prov_footer(mf) |
862 | 48x |
if (length(prfoot) && any(nzchar(prfoot))) { |
863 | 31x |
flines <- flines + nlines(prov_footer(mf), colwidths = colwidths, max_width = max_width, fontspec = fontspec) |
864 | 31x |
if (havemn) { |
865 | 24x |
flines <- flines + 1L |
866 |
} ## space between main and prov footer. |
|
867 |
} |
|
868 |
## this time its for the divider between the footers and whatever is above them |
|
869 |
## (either table body or referential footnotes) |
|
870 | 48x |
if (flines > 0) { |
871 | 32x |
flines <- flines + dh + 1L |
872 |
} |
|
873 |
## this time its for the divider between the referential footnotes and |
|
874 |
## the table body IFF we have any, otherwise that divider+blanks pace doesn't get drawn |
|
875 | 48x |
if (NROW(cfn_df) > 0) { |
876 | ! |
cinfo_lines <- cinfo_lines + sum(cfn_df$nlines) |
877 | ! |
flines <- flines + dh + 1L |
878 |
} |
|
879 | ||
880 | 48x |
if (verbose) { |
881 | 17x |
message( |
882 | 17x |
"Determining lines required for footer content", |
883 | 17x |
if (NROW(cfn_df) > 0) " [column fnotes present]", |
884 | 17x |
": ", flines, " lines" |
885 |
) |
|
886 |
} |
|
887 | ||
888 | 48x |
ret <- lpp - flines - tlines - cinfo_lines |
889 | ||
890 | 48x |
if (verbose) { |
891 | 17x |
message("Lines per page available for tables rows: ", ret, " (original: ", lpp, ")") |
892 |
} |
|
893 | 48x |
ret |
894 |
} |
|
895 | ||
896 |
## this is ok to be unchanged because by this point |
|
897 |
## all of these are in terms of space widths |
|
898 |
calc_rcpp <- function(pg_size_spec, mf, colwidths) { |
|
899 | ! |
cpp <- pg_size_spec$cpp |
900 | ||
901 | ! |
cpp - table_inset(mf) - colwidths[1] - mf_colgap(mf) |
902 |
} |
|
903 | ||
904 |
splice_idx_lists <- function(lsts) { |
|
905 | ! |
list( |
906 | ! |
pag_row_indices = do.call(c, lapply(lsts, function(xi) xi$pag_row_indices)), |
907 | ! |
pag_col_indices = do.call(c, lapply(lsts, function(yi) yi$pag_col_indices)) |
908 |
) |
|
909 |
} |
|
910 | ||
911 |
#' Paginate a table-like object for rendering |
|
912 |
#' |
|
913 |
#' These functions perform or diagnose bi-directional pagination on an object. |
|
914 |
#' |
|
915 |
#' `paginate_indices` renders `obj` into a `MatrixPrintForm` (MPF), then uses that representation to |
|
916 |
#' calculate the rows and columns of `obj` corresponding to each page of the pagination of `obj`, but |
|
917 |
#' simply returns these indices rather than paginating `obj` itself (see Details for an important caveat). |
|
918 |
#' |
|
919 |
#' `paginate_to_mpfs` renders `obj` into its MPF intermediate representation, then paginates that MPF into |
|
920 |
#' component MPFs each corresponding to an individual page and returns those in a `list`. |
|
921 |
#' |
|
922 |
#' `diagnose_pagination` attempts pagination via `paginate_to_mpfs`, then returns diagnostic information |
|
923 |
#' which explains why page breaks were positioned where they were, or alternatively why no valid pagination |
|
924 |
#' could be found. |
|
925 |
#' |
|
926 |
#' @details |
|
927 |
#' All three of these functions generally support all classes which have a corresponding [matrix_form()] |
|
928 |
#' method which returns a valid `MatrixPrintForm` object (including `MatrixPrintForm` objects themselves). |
|
929 |
#' |
|
930 |
#' `paginate_indices` is directly called by `paginate_to_mpfs` (and thus `diagnose_pagination`). For most |
|
931 |
#' classes, and most tables represented by supported classes, calling `paginate_to_mpfs` is equivalent to a |
|
932 |
#' manual `paginate_indices -> subset obj into pages -> matrix_form` workflow. |
|
933 |
#' |
|
934 |
#' The exception to this equivalence is objects which support "forced pagination", or pagination logic which |
|
935 |
#' is built into the object itself rather than being a function of space on a page. Forced pagination |
|
936 |
#' generally involves the creation of, e.g., page-specific titles which apply to these forced paginations. |
|
937 |
#' `paginate_to_mpfs` and `diagnose_pagination` support forced pagination by automatically calling the |
|
938 |
#' [do_forced_paginate()] generic on the object and then paginating each object returned by that generic |
|
939 |
#' separately. The assumption here, then, is that page-specific titles and such are handled by the class' |
|
940 |
#' [do_forced_paginate()] method. |
|
941 |
#' |
|
942 |
#' `paginate_indices`, on the other hand, *does not support forced pagination*, because it returns only a |
|
943 |
#' set of indices for row and column subsetting for each page, and thus cannot retain any changes, e.g., |
|
944 |
#' to titles, done within [do_forced_paginate()]. `paginate_indices` does call [do_forced_paginate()], but |
|
945 |
#' instead of continuing it throws an error in the case that the result is larger than a single "page". |
|
946 |
#' |
|
947 |
#' @inheritParams vert_pag_indices |
|
948 |
#' @inheritParams pag_indices_inner |
|
949 |
#' @inheritParams page_lcpp |
|
950 |
#' @inheritParams toString |
|
951 |
#' @inheritParams propose_column_widths |
|
952 |
#' @param lpp (`numeric(1)` or `NULL`)\cr lines per page. If `NA` (the default), this is calculated automatically |
|
953 |
#' based on the specified page size). `NULL` indicates no vertical pagination should occur. |
|
954 |
#' @param cpp (`numeric(1)` or `NULL`)\cr width (in characters) per page. If `NA` (the default), this is calculated |
|
955 |
#' automatically based on the specified page size). `NULL` indicates no horizontal pagination should occur. |
|
956 |
#' @param pg_size_spec (`page_size_spec`)\cr. a pre-calculated page size specification. Typically this is not set by |
|
957 |
#' end users. |
|
958 |
#' @param col_gap (`numeric(1)`)\cr The number of spaces to be placed between columns |
|
959 |
#' in the rendered table (and assumed for horizontal pagination). |
|
960 |
#' @param page_num (`string`)\cr placeholder string for page numbers. See [default_page_number] for more |
|
961 |
#' information. Defaults to `NULL`. |
|
962 |
#' |
|
963 |
#' @return |
|
964 |
#' * `paginate_indices` returns a `list` with two elements of the same length: `pag_row_indices` and `pag_col_indices`. |
|
965 |
#' * `paginate_to_mpfs` returns a `list` of `MatrixPrintForm` objects representing each individual page after |
|
966 |
#' pagination (including forced pagination if necessary). |
|
967 |
#' |
|
968 |
#' @examples |
|
969 |
#' mpf <- basic_matrix_form(mtcars) |
|
970 |
#' |
|
971 |
#' paginate_indices(mpf, pg_width = 5, pg_height = 3) |
|
972 |
#' |
|
973 |
#' paginate_to_mpfs(mpf, pg_width = 5, pg_height = 3) |
|
974 |
#' |
|
975 |
#' @aliases paginate pagination |
|
976 |
#' @export |
|
977 |
paginate_indices <- function(obj, |
|
978 |
page_type = "letter", |
|
979 |
font_family = "Courier", |
|
980 |
font_size = 8, |
|
981 |
lineheight = 1, |
|
982 |
landscape = FALSE, |
|
983 |
pg_width = NULL, |
|
984 |
pg_height = NULL, |
|
985 |
margins = c(top = .5, bottom = .5, left = .75, right = .75), |
|
986 |
lpp = NA_integer_, |
|
987 |
cpp = NA_integer_, |
|
988 |
min_siblings = 2, |
|
989 |
nosplitin = list( |
|
990 |
rows = character(), |
|
991 |
cols = character() |
|
992 |
), |
|
993 |
colwidths = NULL, |
|
994 |
tf_wrap = FALSE, |
|
995 |
max_width = NULL, |
|
996 |
indent_size = 2, |
|
997 |
pg_size_spec = NULL, |
|
998 |
rep_cols = num_rep_cols(obj), |
|
999 |
col_gap = 3, |
|
1000 |
fontspec = font_spec(font_family, font_size, lineheight), |
|
1001 |
verbose = FALSE) { |
|
1002 |
## this preserves backwards compatibility |
|
1003 |
## could start deprecation cycle of char input |
|
1004 | 50x |
if (is.character(nosplitin)) { |
1005 | 47x |
nosplitin <- list( |
1006 | 47x |
rows = nosplitin, |
1007 | 47x |
cols = character() |
1008 |
) |
|
1009 |
} |
|
1010 | 50x |
newdev <- open_font_dev(fontspec) |
1011 | 50x |
if (newdev) { |
1012 | 3x |
on.exit(close_font_dev()) |
1013 |
} |
|
1014 |
## this MUST alsways return a list, inluding list(obj) when |
|
1015 |
## no forced pagination is needed! otherwise stuff breaks for things |
|
1016 |
## based on s3 classes that are lists underneath!!! |
|
1017 | 50x |
fpags <- do_forced_paginate(obj) |
1018 |
## if we have more than one forced "page", |
|
1019 |
## paginate each of them individually and return the result. |
|
1020 |
## forced pagination is ***currently*** only vertical, so |
|
1021 |
## we don't have to worry about divying up colwidths here, |
|
1022 |
## but we will if we ever allow force_paginate to do horiz |
|
1023 |
## pagination. |
|
1024 | 50x |
if (length(fpags) > 1) { |
1025 | 1x |
stop( |
1026 | 1x |
"forced pagination is required for this object (class: ", class(obj)[1], |
1027 | 1x |
") this is not supported in paginate_indices. Use paginate_to_mpfs or call ", |
1028 | 1x |
"do_forced_paginate on your object and paginate each returned section separately." |
1029 |
) |
|
1030 |
} |
|
1031 | ||
1032 |
## order is annoying here, since we won't actually need the mpf if |
|
1033 |
## we run into forced pagination, but life is short and this should work fine. |
|
1034 | 49x |
mpf <- matrix_form(obj, TRUE, TRUE, indent_size = indent_size, fontspec = fontspec) |
1035 | 49x |
if (is.null(colwidths)) { |
1036 | 2x |
colwidths <- mf_col_widths(mpf) %||% propose_column_widths(mpf, fontspec = fontspec) |
1037 |
} else { |
|
1038 | 47x |
mf_col_widths(mpf) <- colwidths |
1039 |
} |
|
1040 | ||
1041 | 49x |
mf_colgap(mpf) <- col_gap |
1042 | 49x |
if (!is.null(rep_cols) && rep_cols != num_rep_cols(obj)) { |
1043 | 3x |
num_rep_cols(mpf) <- rep_cols |
1044 |
} |
|
1045 | 49x |
if (NROW(mf_cinfo(mpf)) == 0) { |
1046 | ! |
mpf <- mpf_infer_cinfo(mpf, colwidths, rep_cols, fontspec = fontspec) |
1047 |
} |
|
1048 | ||
1049 | 49x |
if (is.null(pg_size_spec)) { |
1050 | 2x |
pg_size_spec <- calc_lcpp( |
1051 | 2x |
page_type = page_type, |
1052 |
## font_family = font_family, |
|
1053 |
## font_size = font_size, |
|
1054 |
## lineheight = lineheight, |
|
1055 | 2x |
fontspec = fontspec, |
1056 | 2x |
landscape = landscape, |
1057 | 2x |
pg_width = pg_width, |
1058 | 2x |
pg_height = pg_height, |
1059 | 2x |
margins = margins, |
1060 | 2x |
lpp = lpp, |
1061 | 2x |
cpp = cpp, |
1062 | 2x |
tf_wrap = tf_wrap, |
1063 | 2x |
max_width = max_width, |
1064 | 2x |
colwidths = colwidths, |
1065 | 2x |
inset = table_inset(mpf), |
1066 | 2x |
col_gap = col_gap |
1067 |
) |
|
1068 |
} |
|
1069 | ||
1070 |
## we can't support forced pagination in paginate_indices because |
|
1071 |
## forced pagination is generally going to set page titles, which |
|
1072 |
## we can't preserve when just returning lists of indices. |
|
1073 |
## Instead we make a hard assumption here that any forced pagination |
|
1074 |
## has already occurred. |
|
1075 | ||
1076 |
## this wraps the cell contents AND shoves referential footnote |
|
1077 |
## info into mf_rinfo(mpf) |
|
1078 | 49x |
mpf <- do_cell_fnotes_wrap(mpf, colwidths, max_width, tf_wrap = tf_wrap, fontspec = fontspec) |
1079 | ||
1080 |
# rlistings note: if there is a wrapping in a keycol, it is not calculated correctly |
|
1081 |
# in the above call, so we need to keep this information in mf_rinfo |
|
1082 |
# and use it here. |
|
1083 | 49x |
mfri <- mf_rinfo(mpf) |
1084 | 49x |
keycols <- .get_keycols_from_listing(obj) |
1085 | 49x |
if (NROW(mfri) > 1 && .is_listing_mf(mpf) && length(keycols) > 0) { |
1086 |
# Lets determine the groupings created by keycols |
|
1087 | 12x |
keycols_grouping_df <- NULL |
1088 | 12x |
for (i in seq_along(keycols)) { |
1089 | 24x |
kcol <- keycols[i] |
1090 | 24x |
if (is(obj, "MatrixPrintForm")) { |
1091 |
# This makes the function work also in the case we have only matrix form (mainly for testing purposes) |
|
1092 | 24x |
kcolvec <- mf_strings(obj)[, mf_strings(obj)[1, , drop = TRUE] == kcol][-1] |
1093 | 24x |
while (any(kcolvec == "")) { |
1094 | 284x |
kcolvec[which(kcolvec == "")] <- kcolvec[which(kcolvec == "") - 1] |
1095 |
} |
|
1096 |
} else { |
|
1097 | ! |
kcolvec <- obj[[kcol]] |
1098 | ! |
kcolvec <- vapply(kcolvec, format_value, "", format = obj_format(kcolvec), na_str = obj_na_str(kcolvec)) |
1099 |
} |
|
1100 | 24x |
groupings <- as.numeric(factor(kcolvec, levels = unique(kcolvec))) |
1101 | 24x |
where_they_start <- which(c(1, diff(groupings)) > 0) |
1102 | 24x |
keycols_grouping_df <- cbind( |
1103 | 24x |
keycols_grouping_df, |
1104 | 24x |
where_they_start[groupings] |
1105 | 24x |
) # take the groupings |
1106 |
} |
|
1107 | ||
1108 |
# Creating the real self_extend for mf_rinfo (if the line is chosen for pagination start) |
|
1109 | 12x |
self_extent_df <- apply(keycols_grouping_df, 2, function(x) mfri$self_extent[x]) |
1110 | 12x |
mf_rinfo(mpf) <- cbind(mfri, "self_extent_page_break" = apply(self_extent_df, 1, max)) |
1111 |
} |
|
1112 | ||
1113 | 49x |
if (is.null(pg_size_spec$lpp)) { |
1114 | 1x |
pag_row_indices <- list(seq_len(mf_nrow(mpf))) |
1115 |
} else { |
|
1116 | 48x |
rlpp <- calc_rlpp( |
1117 | 48x |
pg_size_spec, mpf, |
1118 | 48x |
colwidths = colwidths, |
1119 | 48x |
tf_wrap = tf_wrap, verbose = verbose |
1120 |
) |
|
1121 | 48x |
pag_row_indices <- pag_indices_inner( |
1122 | 48x |
pagdf = mf_rinfo(mpf), |
1123 | 48x |
rlpp = rlpp, |
1124 | 48x |
lpp_or_cpp = pg_size_spec$lpp, |
1125 | 48x |
context_lpp_or_cpp = pg_size_spec$lpp - rlpp, |
1126 | 48x |
verbose = verbose, |
1127 | 48x |
min_siblings = min_siblings, |
1128 | 48x |
nosplitin = nosplitin[["rows"]], |
1129 | 48x |
col_gap = col_gap, |
1130 | 48x |
has_rowlabels = mf_has_rlabels(mpf) |
1131 |
) |
|
1132 |
} |
|
1133 | ||
1134 | 44x |
if (is.null(pg_size_spec$cpp)) { |
1135 | 1x |
pag_col_indices <- list(seq_len(mf_ncol(mpf))) |
1136 |
} else { |
|
1137 | 43x |
pag_col_indices <- vert_pag_indices( |
1138 | 43x |
mpf, |
1139 | 43x |
cpp = pg_size_spec$cpp, colwidths = colwidths, |
1140 | 43x |
rep_cols = rep_cols, fontspec = fontspec, |
1141 | 43x |
nosplitin = nosplitin[["cols"]], |
1142 | 43x |
verbose = verbose |
1143 |
) |
|
1144 |
} |
|
1145 | ||
1146 | 43x |
list(pag_row_indices = pag_row_indices, pag_col_indices = pag_col_indices) |
1147 |
} |
|
1148 | ||
1149 | 47x |
setGeneric("has_page_title", function(obj) standardGeneric("has_page_title")) |
1150 | ||
1151 | 47x |
setMethod("has_page_title", "ANY", function(obj) length(page_titles(obj)) > 0) |
1152 | ||
1153 |
#' @rdname paginate_indices |
|
1154 |
#' @export |
|
1155 |
paginate_to_mpfs <- function(obj, |
|
1156 |
page_type = "letter", |
|
1157 |
font_family = "Courier", |
|
1158 |
font_size = 8, |
|
1159 |
lineheight = 1, |
|
1160 |
landscape = FALSE, |
|
1161 |
pg_width = NULL, |
|
1162 |
pg_height = NULL, |
|
1163 |
margins = c(top = .5, bottom = .5, left = .75, right = .75), |
|
1164 |
lpp = NA_integer_, |
|
1165 |
cpp = NA_integer_, |
|
1166 |
min_siblings = 2, |
|
1167 |
nosplitin = character(), |
|
1168 |
colwidths = NULL, |
|
1169 |
tf_wrap = FALSE, |
|
1170 |
max_width = NULL, |
|
1171 |
indent_size = 2, |
|
1172 |
pg_size_spec = NULL, |
|
1173 |
page_num = default_page_number(), |
|
1174 |
rep_cols = NULL, |
|
1175 |
# rep_cols = num_rep_cols(obj), |
|
1176 |
# col_gap = 3, # this could be change in default - breaking change |
|
1177 |
col_gap = 3, |
|
1178 |
fontspec = font_spec(font_family, font_size, lineheight), |
|
1179 |
verbose = FALSE) { |
|
1180 | 57x |
newdev <- open_font_dev(fontspec) |
1181 | 57x |
if (newdev) { |
1182 | 32x |
on.exit(close_font_dev()) |
1183 |
} |
|
1184 | ||
1185 | 57x |
if (isTRUE(page_num)) { |
1186 | 1x |
page_num <- "page {i}/{n}" |
1187 |
} |
|
1188 | 57x |
checkmate::assert_string(page_num, null.ok = TRUE, min.chars = 1) |
1189 | ||
1190 |
# We can return a list of paginated tables and listings |
|
1191 | 57x |
if (.is_list_of_tables_or_listings(obj)) { |
1192 | 8x |
cur_call <- match.call(expand.dots = FALSE) |
1193 | 8x |
mpfs <- unlist( |
1194 | 8x |
lapply(obj, function(obj_i) { |
1195 | 15x |
cur_call[["obj"]] <- obj_i |
1196 | 15x |
eval(cur_call, envir = parent.frame(3L)) |
1197 |
}), |
|
1198 | 8x |
recursive = FALSE |
1199 |
) |
|
1200 | ||
1201 | 7x |
if (!is.null(page_num)) { |
1202 | 3x |
extracted_cpp <- max( |
1203 | 3x |
sapply(mpfs, function(mpf) { |
1204 | 12x |
pf <- prov_footer(mpf) |
1205 | 12x |
nchar(pf[length(pf)]) |
1206 |
}) |
|
1207 |
) |
|
1208 | 3x |
mpfs <- .modify_footer_for_page_nums(mpfs, page_num, extracted_cpp) |
1209 |
} |
|
1210 | ||
1211 | 7x |
return(mpfs) |
1212 |
} |
|
1213 | ||
1214 | 49x |
if (!is.null(page_num)) { |
1215 |
# Only adding a line for pagination -> lpp - 1 would have worked too |
|
1216 | 14x |
prov_footer(obj) <- c(prov_footer(obj), page_num) |
1217 |
} |
|
1218 | ||
1219 | 49x |
mpf <- matrix_form(obj, TRUE, TRUE, indent_size = indent_size, fontspec = fontspec) |
1220 |
# For listings, keycols are mandatory rep_num_cols |
|
1221 | 49x |
if (is.null(rep_cols)) { |
1222 | 44x |
rep_cols <- num_rep_cols(obj) |
1223 |
} |
|
1224 | 49x |
num_rep_cols(mpf) <- rep_cols |
1225 | ||
1226 |
# Turning off min_siblings for listings |
|
1227 | 49x |
if (.is_listing_mf(mpf)) { |
1228 | 13x |
min_siblings <- 0 |
1229 |
} |
|
1230 | ||
1231 |
# Checking colwidths |
|
1232 | 49x |
if (is.null(colwidths)) { |
1233 | 33x |
colwidths <- mf_col_widths(mpf) %||% propose_column_widths(mpf, fontspec = fontspec) |
1234 |
} else { |
|
1235 | 16x |
cur_ncol <- ncol(mpf) |
1236 | 16x |
if (!.is_listing_mf(mpf)) { |
1237 | 10x |
cur_ncol <- cur_ncol + as.numeric(mf_has_rlabels(mpf)) |
1238 |
} |
|
1239 | 16x |
if (length(colwidths) != cur_ncol) { |
1240 | 1x |
stop( |
1241 | 1x |
"non-null colwidths argument must have length ncol(x) (+ 1 if row labels are present and if it is a table) [", |
1242 | 1x |
cur_ncol, "], got length ", length(colwidths) |
1243 |
) |
|
1244 |
} |
|
1245 | 15x |
mf_col_widths(mpf) <- colwidths |
1246 |
} |
|
1247 | ||
1248 | 48x |
if (NROW(mf_cinfo(mpf)) == 0) { |
1249 | ! |
mpf <- mpf_infer_cinfo(mpf, colwidths, rep_cols, fontspec = fontspec) |
1250 |
} |
|
1251 | ||
1252 | 48x |
if (is.null(pg_size_spec)) { |
1253 | 46x |
pg_size_spec <- calc_lcpp( |
1254 | 46x |
page_type = page_type, |
1255 |
## font_family = font_family, |
|
1256 |
## font_size = font_size, |
|
1257 |
## lineheight = lineheight, |
|
1258 | 46x |
fontspec = fontspec, |
1259 | 46x |
landscape = landscape, |
1260 | 46x |
pg_width = pg_width, |
1261 | 46x |
pg_height = pg_height, |
1262 | 46x |
margins = margins, |
1263 | 46x |
lpp = lpp, |
1264 | 46x |
cpp = cpp, |
1265 | 46x |
tf_wrap = tf_wrap, |
1266 | 46x |
max_width = max_width, |
1267 | 46x |
colwidths = colwidths, |
1268 | 46x |
inset = table_inset(mpf), |
1269 | 46x |
col_gap = col_gap |
1270 |
) |
|
1271 |
} |
|
1272 |
## this MUST always return a list, including list(obj) when |
|
1273 |
## no forced pagination is needed! otherwise stuff breaks for things |
|
1274 |
## based on s3 classes that are lists underneath!!! |
|
1275 | 48x |
fpags <- do_forced_paginate(obj) |
1276 | ||
1277 |
## if we have more than one forced "page", |
|
1278 |
## paginate each of them individually and return the result. |
|
1279 |
## forced pagination is ***currently*** only vertical, so |
|
1280 |
## we don't have to worry about divying up colwidths here, |
|
1281 |
## but we will if we ever allow force_paginate to do horiz |
|
1282 |
## pagination. |
|
1283 | 48x |
if (length(fpags) > 1) { |
1284 |
# Correction for case we are entering here (page_by) |
|
1285 | 1x |
if (!is.null(page_num)) { |
1286 | ! |
prov_footer(obj) <- head(prov_footer(obj), -1) |
1287 | ! |
fpags <- lapply(fpags, function(x) { |
1288 | ! |
prov_footer(x) <- head(prov_footer(x), -1) |
1289 | ! |
x |
1290 |
}) |
|
1291 |
} |
|
1292 |
# XXX to merge with listings and avoid recursive (after PR #296) |
|
1293 | 1x |
deep_pag <- paginate_to_mpfs( # what about the other parameters? |
1294 | 1x |
fpags, |
1295 | 1x |
pg_size_spec = pg_size_spec, |
1296 | 1x |
colwidths = colwidths, |
1297 | 1x |
min_siblings = min_siblings, |
1298 | 1x |
nosplitin = nosplitin, |
1299 | 1x |
fontspec = fontspec, |
1300 | 1x |
verbose = verbose, |
1301 | 1x |
rep_cols = rep_cols, |
1302 | 1x |
page_num = page_num |
1303 |
) |
|
1304 | 1x |
return(deep_pag) |
1305 | 47x |
} else if (has_page_title(fpags[[1]])) { |
1306 | ! |
obj <- fpags[[1]] |
1307 |
} |
|
1308 | ||
1309 |
## we run into forced pagination, but life is short and this should work fine. |
|
1310 | 47x |
mpf <- matrix_form(obj, TRUE, TRUE, indent_size = indent_size, fontspec = fontspec) |
1311 | 47x |
num_rep_cols(mpf) <- rep_cols |
1312 | 47x |
if (is.null(colwidths)) { |
1313 | ! |
colwidths <- mf_col_widths(mpf) %||% propose_column_widths(mpf, fontspec = fontspec) |
1314 |
} |
|
1315 | 47x |
mf_col_widths(mpf) <- colwidths |
1316 | 47x |
mf_colgap(mpf) <- col_gap |
1317 | ||
1318 | 47x |
page_indices <- paginate_indices( |
1319 | 47x |
obj = obj, |
1320 |
## page_type = page_type, |
|
1321 |
## font_family = font_family, |
|
1322 |
## font_size = font_size, |
|
1323 |
## lineheight = lineheight, |
|
1324 |
## landscape = landscape, |
|
1325 |
## pg_width = pg_width, |
|
1326 |
## pg_height = pg_height, |
|
1327 |
## margins = margins, |
|
1328 | 47x |
pg_size_spec = pg_size_spec, |
1329 |
## lpp = lpp, |
|
1330 |
## cpp = cpp, |
|
1331 | 47x |
min_siblings = min_siblings, |
1332 | 47x |
nosplitin = nosplitin, |
1333 | 47x |
colwidths = colwidths, |
1334 | 47x |
tf_wrap = tf_wrap, |
1335 |
## max_width = max_width, |
|
1336 | 47x |
rep_cols = rep_cols, |
1337 | 47x |
verbose = verbose, |
1338 | 47x |
col_gap = col_gap, |
1339 | 47x |
fontspec = fontspec |
1340 |
) |
|
1341 | ||
1342 | 43x |
pagmats <- lapply(page_indices$pag_row_indices, function(ii) { |
1343 | 89x |
mpf_subset_rows(mpf, ii, keycols = .get_keycols_from_listing(obj)) |
1344 |
}) |
|
1345 |
## these chunks now carry around their (correctly subset) col widths... |
|
1346 | 43x |
res <- lapply(pagmats, function(matii) { |
1347 | 89x |
lapply(page_indices$pag_col_indices, function(jj) { |
1348 | 220x |
mpf_subset_cols(matii, jj, keycols = .get_keycols_from_listing(obj)) |
1349 |
}) |
|
1350 |
}) |
|
1351 | ||
1352 | 43x |
res <- unlist(res, recursive = FALSE) |
1353 | ||
1354 |
# Adding page numbers if needed |
|
1355 | 43x |
if (!is.null(page_num)) { |
1356 | 14x |
res <- .modify_footer_for_page_nums( |
1357 | 14x |
mf_list = res, |
1358 | 14x |
page_num_format = page_num, |
1359 | 14x |
current_cpp = pg_size_spec$cpp |
1360 |
) |
|
1361 |
} |
|
1362 | ||
1363 | 42x |
res |
1364 |
} |
|
1365 | ||
1366 |
.modify_footer_for_page_nums <- function(mf_list, page_num_format, current_cpp) { |
|
1367 | 17x |
total_pages <- length(mf_list) |
1368 | 17x |
page_str <- gsub("\\{n\\}", total_pages, page_num_format) |
1369 | 17x |
page_nums <- vapply( |
1370 | 17x |
seq_len(total_pages), |
1371 | 17x |
function(x) { |
1372 | 135x |
gsub("\\{i\\}", x, page_str) |
1373 |
}, |
|
1374 | 17x |
FUN.VALUE = character(1) |
1375 |
) |
|
1376 | 17x |
page_footer <- sprintf(paste0("%", current_cpp, "s"), page_nums) |
1377 | 17x |
if (any(nchar(page_footer) > current_cpp)) { |
1378 | 1x |
stop("Page numbering string (page_num) is too wide to fit the desired page size width (cpp).") |
1379 |
} |
|
1380 | ||
1381 | 16x |
lapply(seq_along(mf_list), function(pg_i) { |
1382 | 69x |
prov_footer(mf_list[[pg_i]]) <- c(head(prov_footer(mf_list[[pg_i]]), -1), page_footer[pg_i]) |
1383 | 69x |
mf_list[[pg_i]] |
1384 |
}) |
|
1385 |
} |
|
1386 | ||
1387 |
# This works only with matrix_form objects |
|
1388 |
.is_listing_mf <- function(mf) { |
|
1389 | 1143x |
all(mf_rinfo(mf)$node_class == "listing_df") |
1390 |
} |
|
1391 | ||
1392 |
# Extended copy of get_keycols |
|
1393 |
.get_keycols_from_listing <- function(obj) { |
|
1394 | 88x |
if (is(obj, "listing_df")) { |
1395 | ! |
names(which(sapply(obj, is, class2 = "listing_keycol"))) |
1396 | 88x |
} else if (is(obj, "MatrixPrintForm") && .is_listing_mf(obj)) { |
1397 | 52x |
obj$listing_keycols |
1398 |
} else { |
|
1399 | 36x |
NULL # table case |
1400 |
} |
|
1401 |
} |
|
1402 | ||
1403 |
#' @importFrom utils capture.output |
|
1404 |
#' @details |
|
1405 |
#' `diagnose_pagination` attempts pagination and then, regardless of success or failure, returns diagnostic |
|
1406 |
#' information about pagination attempts (if any) after each row and column. |
|
1407 |
#' |
|
1408 |
#' The diagnostics data reflects the final time the pagination algorithm evaluated a page break at the |
|
1409 |
#' specified location, regardless of how many times the position was assessed in total. |
|
1410 |
#' |
|
1411 |
#' To get information about intermediate attempts, perform pagination with `verbose = TRUE` and inspect |
|
1412 |
#' the messages in order. |
|
1413 |
#' |
|
1414 |
#' @importFrom utils capture.output |
|
1415 |
#' |
|
1416 |
#' @return |
|
1417 |
#' * `diagnose_pagination` returns a `list` containing: |
|
1418 |
#' |
|
1419 |
#' \describe{ |
|
1420 |
#' \item{`lpp_diagnostics`}{Diagnostic information regarding lines per page.} |
|
1421 |
#' \item{`row_diagnostics`}{Basic information about rows, whether pagination was attempted |
|
1422 |
#' after each row, and the final result of such an attempt, if made.} |
|
1423 |
#' \item{`cpp_diagnostics`}{Diagnostic information regarding columns per page.} |
|
1424 |
#' \item{`col_diagnostics`}{Very basic information about leaf columns, whether pagination |
|
1425 |
#' was attempted after each leaf column, ad the final result of such attempts, if made.} |
|
1426 |
#' } |
|
1427 |
#' |
|
1428 |
#' @note |
|
1429 |
#' For `diagnose_pagination`, the column labels are not displayed in the `col_diagnostics` element |
|
1430 |
#' due to certain internal implementation details; rather the diagnostics are reported in terms of |
|
1431 |
#' absolute (leaf) column position. This is a known limitation, and may eventually be changed, but the |
|
1432 |
#' information remains useful as it is currently reported. |
|
1433 |
#' |
|
1434 |
#' `diagnose_pagination` is intended for interactive debugging use and *should not be programmed against*, |
|
1435 |
#' as the exact content and form of the verbose messages it captures and returns is subject to change. |
|
1436 |
#' |
|
1437 |
#' Because `diagnose_pagination` relies on `capture.output(type = "message")`, it cannot be used within the |
|
1438 |
#' `testthat` (and likely other) testing frameworks, and likely cannot be used within `knitr`/`rmarkdown` |
|
1439 |
#' contexts either, as this clashes with those systems' capture of messages. |
|
1440 |
#' |
|
1441 |
#' @examples |
|
1442 |
#' diagnose_pagination(mpf, pg_width = 5, pg_height = 3) |
|
1443 |
#' clws <- propose_column_widths(mpf) |
|
1444 |
#' clws[1] <- floor(clws[1] / 3) |
|
1445 |
#' dgnost <- diagnose_pagination(mpf, pg_width = 5, pg_height = 3, colwidths = clws) |
|
1446 |
#' try(diagnose_pagination(mpf, pg_width = 1)) # fails |
|
1447 |
#' |
|
1448 |
#' @rdname paginate_indices |
|
1449 |
#' @export |
|
1450 |
diagnose_pagination <- function(obj, |
|
1451 |
page_type = "letter", |
|
1452 |
font_family = "Courier", |
|
1453 |
font_size = 8, |
|
1454 |
lineheight = 1, |
|
1455 |
landscape = FALSE, |
|
1456 |
pg_width = NULL, |
|
1457 |
pg_height = NULL, |
|
1458 |
margins = c(top = .5, bottom = .5, left = .75, right = .75), |
|
1459 |
lpp = NA_integer_, |
|
1460 |
cpp = NA_integer_, |
|
1461 |
min_siblings = 2, |
|
1462 |
nosplitin = character(), |
|
1463 |
colwidths = propose_column_widths(matrix_form(obj, TRUE), fontspec = fontspec), |
|
1464 |
tf_wrap = FALSE, |
|
1465 |
max_width = NULL, |
|
1466 |
indent_size = 2, |
|
1467 |
pg_size_spec = NULL, |
|
1468 |
rep_cols = num_rep_cols(obj), |
|
1469 |
col_gap = 3, |
|
1470 |
verbose = FALSE, |
|
1471 |
fontspec = font_spec( |
|
1472 |
font_family, |
|
1473 |
font_size, |
|
1474 |
lineheight |
|
1475 |
), |
|
1476 |
...) { |
|
1477 | 6x |
new_dev <- open_font_dev(fontspec) |
1478 | 6x |
if (new_dev) { |
1479 | 4x |
on.exit(close_font_dev()) |
1480 |
} |
|
1481 | 6x |
fpag <- do_forced_paginate(obj) |
1482 | 6x |
if (length(fpag) > 1) { |
1483 | 1x |
return(lapply( |
1484 | 1x |
fpag, |
1485 | 1x |
diagnose_pagination, |
1486 | 1x |
page_type = page_type, |
1487 | 1x |
landscape = landscape, |
1488 | 1x |
pg_width = pg_width, |
1489 | 1x |
pg_height = pg_height, |
1490 | 1x |
margins = margins, |
1491 | 1x |
lpp = lpp, |
1492 | 1x |
cpp = cpp, |
1493 | 1x |
tf_wrap = tf_wrap, |
1494 | 1x |
max_width = max_width, |
1495 | 1x |
colwidths = colwidths, |
1496 | 1x |
col_gap = col_gap, |
1497 | 1x |
min_siblings = min_siblings, |
1498 | 1x |
nosplitin = nosplitin, |
1499 | 1x |
fontspec = fontspec |
1500 |
)) |
|
1501 |
} |
|
1502 | ||
1503 | 5x |
mpf <- matrix_form(obj, TRUE, fontspec = fontspec) |
1504 | 5x |
msgres <- capture.output( |
1505 |
{ |
|
1506 | 5x |
tmp <- try( |
1507 | 5x |
paginate_to_mpfs( |
1508 | 5x |
obj, |
1509 | 5x |
page_type = page_type, |
1510 | 5x |
landscape = landscape, |
1511 | 5x |
pg_width = pg_width, |
1512 | 5x |
pg_height = pg_height, |
1513 | 5x |
margins = margins, |
1514 | 5x |
lpp = lpp, |
1515 | 5x |
cpp = cpp, |
1516 | 5x |
tf_wrap = tf_wrap, |
1517 | 5x |
max_width = max_width, |
1518 | 5x |
colwidths = colwidths, |
1519 | 5x |
col_gap = col_gap, |
1520 | 5x |
min_siblings = min_siblings, |
1521 | 5x |
nosplitin = nosplitin, |
1522 | 5x |
fontspec = fontspec, |
1523 | 5x |
verbose = TRUE |
1524 |
) |
|
1525 |
) |
|
1526 |
}, |
|
1527 | 5x |
type = "message" |
1528 |
) |
|
1529 | 5x |
if (is(tmp, "try-error") && grepl("Width of row labels equal to or larger", tmp)) { |
1530 | ! |
cond <- attr(tmp, "condition") |
1531 | ! |
stop(conditionMessage(cond), call. = conditionCall(cond)) |
1532 |
} |
|
1533 | ||
1534 | 5x |
lpp_diagnostic <- grep("^(Determining lines|Lines per page available).*$", msgres, value = TRUE) |
1535 | 5x |
cpp_diagnostic <- unique(grep("^Adjusted characters per page.*$", msgres, value = TRUE)) |
1536 | ||
1537 | 5x |
mpf <- do_cell_fnotes_wrap( |
1538 | 5x |
mpf, |
1539 | 5x |
widths = colwidths, max_width = max_width, tf_wrap = tf_wrap, |
1540 | 5x |
fontspec = font_spec(font_family, font_size, lineheight) |
1541 |
) |
|
1542 | 5x |
mpf <- mpf_infer_cinfo(mpf, colwidths = colwidths, fontspec = fontspec) |
1543 | ||
1544 | 5x |
rownls <- grep("Checking pagination after row", msgres, fixed = TRUE) |
1545 | 5x |
rownum <- as.integer(gsub("[^[:digit:]]*(.*)$", "\\1", msgres[rownls])) |
1546 | 5x |
rowmsgs <- vapply(unique(rownum), function(ii) { |
1547 | ! |
idx <- max(which(rownum == ii)) |
1548 | ! |
gsub("\\t[.]*", "", msgres[rownls[idx] + 1]) |
1549 |
}, "") |
|
1550 | ||
1551 | 5x |
msgdf <- data.frame( |
1552 | 5x |
abs_rownumber = unique(rownum), |
1553 | 5x |
final_pag_result = rowmsgs, stringsAsFactors = FALSE |
1554 |
) |
|
1555 | 5x |
rdf <- mf_rinfo(mpf)[, c("abs_rownumber", "label", "self_extent", "par_extent", "node_class")] |
1556 | 5x |
rdf$pag_attempted <- rdf$abs_rownumber %in% rownum |
1557 | 5x |
row_diagnose <- merge(rdf, msgdf, by = "abs_rownumber", all.x = TRUE) |
1558 | ||
1559 | 5x |
colnls <- grep("Checking pagination after column", msgres, fixed = TRUE) |
1560 | 5x |
colnum <- as.integer(gsub("[^[:digit:]]*(.*)$", "\\1", msgres[colnls])) |
1561 | 5x |
colmsgs <- vapply(unique(colnum), function(ii) { |
1562 | ! |
idx <- max(which(colnum == ii)) |
1563 | ! |
gsub("\\t[.]*", "", msgres[colnls[idx] + 1]) |
1564 |
}, "") |
|
1565 | ||
1566 | 5x |
colmsgdf <- data.frame( |
1567 | 5x |
abs_rownumber = unique(colnum), |
1568 | 5x |
final_pag_result = colmsgs, |
1569 | 5x |
stringsAsFactors = FALSE |
1570 |
) |
|
1571 | 5x |
cdf <- mf_cinfo(mpf)[, c("abs_rownumber", "self_extent")] |
1572 | 5x |
cdf$pag_attempted <- cdf$abs_rownumber %in% colnum |
1573 | 5x |
col_diagnose <- merge(cdf, colmsgdf, by = "abs_rownumber", all.x = TRUE) |
1574 | 5x |
names(col_diagnose) <- gsub("^abs_rownumber$", "abs_colnumber", names(col_diagnose)) |
1575 | 5x |
list( |
1576 | 5x |
lpp_diagnostics = lpp_diagnostic, |
1577 | 5x |
row_diagnostics = row_diagnose, |
1578 | 5x |
cpp_diagnostics = cpp_diagnostic, |
1579 | 5x |
col_diagnostics = col_diagnose |
1580 |
) |
|
1581 |
} |
1 |
### This file defines the generics which make up the interface `formatters` offers. |
|
2 |
### Defining methods for these generics for a new table-like class should be fully |
|
3 |
### sufficient for hooking that class up to the `formatters` pagination and rendering |
|
4 |
### machinery. |
|
5 | ||
6 |
#' Make row layout summary data frames for use during pagination |
|
7 |
#' |
|
8 |
#' All relevant information about table rows (e.g. indentations) is summarized in a `data.frame`. |
|
9 |
#' This function works **only** on `rtables` and `rlistings` objects, and not on their `print` counterparts |
|
10 |
#' (like [`MatrixPrintForm`]). |
|
11 |
#' |
|
12 |
#' @inheritParams open_font_dev |
|
13 |
#' @param tt (`ANY`)\cr object representing the table-like object to be summarized. |
|
14 |
#' @param visible_only (`flag`)\cr should only visible aspects of the table structure be reflected |
|
15 |
#' in this summary. Defaults to `TRUE`. May not be supported by all methods. |
|
16 |
#' @param incontent (`flag`)\cr internal detail, do not set manually. |
|
17 |
#' @param repr_ext (`integer(1)`)\cr internal detail, do not set manually. |
|
18 |
#' @param repr_inds (`integer`)\cr internal detail, do not set manually. |
|
19 |
#' @param sibpos (`integer(1)`)\cr internal detail, do not set manually. |
|
20 |
#' @param nsibs (`integer(1)`)\cr internal detail, do not set manually. |
|
21 |
#' @param rownum (`numeric(1)`)\cr internal detail, do not set manually. |
|
22 |
#' @param indent (`integer(1)`)\cr internal detail, do not set manually. |
|
23 |
#' @param colwidths (`numeric`)\cr internal detail, do not set manually. |
|
24 |
#' @param path (`character`)\cr path to the (sub)table represented by `tt`. Defaults to `character()`. |
|
25 |
#' @param max_width (`numeric(1)` or `NULL`)\cr maximum width for title/footer materials. |
|
26 |
#' @param col_gap (`numeric(1)`)\cr the gap to be assumed between columns, in number of spaces with |
|
27 |
#' font specified by `fontspec`. |
|
28 |
#' |
|
29 |
#' @import methods |
|
30 |
#' @include matrix_form.R |
|
31 |
#' |
|
32 |
#' @details |
|
33 |
#' When `visible_only` is `TRUE` (the default), methods should return a `data.frame` with exactly one |
|
34 |
#' row per visible row in the table-like object. This is useful when reasoning about how a table will |
|
35 |
#' print, but does not reflect the full pathing space of the structure (though the paths which are given |
|
36 |
#' will all work as is). |
|
37 |
#' |
|
38 |
#' If supported, when `visible_only` is `FALSE`, every structural element of the table (in row-space) |
|
39 |
#' will be reflected in the returned `data.frame`, meaning the full pathing-space will be represented |
|
40 |
#' but some rows in the layout summary will not represent printed rows in the table as it is displayed. |
|
41 |
#' |
|
42 |
#' Most arguments beyond `tt` and `visible_only` are present so that `make_row_df` methods can call |
|
43 |
#' `make_row_df` recursively and retain information, and should not be set during a top-level call. |
|
44 |
#' |
|
45 |
#' @return A `data.frame` of row/column-structure information used by the pagination machinery. |
|
46 |
#' |
|
47 |
#' @note The technically present root tree node is excluded from the summary returned by |
|
48 |
#' both `make_row_df` and `make_col_df` (see relevant functions in`rtables`), as it is the |
|
49 |
#' row/column structure of `tt` and thus not useful for pathing or pagination. |
|
50 |
#' |
|
51 |
#' @examples |
|
52 |
#' # Expected error with matrix_form. For real case examples consult {rtables} documentation |
|
53 |
#' mf <- basic_matrix_form(iris) |
|
54 |
#' # make_row_df(mf) # Use table obj instead |
|
55 |
#' |
|
56 |
#' @export |
|
57 |
#' @name make_row_df |
|
58 |
setGeneric("make_row_df", function(tt, colwidths = NULL, visible_only = TRUE, |
|
59 |
rownum = 0, |
|
60 |
indent = 0L, |
|
61 |
path = character(), |
|
62 |
incontent = FALSE, |
|
63 |
repr_ext = 0L, |
|
64 |
repr_inds = integer(), |
|