Defines functions clean_book render_new_session render_cur_session preview_chapter render_book

Documented in clean_book preview_chapter render_book

#' 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{render}()}. For \code{preview_chapter()}, \code{...}
#'   is passed to \code{render_book()}. See \code{rmarkdown::\link{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'
) {


  # 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 {

  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."

  if (config_file != '_bookdown.yml') {
    unlink(tmp_config <- tempfile('_bookdown_', '.', '.yml'))
    if (file.exists('_bookdown.yml')) file.rename('_bookdown.yml', tmp_config)
    file.rename(config_file, '_bookdown.yml')
      file.rename('_bookdown.yml', config_file)
      if (file.exists(tmp_config)) file.rename(tmp_config, '_bookdown.yml')
    }, add = TRUE)

  on.exit(opts$restore(), add = TRUE)
  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
    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))
    aux_dirs = files_cache_dirs('.')
    if (length(aux_dirs)) {
      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)

#' @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, ...) {
    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)
    list(output_format = output_format, ..., clean = FALSE, envir = envir),
  # 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)

    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())
      '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.'

Try the bookdown package in your browser

Any scripts or data that you put into this service are public.

bookdown documentation built on March 7, 2023, 5:38 p.m.