Enable code folding in bookdown and blogdown

Code folding in bookdown and blogdown

Option code_folding: true, like in classical rmarkdown documents, is not working in bookdown or blogdown but it is possible to enable it with some tricks. All files presented here, the javascript and Rmd files necessary for bookdown and the html files necessary for blogdown, to enable code folding are available on my github blog tips repository.
As in the present blog post, these codes allow for:

  • a button on each code chunk to show/hide it
  • a global button with a dropdown menu to show/hide all codes on your page

Code folding in bookdown

Some time ago, I answered a question on stackoverflow to make code folding working with bookdown. Although this answer works, I was not totally satisfied with the behavior of the “Show/Hide Global” button. Today, I finally made it work properly ! I did “reverse engineering” to find what worked in the classical rmarkdown and defined the appropriate css for bookdown.

Some javascript codes

The main javascript function that will be called codefolding.js needs to find .sourceCode class divisions to work with bookdown. This also requires complementary javascript functions of bootstrap, but not all.
Here are the steps:

  1. Create a js folder in the same directory than your Rmd file
  2. Download javascript functions transition.js, collapse.js and dropdown.js here for instance: https://github.com/twbs/bootstrap/tree/v3.3.7/js and store them in your js folder
  3. Create a new file in the js folder called codefolding.js with the following code. This is the same as for rmarkdown code_folding option but with pre.sourceCode added to find R-code chunks. You can had any other code language with pre.lang in this file.

The codefolding.js file:

window.initializeCodeFolding = function(show) {

  // handlers for show-all and hide all
  $("#rmd-show-all-code").click(function() {
    $('div.r-code-collapse').each(function() {
      $(this).collapse('show');
    });
  });
  $("#rmd-hide-all-code").click(function() {
    $('div.r-code-collapse').each(function() {
      $(this).collapse('hide');
    });
  });

  // index for unique code element ids
  var currentIndex = 1;

  // select all R code blocks
  var rCodeBlocks = $('pre.sourceCode, pre.r, pre.python, pre.bash, pre.sql, pre.cpp, pre.stan, pre.js');
  rCodeBlocks.each(function() {

    // create a collapsable div to wrap the code in
    var div = $('<div class="collapse r-code-collapse"></div>');
    if (show)
      div.addClass('in');
    var id = 'rcode-643E0F36' + currentIndex++;
    div.attr('id', id);
    $(this).before(div);
    $(this).detach().appendTo(div);

    // add a show code button right above
    var showCodeText = $('<span>' + (show ? 'Hide' : 'Code') + '</span>');
    var showCodeButton = $('<button type="button" class="btn btn-default btn-xs code-folding-btn pull-right"></button>');
    showCodeButton.append(showCodeText);
    showCodeButton
        .attr('data-toggle', 'collapse')
        .attr('data-target', '#' + id)
        .attr('aria-expanded', show)
        .attr('aria-controls', id);

    var buttonRow = $('<div class="row"></div>');
    var buttonCol = $('<div class="col-md-12"></div>');

    buttonCol.append(showCodeButton);
    buttonRow.append(buttonCol);

    div.before(buttonRow);

    // update state of button on show/hide
    div.on('hidden.bs.collapse', function () {
      showCodeText.text('Code');
    });
    div.on('show.bs.collapse', function () {
      showCodeText.text('Hide');
    });
  });

}

The Rmd file

The bookdown Rmd script includes all js functions directly written in the header, so that the js folder is not useful for the final document itself. When creating the setup chunk, the option is set to show all code blocks by default, but you can choose to hide them by default with 'show' === 'hide'. The complete Rmd file to be used as a basis can be found on my blog tips github repository.

  • YAML header reads a header.html that is built when knitting the Rmd file.
---
title: "Toggle R code"
author: "StatnMap"
date: '03 août, 2018'
output:
  bookdown::html_document2:
      includes:
        in_header: header.html
  bookdown::gitbook:
      includes:
        in_header: header.html
---
  • The R-code below is to be included in a chunk with echo=FALSE. This reads js files and includes them in an external header.html with the css asociated. This header.html will then be directly included in the bookdown files thanks to the above yaml configuration.
    You’ll see that there is some css in this code that you can modify for the position, colors and whatever you want on these buttons with some more css.
codejs <- readr::read_lines("/mnt/Data/autoentrepreneur/js/codefolding.js")
collapsejs <- readr::read_lines("/mnt/Data/autoentrepreneur/js/collapse.js")
transitionjs <- readr::read_lines("/mnt/Data/autoentrepreneur/js/transition.js")
dropdownjs <- readr::read_lines("/mnt/Data/autoentrepreneur/js/dropdown.js")

htmlhead <- c(
  paste('
<script>',
paste(transitionjs, collapse = "\n"),
'</script>
<script>',
paste(collapsejs, collapse = "\n"),
'</script>
<script>',
paste(codejs, collapse = "\n"),
'</script>
<script>',
paste(dropdownjs, collapse = "\n"),
'</script>
<style type="text/css">
.code-folding-btn { margin-bottom: 4px; }
.row { display: flex; }
.collapse { display: none; }
.in { display:block }
.pull-right > .dropdown-menu {
    right: 0;
    left: auto;
}
.open > .dropdown-menu {
    display: block;
}
.dropdown-menu {
    position: absolute;
    top: 100%;
    left: 0;
    z-index: 1000;
    display: none;
    float: left;
    min-width: 160px;
    padding: 5px 0;
    margin: 2px 0 0;
    font-size: 14px;
    text-align: left;
    list-style: none;
    background-color: #fff;
    -webkit-background-clip: padding-box;
    background-clip: padding-box;
    border: 1px solid #ccc;
    border: 1px solid rgba(0,0,0,.15);
    border-radius: 4px;
    -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
    box-shadow: 0 6px 12px rgba(0,0,0,.175);
}
</style>
<script>
$(document).ready(function () {
  window.initializeCodeFolding("show" === "show");
});
</script>
', sep = "\n"),
  paste0('
<script>
document.write(\'<div class="btn-group pull-right" style="position: absolute; top: 20%; right: 2%; z-index: 200"><button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true" data-_extension-text-contrast=""><span>Code</span> <span class="caret"></span></button><ul class="dropdown-menu" style="min-width: 50px;"><li><a id="rmd-show-all-code" href="#">Show All Code</a></li><li><a id="rmd-hide-all-code" href="#">Hide All Code</a></li></ul></div>\')
</script>
')
)

readr::write_lines(htmlhead, path = "/mnt/Data/autoentrepreneur/header.html")

Code folding in blogdown

As I recently migrated to hugo, I decided I could include this trick with blogdown. As you can see on the present article, code folding is enabled. You can retrieve all following codes in my github blog tips repository. If you want to see all modifications that I added to my “hugo-statnmap-template” for code folding, I recommend you to refer to this code-folding pull request in my hugo template on github. Note that my template is multilingual, so is the code-folding code on github too.
This relies on the same codes than for bookdown, except that we can set it up once for the entire website using your theme.

Some javascript libraries

The same javascript libraries are necessary.

  1. Create a js folder in the static directory of your hugo theme.
  2. Add the complementary javascript functions of bootstrap: transition.js, collapse.js and dropdown.js (here for instance) in the js directory.
  3. In this same js directory, create a new file called codefolding.js. The code is exactly the same as for bookdown.

The codefolding.js file:

window.initializeCodeFolding = function(show) {

  // handlers for show-all and hide all
  $("#rmd-show-all-code").click(function() {
    $('div.r-code-collapse').each(function() {
      $(this).collapse('show');
    });
  });
  $("#rmd-hide-all-code").click(function() {
    $('div.r-code-collapse').each(function() {
      $(this).collapse('hide');
    });
  });

  // index for unique code element ids
  var currentIndex = 1;

  // select all R code blocks
  var rCodeBlocks = $('pre.sourceCode, pre.r, pre.python, pre.bash, pre.sql, pre.cpp, pre.stan, pre.js');
  rCodeBlocks.each(function() {

    // create a collapsable div to wrap the code in
    var div = $('<div class="collapse r-code-collapse"></div>');
    if (show)
      div.addClass('in');
    var id = 'rcode-643E0F36' + currentIndex++;
    div.attr('id', id);
    $(this).before(div);
    $(this).detach().appendTo(div);

    // add a show code button right above
    var showCodeText = $('<span>' + (show ? 'Hide' : 'Code') + '</span>');
    var showCodeButton = $('<button type="button" class="btn btn-default btn-xs code-folding-btn pull-right"></button>');
    showCodeButton.append(showCodeText);
    showCodeButton
        .attr('data-toggle', 'collapse')
        .attr('data-target', '#' + id)
        .attr('aria-expanded', show)
        .attr('aria-controls', id);

    var buttonRow = $('<div class="row"></div>');
    var buttonCol = $('<div class="col-md-12"></div>');

    buttonCol.append(showCodeButton);
    buttonRow.append(buttonCol);

    div.before(buttonRow);

    // update state of button on show/hide
    div.on('hidden.bs.collapse', function () {
      showCodeText.text('Code');
    });
    div.on('show.bs.collapse', function () {
      showCodeText.text('Hide');
    });
  });

}

Modify your hugo templates

  1. In your theme config file, you can add the following parameters to enable or disable totally codefolding in the website and to define if it is shown or not by default. Note that this can also be defined in each blog article (see below).
# Set to true to disable code folding
    disable_codefolding = false
# Set to "hide" or "show" all codes by default
    codefolding_show = "hide"
  1. In the main footer (or header) of your website, you need to load the javascript libraries. You can use the following code (footer_js.html).
{{ if not .Site.Params.disable_codefolding }}
  <script src="{{ "js/collapse.js" | relURL }}"></script>
  <script src="{{ "js/dropdown.js" | relURL }}"></script>
  <script src="{{ "js/transition.js" | relURL }}"></script>
{{ end }}
  1. To be able to allow or not code_folding in each article, you need to load codefolding.js in the article footer (or header) directly. You can also let the possibility to choose for each article if all codes are shown or hidden when loading the article. This is the same part of code as for bookdown with "show" === "show", but here we use .Site.Params or .Params to define it. For instance, in the present website, you can go on the other blog posts and you will see that all codes are hidden by default "show" === "hide". When you arrive on the present article on code-folding, all codes where opened. This is because I set codefolding_show: 'show' in the yaml header of my Rmd file. Same is for parameter disable_codefolding: false that you can set in your main website config file as well as in each blog article. Thus, you need to add this part of code in your article footer (or header) template directly (article_footer_js.html).
{{ if and (not .Site.Params.disable_codefolding) (not .Params.disable_codefolding) (in (string .Content) "</pre>") }} 
  <script>
  $(document).ready(function () {
    window.initializeCodeFolding("show" === {{ if isset .Params "codefolding_show" }}{{ .Params.codefolding_show }}{{ else }}{{ default ("hide") .Site.Params.codefolding_show }}{{ end }});
  });
  </script>
  <script src="{{ "js/codefolding.js" | relURL }}"></script>
{{ end }}
  1. You need to find the appropriate place to add the following code in the template of your articles, maybe in article header. This code creates the main button that will enable “show/hide all code blocks” at the same time. In my case, I stored the following code in header_maincodefolding.html in the template/partials directory. I then added {{ partial "header_maincodefolding" . }} in my article header.
{{ if and (not .Site.Params.disable_codefolding) (not .Params.disable_codefolding) (in (string .Content) "</pre>") }}
<div id="code-folding-buttons" class="btn-group pull-right">
  <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true" data-_extension-text-contrast="">
    <span>Show/Hide all code</span> 
    <span class="caret"></span>
  </button>
  <ul class="dropdown-menu" style="min-width: 50px;">
  <li><a id="rmd-show-all-code" href="#">Show All Code</a>
  </li><li><a id="rmd-hide-all-code" href="#">Hide All Code</a></li>
  </ul>
</div>
{{ end }}

Add some css

For all the buttons to appear correctly on your webpage, you need to place them with some css. Here are the css codes I used.

  • Code for the main “show/hide all codes” button
#code-folding-buttons {float: right;}
#code-folding-buttons .pull-right > .dropdown-menu {
    right: 0;
    left: auto;
}
#code-folding-buttons .btn.btn-default.btn-xs.dropdown-toggle {
    float: right;
}
#code-folding-buttons.open > .dropdown-menu {
    display: block;
}
#code-folding-buttons .dropdown-menu {
    top: 100%;
    left: 0;
    z-index: 1000;
    display: none;
    min-width: 160px;
    padding: 5px 0;
    margin: 2px 0 0;
    font-size: 14px;
    text-align: left;
    list-style: none;
    background-color: #fff;
    -webkit-background-clip: padding-box;
    background-clip: padding-box;
    border: 1px solid #ccc;
    border: 1px solid rgba(0,0,0,.15);
    border-radius: 4px;
    -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
    box-shadow: 0 6px 12px rgba(0,0,0,.175);
}
#code-folding-buttons .dropdown-menu li {
    padding: 5px;
}
  • Code for buttons over each code block
.code-folding-btn { 
    margin-bottom: 4px; 
}
.row { 
    display: flex;
    border-bottom: solid 1px #d7d7d7;
}
.collapse {
    display: none; 
}
.in { 
    display: block ;
    border: solid 1px #d7d7d7;
    border-radius: 5px;
}
.col-md-12 {
    margin: 0 0 0 auto;
}

Again, all these codes are available on my github blog tips repository

comments powered by Disqus