Using Gitbook with R Markdown
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 thelog
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 thegh-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()
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) | |
} |
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.