Using Gitbook with R Markdown

[This article was first published on Jason.Bryer.org Blog - R, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Gitbook has been getting some (deserved) attention. For those who haven’t seen it, Gitbook is a system to create really beautiful interactive web (or PDF and ebook) books. For me, the timing of discovering this framework could not be better as I am preparing documentation for propensity score analysis for an upcoming workshop I am giving. Of course, I want to use it to write about R and would prefer to use R markdown instead of just plain markdown. I have written a number of R functions to combine the functionality of knitr and Gitbook. The source code is on Gist or can be sourced directly using the devtools package.

> devtools::source_gist(11049319)

The first step is to create a new Gitbook. The newGitbook will create a new directory and create four files.

  • .bookignore – This file is formatted like any .gitignore file, but is specifically for Gitbook. That is, list files here that you want Gitbook to ignore, but want Git to manage. This defaults to include .Rmd, .md, .R, and .Rproj files as well as the log directory (this will be discussed below).
  • .gitignore – This defaults to include the _book directory. This is the default location for the built book and it is likely you do not want to include it in your master branch. As we will see below, this directory will be published to the gh-pages branch.
  • README.md – Introduction to your Gitbook.
  • SUMMARY.md – Defines the structure (i.e. table of contents) of your Gitbook.

You can get more information about how Gitbook is organized at their webiste gitbook.io.

> newGitbook('~/Dropbox/Projects/testbook')
Creating /Users/jbryer/Dropbox/Projects/testbook
Writing .bookignore...
Writing .gitignore...
Writing README.md...
Writing SUMMARY.md...
You can now open README.md and SUMMARY.md. Once you are done 
editting SUMMARY.md, initGitbook() will create the file and folder 
structure for your new Gitbook.

I should note that the newGitbook will change the working directory to the location of your new Gitbook.

> getwd()
[1] "/Users/jbryer/Dropbox/Projects/testbook"

And we can see the four files it created.

Jasons-MacBook-Air:testbook jbryer$ ls -la
total 32
drwxr-xr-x    6 jbryer  staff   204 Apr 18 10:52 .
drwxr-xr-x  110 jbryer  staff  3740 Apr 18 10:52 ..
-rw-r--r--    1 jbryer  staff    35 Apr 18 10:52 .bookignore
-rw-r--r--    1 jbryer  staff    49 Apr 18 10:52 .gitignore
-rw-r--r--    1 jbryer  staff    75 Apr 18 10:52 README.md
-rw-r--r--    1 jbryer  staff   231 Apr 18 10:52 SUMMARY.md

At this stage you can open SUMMARY.md and change the outline of your book. Once you are done, the initGitbook function will create the files and folders for your book. Unlike the Gitbook command line, this function will change the file extensions of all your files to .Rmd (excluding README.md and SUMMARY.md) even though you specify .md in the links in that file. The buildRmd function discussed below will convert those .Rmd files to .md.

> initGitbook()

Files

The buildRmd function will convert all .Rmd files in your project to .md using the knitr package. It should be noted that this function will create a file, .rmdbuild.Rda, in your working directory. This is an R data file that saves the status of the last build. This allows this function to only build R markdown files that have changed since the last build and therefore, increase the execution time. By default, all the knitr messages will be printed to the console. If you specify log.dir parameter, then all the output will be saved to log files in that given directory (one log file per Rmd file). There is also a clean parameter that will build all R markdown files regardless of their modification timestamp.

> buildRmd()

The buildGitbook function is simply a wrapper to the Gitbook command line and will generate your book from the markdown sources. As of this writing, there is a bug in Gitbook where the image URLS are incorrect. This function will fix the URLs. There is also another issue that links to the Introduction point to / and not /index.html. These will also be fixed.

> buildGitbook()

The openGitbook will open your built book using your system’s default web browser.

> openGitbook()

Lastly, the publishGitbook will publish your built Gitbook to the gh-pages branch of the specified Github repository. Special thanks to Ramnath Vaidyanathan who provided the shell script to do this. Take a look at the link as you can optionally save this as a Git hook to publish atomically after checking in code to the master branch.

> publishGitbook(repo='jbryer/testbook')

Please leave comments below or suggestions to make this better. I would also be interested if there is interest in rolling this up into an R package.


Here is the source code:

require(knitr)
#' Initializes a new Gitbook.
#'
#' This will initalize a new Gitbook in the given directory. When done, it will
#' also change the working directory.
#'
#' @author Jason Bryer <jason@bryer.org>
newGitbook <- function(dir) {
.Deprecated('This function has been moved to the gitbook R package. See http://jason.bryer.org/Rgitbook for more information')
# TODO: May want to make these parameters or options
bookignore <- c('*.RMD','*.rmd','*.Rmd','log/','*.R','*.Rproj')
gitignore <- c('.Rproj.user','_book/','.rmdbuild.Rda','*.DS_Store','log/','.Rhistory')
summary.md <- c("# Summary","This is the summary of my book.",
"",
"* [section 1](section1/README.md)",
" * [example 1](section1/example1.md)",
" * [example 2](section1/example2.md)",
"* [section 2](section2/README.md)",
" * [example 1](section2/example1.md)")
readme.md <- c("# Book Title",
"#### by Your Name",
"",
"Replace with an introduction of your book.")
if(missing(dir)) { stop('dir parameter is required.') }
if(system('npm', ignore.stdout=TRUE) != 0) {
stop("Cannot find node.js. You can install it from http://nodejs.org/download/")
}
if(system('gitbook', ignore.stdout=TRUE) != 0) {
message("Installing gitbook...")
test <- system('npm install gitbook -g')
if(test != 0) { stop("gitbook installation failed.") }
}
dir <- path.expand(dir)
message(paste0('Creating ', dir))
dir.create(dir, recursive=TRUE, showWarnings=FALSE)
olddir <- setwd(dir)
message('Writing .bookignore...')
f <- file('.bookignore')
writeLines(bookignore, f)
close(f)
message('Writing .gitignore...')
f <- file('.gitignore')
writeLines(gitignore, f)
close(f)
message('Writing README.md...')
f <- file('README.md')
writeLines(readme.md, f)
close(f)
message('Writing SUMMARY.md...')
f <- file('SUMMARY.md')
writeLines(summary.md, f)
close(f)
message(
'You can now open README.md and SUMMARY.md. Once you are done
editting SUMMARY.md, initGitbook() will create the file and folder
structure for your new Gitbook.')
}
#' Create files and folders based on contents of SUMMARY.md.
#'
#' This first calls system command \code{gitbook init} but then will change
#' the all the file extensions from \code{.md} to \code{.Rmd} excluding
#' \code{SUMMARY.md} and \code{README.md}.
#'
#' @param dir source directory for the Gitbook.
initGitbook <- function(dir=getwd()) {
.Deprecated('This function has been moved to the gitbook R package. See http://jason.bryer.org/Rgitbook for more information')
test <- system(paste0('gitbook init ', dir))
if(test != 0) { stop("gitbook initalization failed") }
mdfiles <- list.files(dir, '*.md', recursive=TRUE)
mdfiles <- mdfiles[!mdfiles %in% c('README.md', 'SUMMARY.md')]
mdfiles2 <- gsub('.md$', '.Rmd', mdfiles)
file.rename(mdfiles, mdfiles2)
invisible()
}
#' Builds markdown files from all Rmarkdown files in the given directories.
#'
#' This function will build Rmarkdown files in the given directory to markdown.
#' The default is to traverse all subdirectories of the working directory
#' looking for .Rmd files to process. This function will save a file in the
#' working directory called \code{.rmdbuild.Rda} that contain the status of the
#' last successful build. This allows the function to only process changed files.
#'
#' @param dirs character vector of directors to process.
#' @param clean if TRUE, all Rmd files will be built regardless of their
#' modification date.
#' @param log.dir if specified, the output from \code{\link{kintr}} will be saved
#' to a log file in the given directory.
#' @param log.ext if log files are saved, the file extension to use.
#' @param ... other parameters.
#' @author Jason Bryer <jason@bryer.org>
buildRmd <- function(dirs = getwd(), clean=FALSE, log.dir, log.ext='.txt', ...) {
.Deprecated('This function has been moved to the gitbook R package. See http://jason.bryer.org/Rgitbook for more information')
if(!exists('statusfile')) {
statusfile <- '.rmdbuild.Rda'
}
rmds <- list.files(dirs, '.rmd$', ignore.case=TRUE, recursive=TRUE)
finfo <- file.info(rmds)
if(!clean & file.exists(statusfile)) {
load(statusfile)
newfiles <- row.names(finfo)[!row.names(finfo) %in% row.names(rmdinfo)]
existing <- row.names(finfo)[row.names(finfo) %in% row.names(rmdinfo)]
existing <- existing[finfo[existing,]$mtime > rmdinfo[existing,]$mtime]
rmds <- c(newfiles, existing)
}
for(j in rmds) {
if(!missing(log.dir)) {
logfile <- paste0(log.dir, '/', sub('.Rmd$', log.ext, j, ignore.case=TRUE))
dir.create(dirname(logfile), recursive=TRUE, showWarnings=FALSE)
sink(logfile)
}
oldwd <- setwd(dirname(j))
tryCatch({
knit(basename(j), sub('.Rmd$', '.md', basename(j), ignore.case=TRUE))
}, finally={ setwd(oldwd) })
if(!missing(log.dir)) { sink() }
}
rmdinfo <- finfo
last.run <- Sys.time()
last.R.version <- R.version
save(rmdinfo, last.run, last.R.version, file=statusfile)
}
#' This will build a gitbook from the source markdown files.
#'
#' This function is simply a wrapper to a system call to \code{gitbook}.
#'
#' \url{https://github.com/GitbookIO/gitbook}
#'
#' @param source.dir location containing the source files.
#' @param out.dir location of the built book.
#' @param format the format of book. Options are gitbook (default website book),
#' pdf, or ebook.
#' @param title Name of the book to generate, defaults to repo name
#' @param intro Description of the book to generate
#' @param github ID of github repo like : username/repo
#' @param theme the book theme to use.
#' @author Jason Bryer <jason@bryer.org>
buildGitbook <- function(source.dir=getwd(),
out.dir=paste0(getwd(), '/_book'),
format, title, intro, github, theme) {
.Deprecated('This function has been moved to the gitbook R package. See http://jason.bryer.org/Rgitbook for more information')
cmd <- paste0("gitbook build ", source.dir, " --output=", out.dir)
if(!missing(format)) { cmd <- paste0(cmd, " --format=", format) }
if(!missing(title)) { cmd <- paste0(cmd, " --theme=", theme) }
if(!missing(title)) { cmd <- paste0(cmd, ' --title="', title, '"') }
if(!missing(intro)) { cmd <- paste0(cmd, ' --intro="', intro, '"') }
if(!missing(github)) { cmd <- paste0(cmd, ' --github=', github) }
if(!missing(theme)) { cmd <- paste0(cmd, " --theme=", theme) }
system(cmd)
# Post-process hack to fix broken img urls.
# https://github.com/GitbookIO/gitbook/issues/99
# Will also fix links to the Introduction
# https://github.com/GitbookIO/gitbook/issues/113
dirs <- list.dirs(out.dir, recursive=FALSE, full.names=FALSE)
for(i in seq_along(dirs)) {
files <- list.files(paste0(out.dir, '/', dirs[i]), '*.html')
for(j in seq_along(files)) {
fconn <- file(paste0(out.dir, '/', dirs[i], '/', files[j]))
file <- readLines(fconn)
close(fconn)
file <- gsub(paste0(dirs[i], '/', dirs[i], '/'), '', file)
file <- gsub('./">', './index.html">', file)
fconn <- file(paste0(out.dir, '/', dirs[i], '/', files[j]))
writeLines(file, fconn)
close(fconn)
}
}
}
#' Open a built gitbook.
#'
#' This function is a wrapper to the system call of \code{open} which should
#' open the book in the system's default web browser.
#'
#' @param out.dir location of the built gitbook.
#' @author Jason Bryer <jason@bryer.org>
openGitbook <- function(out.dir=paste0(getwd(), '/_book')) {
.Deprecated('This function has been moved to the gitbook R package. See http://jason.bryer.org/Rgitbook for more information')
browseURL(paste0(out.dir, '/index.html'))
}
#' Publish the built gitbook to Github.
#'
#' Note that this is a wrapper to system \code{git} call.
#'
#' This function assumes that the repository has already exists on Github.
#'
#' Thanks to ramnathv for the shell script.
#' https://github.com/GitbookIO/gitbook/issues/106#issuecomment-40747887
#'
#' @param repo the github repository. Should be of form username/repository
#' @param out.dir location of the built gitbook.
#' @param message commit message.
#' @author Jason Bryer <jason@bryer.org>
publishGitbook <- function(repo,
out.dir=paste0(getwd(), '/_book'),
message='Update built gitbook') {
.Deprecated('This function has been moved to the gitbook R package. See http://jason.bryer.org/Rgitbook for more information')
cmd <- paste0(
"cd ", out.dir, " \n",
"git init \n",
"git commit --allow-empty -m '", message,"' \n",
"git checkout -b gh-pages \n",
"git add . \n",
"git commit -am '", message, "' \n",
"git push git@github.com:", repo, " gh-pages --force ")
system(cmd)
}
view raw gitbook.R hosted with ❤ by GitHub

To leave a comment for the author, please follow the link and comment on their blog: Jason.Bryer.org Blog - R.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)