pkgload::load_all()
library(testthat)
library(stringr)
library(purrr)
library(usethis)
library(xfun)
library(rstudioapi)
# Add license
use_gpl3_license(name = "Marion Louveaux")

# Create inst directory and copy bookdown template in inst
dir_book <- "inst/skeleton"

if (!dir.exists(paths = here::here(dir_book))) {
  dir.create(path = here::here(dir_book), recursive = TRUE)
}
bookdown:::bookdown_skeleton(here::here(dir_book))

# Hide dev folder from build
usethis::use_build_ignore("dev/")

# git
usethis::use_git()

# description
usethis::use_description(
  fields = list(
    Title = "Prepares a bookdown project from a skeleton and a template",
    `Authors@R` = 'person("Marion", "Louveaux", email = "marion.louveaux@gmail.com",
                          role = c("aut", "cre"),
                          comment = c(ORCID = "0000-0002-1794-3748"))',
    Description = "Prepares a bookdown project from a customizable bookdown skeleton and, if needed, a template for the chapters. Helps creating reproducible yet flexible bookdown projects, for instance to deliver data analyses to customers following a standardized format.",
    License = "GPL-3",
    URL = "https://github.com/marionlouveaux/bookprep",
    BugReports = "https://github.com/marionlouveaux/bookprep/issues"
  )
)

# Amend description import and suggests
attachment::att_amend_desc()



# readme
usethis::use_readme_rmd()

# inflate
fusen::inflate(
  rmd = "dev/devstuff_history.Rmd",
  name = "Getting started", check = FALSE
)

usethis::use_github_action_check_standard()

usethis::use_github_action("pkgdown")
usethis::use_github_action("test-coverage")

usethis::use_coverage()
usethis::use_code_of_conduct()

Bookdown skeleton creation

The function prepare_book() requires a {bookdown} skeleton and a template. The {bookdown} skeleton contains only the following files: "_bookdown.yml", "_output.yml", "index.Rmd", "preamble.tex", "README.md", and "style.css". These files condition the architecture (the skeleton) of the future book, i.e. how it will be printed/rendered, its css style, link to bibliography and information about author, title and creation date. By default, prepare_book() uses the skeleton stored in /book_skeleton in the {bookprep} package. You can use your own skeleton if you prefer.

The {bookdown} skeleton created for the {bookprep} package was created by cleaning info specific to the {bookdown} package and replacing them by generic variable names:
- Index.Rmd: create variable book_title, author_name, creation_date (default to Sys.Date()), short_description, index_title and index_content
- _bookdown.yml: reuse book_title variable
- _output.yml: reuse book_title variable, add info that this book was published with {bookprep}
- README.md: reuse variable book_title and specifies that this book was published with {bookprep}

Template creation

The template contains Rmd files corresponding to the named chapters, an index.md file containing the content of the future index and, if needed, an extra .css file and an extra .bib file. Named chapters and index.md can include variables. These variables can be replaced with custom content using the replacements parameter of prepare_book().

An example of template is included in {bookprep} in /book_template_example. You can also provide your own template, or create a minimal one using the function initialize_template().

#' Find and replace all variables in one of the files of the book skeleton
#'
#' @param file path of a file from skeleton: _bookdown.yml, _output.yml, index.Rmd, README.md
#' @param replacements a vector of correspondences between an input and an output. Input is fixed and corresponds to: book_title, author_name, creation_date, short_description and index_title
#' @importFrom stringr str_replace_all
#'
#' @return path of the modified file
#'
#' @examples
find_and_replace <- function(file, replacements = c(
                               "book_title" = "My book",
                               "author_name" = "Marion Louveaux",
                               "creation_date" = "`r Sys.Date()`",
                               "short_description" = "An example of book with {bookprep}.",
                               "index_title" = "Context"
                             )) {
  file <- normalizePath(file, mustWork = FALSE)
  if (!file.exists(file)) {
    stop("The specified file does not exist.")
  }

  original_text <- readLines(con = file)
  modified_text <- str_replace_all(
    string = original_text,
    replacements
  )

  writeLines(enc2utf8(modified_text), con = file)
  file
}
# copy to a temporary file to avoid overwriting the package
index_tmp <- tempfile(pattern = "index-")

file.copy(from = system.file("book_skeleton/index.Rmd", package = "bookprep"), to = index_tmp)

index_modified <- find_and_replace(
  file = index_tmp,
  replacements = c(
    "book_title" = "My book",
    "author_name" = "Marion Louveaux",
    "creation_date" = "`r Sys.Date()`",
    "short_description" = "An example of book with {bookprep}.",
    "index_title" = "Context"
  )
)

index_lines <- readLines(index_modified)
test_that("find_and_replace works", {
  expect_equal(index_lines[2], 'title: \"My book\"')
  expect_equal(index_lines[3], 'author: "Marion Louveaux"')
})


test_that("find_and_replace gives error", {
  expect_error(
    object = find_and_replace(
      file = "non_existing_path",
      replacements = c(
        "book_title" = "My book",
        "author_name" = "Marion Louveaux",
        "creation_date" = "`r Sys.Date()`",
        "short_description" = "An example of book with {bookprep}.",
        "index_title" = "Context"
      )
    ), "The specified file does not exist."
  )
})
#' Modify all variables in files from the book skeleton
#'
#' @param path path to directory (skeleton) containing all files to modify
#' @param pattern regex to identify files to prepare (by default, all Rmd, yml and md files)
#' @inheritParams find_and_replace
#' @importFrom purrr map
#'
#' @return path from each modified file
#'
#' @examples
#'
prepare_files <- function(path, replacements = c(
                            "book_title" = "My book",
                            "author_name" = "Marion Louveaux",
                            "creation_date" = "`r Sys.Date()`",
                            "short_description" = "An example of book with {bookprep}.",
                            "index_title" = "Context"
                          ),
                          pattern = "[.]Rmd$|[.]yml$|[.]md$") {
  all_files_in_path <- list.files(path, full.names = TRUE, recursive = TRUE)
  files_to_modify <- grep(
    pattern = pattern,
    all_files_in_path
  )
  map(
    all_files_in_path[files_to_modify],
    ~ find_and_replace(file = .x, replacements = replacements)
  )
  all_files_in_path[files_to_modify]
}
# Create temporary directory for reproducible example
dir_tmp <- tempfile(pattern = "proj-")
dir.create(dir_tmp)
# browseURL(dir_tmp)

# prepare_files() requires a directory with some files inside to look for replacements
file.copy(from = system.file("book_skeleton", package = "bookprep"), to = dir_tmp, recursive = TRUE)

modified_files <- prepare_files(
  path = dir_tmp,
  replacements = c(
    "book_title" = "My book",
    "author_name" = "Marion Louveaux",
    "creation_date" = "`r Sys.Date()`",
    "short_description" = "An example of book with {bookprep}.",
    "index_title" = "Context"
  )
)


test_that("prepare_files works", {
  # index modified
  index_lines <- modified_files[grep(pattern = "index[.]Rmd", x = modified_files)]
  index_content <- readLines(index_lines)
  expect_equal(index_content[3], 'author: "Marion Louveaux"')

  # _output.yml modified
  output_lines <- modified_files[grep(pattern = "_output[.]yml", x = modified_files)]
  output_content <- readLines(output_lines)
  expect_equal(output_content[6], '        <li><a href=\"./\">My book</a></li>')
})

Prepare a custom bookdown from a skeleton and a template

#' Prepare customised bookdown from skeleton and template
#' This function creates an Rstudio project in path, sets build type to "website", copies the content of book_skeleton at the root of the project, copies all files from the book template, modifies pre-defined variable book_title, author_name, creation_date, as well as any variable added by the user in replacements (index_title, index_content...), and inserts the content of index.txt in index.Rmd (here containing the user defined variables index_title and index_content).
#'
#' @param path where to create the bookdown
#' @param skeleton path to directory containing bookdown skeleton
#' @param template path to directory containing template files for chapters and index content
#' @inheritParams find_and_replace
#' @inheritParams prepare_files
#' @importFrom usethis create_project
#' @importFrom stringr str_replace
#'
#' @return used for side effect: file creation in path
#' @export
#'
#' @examples
prepare_book <- function(
                         path,
                         replacements = c(
                           "book_title" = "My book",
                           "author_name" = "Marion Louveaux",
                           "creation_date" = "`r Sys.Date()`",
                           "short_description" = "An example of book with {bookprep}.",
                           "index_title" = "Context"
                         ),
                         skeleton = system.file("book_skeleton", package = "bookprep"),
                         template = system.file("book_template_example", package = "bookprep"),
                         pattern = "[.]Rmd$|[.]yml$|[.]md$") {
  # Create project
  path <- normalizePath(path)
  create_project(path = path, open = FALSE)

  # Modify buildType: "BuildType: Package" to "BuildType: Website"
  rproj_path <- list.files(path, pattern = "[.]Rproj$", full.names = TRUE)
  if (length(rproj_path) != 0) { # rstudioapi available
    proj_lines <- readLines(rproj_path[1])
    proj_lines_modified <- str_replace(
      string = proj_lines,
      pattern = "BuildType: Package",
      replacement = "BuildType: Website"
    )
    writeLines(text = proj_lines_modified, con = list.files(path, pattern = "[.]Rproj$", full.names = TRUE))
  }

  # Add all files from book skeleton
  file.copy(from = list.files(skeleton, full.names = TRUE), to = path, recursive = TRUE)

  # Add all files from book template (Rmd, css...) except index
  if (!dir.exists(template)) { # by default use the template of the package
    stop(template, " directory does not exist.") # means that user provided a incorrect path to a custom template
  }
  all_files_wo_index <- list.files(normalizePath(template), full.names = TRUE)
  all_files_wo_index <- all_files_wo_index[!grepl("^index", basename(all_files_wo_index))]
  file.copy(from = all_files_wo_index, to = path, recursive = TRUE)

  # Detect css files and modify _output.yml
  extra_css <- basename(all_files_wo_index)[grepl("[.]css$", basename(all_files_wo_index))]
  if (length(extra_css) != 0) {
    output_lines <- readLines(file.path(path, "_output.yml"))
    output_modified <- str_replace(
      string = output_lines,
      pattern = "css: style.css",
      replacement = paste0(
        "css: [style.css, ",
        paste(extra_css, collapse = ","), "]"
      )
    )
    writeLines(text = output_modified, con = file.path(path, "_output.yml"))
  }

  # Detect bib files and modify index.Rmd
  extra_bib <- basename(all_files_wo_index)[grepl("[.]bib$", basename(all_files_wo_index))]
  if (length(extra_bib) != 0) {
    index_lines <- readLines(file.path(path, "index.Rmd"))
    index_modified <- str_replace(
      string = index_lines,
      pattern = "bibliography: \\[book[.]bib\\]$",
      replacement = paste0(
        "bibliography: [book.bib, ",
        paste(extra_bib, collapse = ","), "]"
      )
    )
    writeLines(text = index_modified, con = file.path(path, "index.Rmd"))
  }


  # Copy content from index.md or index.txt
  # NB: this content can contain variables such as index_title and index_content
  index_txt <- grep(
    pattern = "index[.](txt|md)$",
    list.files(template)
  )
  index_content_from_txt <- readLines(list.files(template, full.names = TRUE)[index_txt])
  index_rmd_path <- list.files(path, pattern = "^index[.]Rmd$", full.names = TRUE)

  # Concatenate and write current index.Rmd with content from index.md or index.txt
  new_index_content <- c(
    readLines(index_rmd_path),
    index_content_from_txt
  )
  writeLines(enc2utf8(new_index_content), con = index_rmd_path)

  # Modify files for replacements
  prepare_files(
    path = path,
    replacements = replacements,
    pattern = pattern
  )
}
# Create temporary directory for reproducible example
dir_tmp <- tempfile(pattern = "proj-")
dir.create(dir_tmp)
# browseURL(dir_tmp)

prepare_book(
  path = dir_tmp,
  template = system.file("book_template_example", package = "bookprep"),
  replacements = c(
    # replacements mandatory for the bookdown skeleton
    "book_title" = "My book",
    "author_name" = "Marion Louveaux",
    "creation_date" = "`r Sys.Date()`",
    "short_description" = "An example of book with {bookprep}.",
    # replacement specific to my template that you can add if you want
    "index_title" = "Context"
  )
)
# Create temporary directory for reproducible example
dir_tmp <- tempfile(pattern = "proj-")
dir.create(dir_tmp)
# browseURL(dir_tmp)


withr::with_dir(
  dir_tmp,
  prepare_book(
    path = dir_tmp,
    template = system.file("book_template_example", package = "bookprep"),
    replacements = c(
      # replacements mandatory for the bookdown skeleton
      "book_title" = "My book",
      "author_name" = "Marion Louveaux",
      "creation_date" = "`r Sys.Date()`",
      "short_description" = "An example of book with {bookprep}.",
      # replacement specific to my template (variable in index.md)
      "index_title" = "Context"
    )
  )
)


test_that("prepare_book works", {
  expect_true(file.exists(file.path(dir_tmp, "index.Rmd")))

  # checks that Rproj exists
  rproj_path <- list.files(dir_tmp, pattern = "[.]Rproj$", full.names = TRUE)
  if (length(rproj_path) != 0) {
    expect_length(rproj_path, 1)
    # Checks that build type is website in Rproj
    rproj_content <- readLines(rproj_path)
    expect_equal(rproj_content[14], "BuildType: Website")
  }
  # Checks that there is an extra.css file in the book template example
  css_path <- list.files(system.file("book_template_example", package = "bookprep"),
    pattern = "extra[.]css$"
  )
  expect_length(css_path, 1)
  # Checks that the extra.css is in the _output.yml
  outputyml_path <- list.files(dir_tmp, pattern = "_output[.]yml$", full.names = TRUE)
  outputyml_content <- readLines(outputyml_path)
  expect_length(grep(outputyml_content[2], pattern = "extra[.]css"), 1)


  # Checks that there is an extra.bib file in the book template example
  bib_path <- list.files(system.file("book_template_example", package = "bookprep"),
    pattern = "extra[.]bib$"
  )
  expect_length(bib_path, 1)
  # Checks that the extra.bib is in the index.Rmd
  index_path <- list.files(dir_tmp, pattern = "index[.]Rmd$", full.names = TRUE)
  index_content <- readLines(index_path)
  expect_length(grep(index_content[7], pattern = "extra[.]bib"), 1)


  # Checks that content of index.md has been copied in index.Rmd and index_title replaced

  index_path <- list.files(dir_tmp, pattern = "index[.]Rmd$", full.names = TRUE)
  index_content <- readLines(index_path)
  expect_equal(index_content[12], paste0("# ", "Context"))
  expect_equal(index_content[13], "")
  expect_equal(index_content[14], "Write here what you want to see in the <span class=\"red\">index</span>")
})


test_that("initialize_template gives error", {
  expect_error(
    object = prepare_book(
      path = dir_tmp,
      template = "non_existing_path"
    ),
    ".* directory does not exist."
  )
})

Initialize a template

As stated above, the template contains named chapters (Rmd files), an index.md file containing the level 1 title and content of the future index and, if needed, an extra .css file and an extra .bib file. The function initialize_template() helps creating the named chapters and adding (or not) a reference chapter. You can then complete this template manually by writing custom variables in the chapter files (these will be replaced by prepare_book()) and adding .css and .bib files to the template folder.

#' Initialize a book template with named chapters and base content
#'
#' @param path where to create the template
#' @param chapters titles of Rmd files to include in the template
#' @param references title of the references Rmd or NULL
#' @importFrom xfun write_utf8
#'
#' @return used for side effect: file creation in path
#' @export
#'
#' @examples
initialize_template <- function(path, chapters = c("Preface", "Introduction"),
                                references = "References") {
  rmd_tmp <- c(chapters, references)
  rmd_files <- sprintf("%02d-%s.Rmd", seq_along(rmd_tmp) -
    1, rmd_tmp)

  titles <- c(chapters, sprintf(
    "`r if (knitr::is_html_output()) '# %s {-}'`",
    references
  ))
  titles_first_level <- paste("#", titles)

  for (i in seq_along(rmd_files)) {
    file_tmp <- rmd_files[i]
    if (file.exists(file.path(path, file_tmp))) {
      stop("The file ", file_tmp, " exists.")
    } else {
      write_utf8(titles_first_level[i], con = file.path(path, file_tmp))
    }
  }
}
# Create temporary directory for reproducible example
dir_tmp <- tempfile(pattern = "proj-")
dir.create(dir_tmp)
# browseURL(dir_tmp)

initialize_template(
  path = dir_tmp,
  chapters = c(
    "Introduction", "Material and Methods",
    "Results", "Discussion"
  ),
  references = NULL
)
# Create temporary directory for reproducible example
dir_tmp <- tempfile(pattern = "proj-")
dir.create(dir_tmp)
# browseURL(dir_tmp)

initialize_template(
  path = dir_tmp,
  chapters = c(
    "Introduction", "Material and Methods",
    "Results", "Discussion"
  ),
  references = NULL
)

test_that("initialize_template works", {
  expect_true(file.exists(file.path(dir_tmp, "00-Introduction.Rmd")))
  expect_length(list.files(dir_tmp), 4)
  results_content <- readLines(file.path(dir_tmp, "02-Results.Rmd"))
  expect_equal(results_content[1], paste0("# ", "Results"))
})



# Create temporary directory for reproducible example
dir_tmp <- tempfile(pattern = "proj-")
dir.create(dir_tmp)
# browseURL(dir_tmp)
file.create(file.path(dir_tmp, "00-Preface.Rmd"))

test_that("initialize_template gives error", {
  expect_error(
    object = initialize_template(
      path = dir_tmp,
      chapters = c("Preface", "Introduction"),
      references = "References"
    ),
    "The file .* exists."
  )
})


marionlouveaux/bookprep documentation built on Dec. 21, 2021, 2:45 p.m.