-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
render.R
269 lines (247 loc) · 11.9 KB
/
render.R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
#' Render multiple R Markdown documents into a book
#'
#' Render multiple R Markdown files under the current working directory into a
#' book. It can be used in the RStudio IDE (specifically, the \code{knit} field
#' in YAML). The \code{preview_chapter()} function is a wrapper of
#' \code{render_book(preview = TRUE)}.
#'
#' There are two ways to render a book from Rmd files. The default way
#' (\code{new_session = FALSE}) is to merge Rmd files into a single file and
#' render this file. You can also choose to render each individual Rmd file in a
#' new R session (\code{new_session = TRUE}).
#' @param input A directory, an input filename or multiple filenames. For a
#' directory, \file{index.Rmd} will be used if it exists in this (book)
#' project directory. For filenames, if \code{preview = TRUE}, only files
#' specified in this argument are rendered, otherwise all R Markdown files
#' specified by the book are rendered.
#' @param output_format,...,clean,envir Arguments to be passed to
#' \code{rmarkdown::\link[rmarkdown]{render}()}. For \code{preview_chapter()},
#' \code{...} is passed to \code{render_book()}. See
#' \code{rmarkdown::\link[rmarkdown]{render}()}
#' and \href{https://bookdown.org/yihui/bookdown/build-the-book.html}{the
#' bookdown reference book} for details on how output formatting options are
#' set from YAML or parameters supplied by the user when calling
#' \code{render_book()}.
#' @param clean_envir This argument has been deprecated and will be removed in
#' future versions of \pkg{bookdown}.
#' @param output_dir The output directory. If \code{NULL}, a field named
#' \code{output_dir} in the configuration file \file{_bookdown.yml} will be
#' used (possibly not specified, either, in which case a directory name
#' \file{_book} will be used).
#' @param new_session Whether to use new R sessions to compile individual Rmd
#' files (if not provided, the value of the \code{new_session} option in
#' \file{_bookdown.yml} is used; if this is also not provided,
#' \code{new_session = FALSE}).
#' @param preview Whether to render and preview the input files specified by the
#' \code{input} argument. Previewing a certain chapter may save compilation
#' time as you actively work on this chapter, but the output may not be
#' accurate (e.g. cross-references to other chapters will not work).
#' @param config_file The book configuration file.
#' @export
#' @examples
#' # see https://bookdown.org/yihui/bookdown for the full documentation
#' if (file.exists('index.Rmd')) bookdown::render_book('index.Rmd')
#' \dontrun{
#' # will use the default format defined in index.Rmd or _output.yml
#' bookdown::render_book("index.Rmd")
#' # will use the options for format defined in YAML metadata
#' bookdown::render_book("index.Rmd", "bookdown::pdf_book")
#' # If you pass an output format object, it must have all the options set
#' bookdown::render_book("index.Rmd", bookdown::pdf_book(toc = FALSE))
#'
#' # will render the book in the current directory
#' bookdown::render_book()
#' # this is equivalent to
#' bookdown::render_book("index.Rmd")
#' # will render the book living in the specified directory
#' bookdown::render_book("my_book_project")
#' }
render_book = function(
input = ".", output_format = NULL, ..., clean = TRUE, envir = parent.frame(),
clean_envir = !interactive(), output_dir = NULL, new_session = NA,
preview = FALSE, config_file = '_bookdown.yml'
) {
verify_rstudio_version()
# select and check input file(s)
if (length(input) == 1L && dir_exists(input)) {
message(sprintf("Rendering book in directory '%s'", input))
owd = setwd(input); on.exit(setwd(owd), add = TRUE)
# if a directory is passed, we assume that index.Rmd exists
input = get_index_file()
# No input file to use as fallback
if (is_empty(input)) input = NULL
} else {
stop_if_not_exists(input)
}
format = NULL # latex or html
if (is.list(output_format)) {
format = output_format$bookdown_output_format
if (!is.character(format) || !(format %in% c('latex', 'html'))) format = NULL
} else if (is.null(output_format) || is.character(output_format)) {
if (is.null(output_format) || identical(output_format, 'all')) {
# formats can safely be guess when considering index.Rmd and its expected frontmatter
# As a fallback we assumes input could have the YAML, otherwise we just use gitbook();
# Also, when no format provided, return name of the first resolved
output_format = get_output_formats(
fallback_format = "bookdown::gitbook",
first = is.null(output_format),
fallback_index = input
)
}
if (length(output_format) > 1) return(unlist(lapply(output_format, function(fmt)
xfun::Rscript_call(render_book, list(
input, fmt, ..., clean = clean, envir = envir, output_dir = output_dir,
new_session = new_session, preview = preview, config_file = config_file
), fail = c("bookdown::render_book() failed to render the output format '", fmt, "'."))
)))
format = target_format(output_format)
}
if (!missing(clean_envir)) warning(
"The argument 'clean_envir' has been deprecated and will be removed in future ",
"versions of bookdown."
)
on.exit(opts$restore(), add = TRUE)
opts$set(config_file = config_file)
config = load_config() # configurations in _bookdown.yml
output_dir = output_dirname(output_dir, config)
on.exit(xfun::del_empty_dir(output_dir), add = TRUE)
if (!preview) unlink(ref_keys_path(output_dir)) # clean up reference-keys.txt
# store output directory and the initial input Rmd name
opts$set(
output_dir = output_dir,
input_rmd = xfun::relative_path(input),
preview = preview
)
aux_diro = '_bookdown_files'
# move _files and _cache from _bookdown_files to ./, then from ./ to _bookdown_files
aux_dirs = files_cache_dirs(aux_diro)
move_dirs(aux_dirs, basename(aux_dirs))
on.exit({
aux_dirs = files_cache_dirs('.')
if (length(aux_dirs)) {
dir_create(aux_diro)
move_dirs(aux_dirs, file.path(aux_diro, basename(aux_dirs)))
}
}, add = TRUE)
# you may set, e.g., new_session: yes in _bookdown.yml
if (is.na(new_session)) {
new_session = FALSE
if (is.logical(config[['new_session']])) new_session = config[['new_session']]
}
main = book_filename()
if (!grepl('[.][Rr]?md$', main)) main = paste0(main, if (new_session) '.md' else '.Rmd')
delete_main = config[['delete_merged_file']]
check_main = function() file.exists(main) && is.null(delete_main)
if (check_main()) stop(
'The file ', main, ' exists. Please delete it if it was automatically generated. ',
'If you are sure it can be safely overwritten or deleted, please set the option ',
"'delete_merged_file' to true in _bookdown.yml."
)
on.exit(if (check_main()) {
message('Please delete ', main, ' after you finish debugging the error.')
}, add = TRUE)
opts$set(book_filename = main) # store the book filename
files = source_files(format, config)
if (length(files) == 0) stop(
'No input R Markdown files found from the current directory ', getwd(),
' or in the rmd_files field of _bookdown.yml'
)
if (new_session && any(dirname(files) != '.')) stop(
'With new_session = TRUE, all input files must be under the root directory ',
'of the (book) project. You might have used `rmd_files` or `rmd_subdir` to ',
'specify input files from subdirectories, which will not work with `new_session`.'
)
res = if (new_session) {
render_new_session(files, main, config, output_format, clean, envir, ...)
} else {
render_cur_session(files, main, config, output_format, clean, envir, ...)
}
if (!isFALSE(delete_main)) file.remove(main)
res
}
#' @rdname render_book
#' @export
preview_chapter = function(..., envir = parent.frame()) {
render_book(..., envir = envir, preview = TRUE)
}
render_cur_session = function(files, main, config, output_format, clean, envir, ...) {
merge_chapters(
files, main,
insert_chapter_script(config, 'before'),
insert_chapter_script(config, 'after')
)
rmarkdown::render(main, output_format, ..., clean = clean, envir = envir)
}
render_new_session = function(files, main, config, output_format, clean, envir, ...) {
# save a copy of render arguments in a temp file
render_args = tempfile('render', '.', '.rds')
on.exit(file.remove(render_args), add = TRUE)
saveRDS(
list(output_format = output_format, ..., clean = FALSE, envir = envir),
render_args
)
# an RDS file to save all the metadata after compiling each Rmd
render_meta = with_ext(main, '.rds')
files_md = output_path(with_ext(files, '.md'))
# copy pure Markdown input files to output directory; no need to render() them
for (i in which(grepl('[.]md$', files) & files != files_md))
file.copy(files[i], files_md[i], overwrite = TRUE)
# if input is index.Rmd or not preview mode, compile all Rmd's
rerun = !opts$get('preview') || opts$get('input_rmd') %in% get_index_file()
if (!rerun) rerun = files %in% opts$get('input_rmd')
add1 = merge_chapter_script(config, 'before')
add2 = merge_chapter_script(config, 'after')
on.exit(unlink(c(add1, add2)), add = TRUE)
# compile chapters in separate R sessions
for (f in files[rerun]) Rscript_render(f, render_args, render_meta, add1, add2)
if (!all(dirname(files_md) == '.'))
file.copy(files_md[!rerun], basename(files_md[!rerun]), overwrite = TRUE)
meta = clean_meta(render_meta, files)
move = !(unlist(meta) %in% files) # do not move input files to output dir
on.exit(file.rename(unlist(meta)[move], files_md[move]), add = TRUE)
merge_chapters(unlist(meta), main, orig = files)
knit_meta = unlist(lapply(meta, attr, 'knit_meta', exact = TRUE), recursive = FALSE)
intermediates = unlist(lapply(meta, attr, 'intermediates', exact = TRUE))
if (clean) on.exit(unlink(intermediates, recursive = TRUE), add = TRUE)
rmarkdown::render(
main, output_format, ..., clean = clean, envir = envir,
run_pandoc = TRUE, knit_meta = knit_meta
)
}
#' Clean up the output files and directories from the book
#'
#' After a book is rendered, there will be a series of output files and
#' directories created in the book root directory, typically including
#' \file{*_files/}, \file{*_cache/}, \file{_book/}, and some HTML/LaTeX
#' auxiliary files. These filenames depend on the book configurations. This
#' function identifies these files and directories, and delete them if desired,
#' so you can rebuild the book with a clean source.
#' @param clean Whether to delete the possible output files. If \code{FALSE},
#' simply print out a list of files/directories that should probably be
#' deleted. You can set the global option \code{bookdown.clean_book = TRUE} to
#' force this function to delete files. You are recommended to take a look at
#' the list of files at least once before actually deleting them, i.e. run
#' \code{clean_book(FALSE)} before \code{clean_book(TRUE)}.
#' @export
clean_book = function(clean = getOption('bookdown.clean_book', FALSE)) {
r = '_(files|cache)$'
one = with_ext(book_filename(), '') # the main book file
src = with_ext(source_files(all = TRUE), '') # input documents
out = list.files('.', r)
out = out[dir_exists(out)]
out = out[gsub(r, '', out) %in% c(src, one)] # output dirs generated from src names
out = c(out, output_dirname(NULL, create = FALSE)) # output directory
out = c(out, with_ext(one, c('bbl', 'html', 'tex', 'rds'))) # aux files for main file
out = c(out, load_config()[['clean']]) # extra files specified in _bookdown.yml
out = sort(unique(out))
if (length(out) == 0) return(invisible())
if (clean) unlink(out, recursive = TRUE) else {
out = out[file.access(out) == 0]
if (length(out) == 0) return(invisible())
message(
'These files/dirs can probably be removed: \n\n', paste(mark_dirs(out), collapse = '\n'),
'\n\nYou can set options(bookdown.clean_book = TRUE) to allow this function to always clean up the book directory for you.'
)
invisible(out)
}
}