Introducing: {hugodownplus} π¦
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Intro
In the last weeks I have put together a small R package {hugodownplus} which extends {hugodown} – the package which powers the blog posts of this and other #RStats Hugo websites.
{hugodownplus} offers a drop-in replacement for the rather minimalistic hugodown::md_markdown()
output format.
This blog post showcases the three main features of {hugodownplus}:
- a table of content,
- an expandable session info box, and
- wrapping text or code in expandable HTML boxes.
Although the main features are already explained in the official documentation, the big advantage of this blog post is that we can actually showcase each feature, which is neither possible in a GitHub README nor within a {pkgdown} website.
Before diving into the details, Iβll briefly elaborate on where the idea and inspiration for this package came from. After showcasing the main features, this blog post gives a glimpse under the hood, and shows how some of the more advanced features are implemented.
Idea & History & Collaboration
After I spent quite some time creating and customizing this website, which is made with Hugo and {hugodown}, quarto became a big thing and I saw a lot of stuff I liked and wanted to bring to my own blog.
It all started with me reading a blog post on “The MockUp” which showed a table of content and used different boxes for code, more information and the session info. I immediately wanted to bring those features to my website, but as Iβm no expert in Rmarkdown, I wasnβt sure if itβd be possible.
I started implementing a table of content, which was pretty straightforward, since this functionality is already included in the
rmarkdown::md_document()
function. All I had to do was to copy code from there to extend the
hugodown::md_document()
output function.
Then I somehow figured out how to create an Rmarkdown child document that contains the session info wrapped in an expandable box using the <details>
and <summary>
HTML tags.
Now I wanted to go one step further, and come up with a function that wraps any content, text or code or a child document in an expandable info, warn or output box. And this is basically where I gave up on figuring it out alone.
I posted a question on SO and put a bounty on it. Luckily, I got help from Shafayet who answered this and a related question.
After implementing all of this, I had a lot of custom functions and files in my website project and the idea was to package it up, so that it is easier to maintain, and others might benefit from it too. Since I was only putting code from different places into this package, and the really unique stuff came from Shafayet, I asked him to become a co-author. All in all it was a fun project and it made me happy to see the power of the #RStats community.
Main Features
Below Iβll showcase the three main features of {hugodownplus}:
- a table of content,
- an expandable session info box, and
- expandable HTML boxes to wrap text or code
Table of Content
This feature is basically copied from
rmarkdown::md_document()
and behaves pretty much in the same way. When using
hugodownplus::md_document()
as output in an Rmarkdown document, we can add the toc
argument and set it to TRUE
. This will add a table of content containing all headings up to the third level. To specify the level of headings we can supply the toc_depth
argument which defaults to 3
.
---
output:
hugodownplus::md_document:
toc: TRUE
title: "Article title"
# other arguments continuing here ...
---
This alone renders a rather naked table of content to the top of the page. To make it look a little bit more visually pleasing, I have implemented a few customization using CSS:
First, I wanted to include the heading βTable of Contentβ which I added as content
before
the first-child
element of an unordered list ul
inside the article-style
class:
.article-style>ul:first-child:before {
content: "Table of Content";
}
Since the CSS targets no other parts of this website this was a quick and easy way to add the words βTable of Contentβ.
Further, I wanted to put the TOC in a centered box and add arrows ββ£β as bullets of the top and second level headings:
.article-style > ul:first-child {
margin: auto; /* centers the TOC */
padding-top: 10px;
padding-bottom: 10px;
border: 1px dotted rgb(105,175,255); /* the border */
border-radius: 5px;
list-style-type: "β£ "; /* arrows first level headings */
}
.article-style > ul:first-child > li > ul {
list-style-type: "β£ "; /* arrows second level headings */
}
The result can be seen on the top of this page. The only downside is that the custom CSS is not applied when this blog post is shown on other sites like βR-bloggersβ.
Session Info Box
Besides
md_document()
{hugodownplus} contains a second function:
child_session_info()
. When used as inline code in an Rmarkdown document, this will create an expandable box containing the current session info.
---
output: hugodownplus::md_document
title: "Article title"
# other arguments continuing here ...
# we do not need the `use_boxes` argument !
---
# Heading 1
Some text
`r child_session_info()`
This alone will render a rather naked HTML box containing the session info using the <details>
and <summary>
tags.
Session Info
#> β Session info βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#> setting value
#> version R version 4.2.1 (2022-06-23)
#> os macOS Big Sur ... 10.16
#> system x86_64, darwin17.0
#> ui X11
#> language (EN)
#> collate en_US.UTF-8
#> ctype en_US.UTF-8
#> tz Europe/Berlin
#> date 2023-02-23
#> pandoc 2.19.2 @ /Applications/RStudio.app/Contents/MacOS/quarto/bin/tools/ (via rmarkdown)
#>
#> β Packages βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#> package * version date (UTC) lib source
#> hugodownplus * 0.0.0.9000 2023-02-19 [1] Github (timteafan/hugodownplus@d79c4c0)
#>
#> [1] /Library/Frameworks/R.framework/Versions/4.2/Resources/library
#>
#> ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
To make it more visually pleasing, we need some CSS magic:
For this website I use the code blow to β¦
- β¦ get padding and margins right,
- β¦ customize the font, color and background of the boxβ header and body, and
- β¦ format the inline code so that it covers the whole box and is displayed in grey.
/* padding, margins and border */
.session {
border: solid rgb(178, 178, 178);
border-width: 1px 1px 1px 5px;
border-radius: 5px;
padding: 0;
margin-left: 0px;
margin-top: 15px;
margin-bottom: 25px;
}
/* box header: background, color, font and padding */
summary.session-header {
padding: 2px 10px 0px 10px;
margin: 0;
background: rgb(31, 34, 41);
color: rgb(178, 178, 178);
font-family: Open Sans,Lucida Sans Unicode,Lucida Grande,sansSerif;
font-size: smaller;
border-radius: 5px;
}
/* change background color when box is expanded */
details[open] > summary.session-header {
background: rgb(41, 47, 61);
}
/* padding and margin of box body */
.session-details {
padding: 10px 10px 0px 10px;
margin: 0 0 10px 0;
}
/* code inside box: margins and setting the border around code to 0 */
details > div > pre.chroma {
border-width: 0px;
margin-left: 0px;
margin-bottom: 0px;
}
/* code inside box: text color */
details.sess > div > pre > code > span > span > span.hljs-comment,
details.sess > div > pre > code > span > span > span > span.hljs-comment {
color: rgb(178, 178, 178);
}
Adding the above CSS code renders the following box:
Session Info
#> β Session info βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#> setting value
#> version R version 4.2.1 (2022-06-23)
#> os macOS Big Sur ... 10.16
#> system x86_64, darwin17.0
#> ui X11
#> language (EN)
#> collate en_US.UTF-8
#> ctype en_US.UTF-8
#> tz Europe/Berlin
#> date 2023-02-23
#> pandoc 2.19.2 @ /Applications/RStudio.app/Contents/MacOS/quarto/bin/tools/ (via rmarkdown)
#>
#> β Packages βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#> package * version date (UTC) lib source
#> hugodownplus * 0.0.0.9000 2023-02-19 [1] Github (timteafan/hugodownplus@d79c4c0)
#>
#> [1] /Library/Frameworks/R.framework/Versions/4.2/Resources/library
#>
#> ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Expandable HTML Boxes
hugodownplus::md_document()
can generate info, warn and output boxes. The idea is that, in a blog post on topic X, we might want to talk a bit more about details of a related concept Y. This might not be interesting for every reader, so we can put this part in an expandable info box, and those interested, can dive in further. Similarly, we can create warn boxes, which draw the attention to one specific issue not every reader might be interested in. Finally output boxes can be used to show the output of a code chunk, only if the reader wants to see it (we actually encountered one of those already above).
To generate an info, warn or output box we just wrap text and/or code (or a child document) into a fenced (pandoc) div using three colons ::: before and after the part that we want to put into a box:
::: {.info-box title="Expand: Title of my info box"}
This is a test box.
We can include text and code:
```{r}
# Here is a code comment and below some code
1 + 1
```
:::
All we have to do is to specify either {.info-box}
, {.warn-box}
or {.output-box}
and a title
inside the div fence :::
. The title
will be shown in the header of the box. We also need to set the use_boxes
argument in the Rmarkdown header to TRUE
.
Similar to the session info box, this will render a naked HTML box:
Expand: Title of my info box
This is a test box
We can include text and code:
# Here is a code comment and below some code
1 + 1
#> [1] 2
Again, some CSS styling is needed to make the box βshineβ:
/* border, margin and padding */
.info-box {
margin-bottom: 15px;
}
.note {
border: solid rgb(51, 192, 155);
border-width: 1px 1px 1px 5px;
border-radius: 5px;
padding: 0;
margin-left: 0px;
}
/* header color, background and font */
summary.note-header,
.note-header {
padding: 2px 10px 0px 10px;
margin: 0;
background: rgb(31, 34, 41);
color: rgb(51, 192, 155);
font-family: Open Sans,Lucida Sans Unicode,Lucida Grande,sansSerif;
font-size: smaller;
border-radius: 5px;
}
/* body padding, margin, font-size */
.note-details {
padding: 10px 10px 0px 10px;
margin: 0 0 10px 0;
font-size: 0.8335rem;
}
/* placement of the icon */
.note-header > i {
margin-left: 5px;
}
/* code in box: no margins and no border */
details > div.note-details > div.highlight > pre.chroma {
border-width: 0px;
margin-left: 0px;
margin-bottom: 0px;
padding: 0;
margin-top: -20px;
}
/* code in box: code background and border radius */
details > div.note-details > div.highlight > pre.chroma > code {
background: #383b49;
border-radius: 3px;
}
Together with the CSS code above the following box will be rendered:
This is a test box.
We can include text and code:
# Here is a code comment and below some code
1 + 1
[1] 2
A Glimpse under the Hood
The reminder of this blog post give a glimpse under the hood, and shows how the session info box as well as the expandable HTML boxes are implemented. Letβs start with the easier one.
Session info box
The session info box is created by the
child_session_info()
function. The only thing that this function does is to create an Rmarkdown child document based on a template session_info.Rmd
using the kntir::knit_child()
function.
child_session_info <- function(pkgs = c("loaded", "attached", "installed")[1]) {
knitr::knit_child(fs::path_package("rmdtmp/session_info.Rmd",
package = "hugodownplus"),
envir = environment(),
quiet = TRUE)
}
The essence of the session_info.Rmd
template looks like this:
<div class="session" markdown="1">
<details class="sess">
<summary class="session-header" markdown="1">
Session Info <i class="fas fa-tools"></i>
</summary>
```{r, echo = FALSE}
sessioninfo::session_info(pkgs = pkgs)
```
</details>
</div>
Basically, we wrap a code chunk containing the
sessioninfo::session_info()
function into <details>
and <summary>
HTML tags together with a custom <div>
and some HMTL classes to make the styling easier.
Expandable HTML boxes
While it would be possible to create similar HTML boxes by just wrapping them in HTML tags manually, I was looking for a way to make it easier to create this kind of boxes.
Writing a custom R function would have been one way to go about it, but I preferred a solution which would let me create HTML boxes within an Rmarkdown document βon the flyβ.
To do that, Shafayet came up with a great idea on SO. We can use pandocβs includes
argument and set the after_body
parameter to an HTML file which will be included after the body is rendered.
This HTML document is basically javascript wrapped in HTML <script>
tags.
The code that generates the info boxes is shown below. To make sense of it, it helps to read it from bottom to top.
<script>
function create_info_box(title, content) {
let summary = document.createElement("summary");
summary.classList.add("note-header");
summary.setAttribute("markdown", "1");
let summary_title = document.createTextNode(title)
let summary_icon = document.createElement('i');
summary_icon.classList.add("fas", "fa-info-circle");
summary.append(summary_title, summary_icon);
let div_note_details = document.createElement("div");
div_note_details.classList.add('note-details');
div_note_details.append(...content)
let details = document.createElement('details');
details.append(summary, div_note_details);
let div_note = document.createElement("div");
div_note.classList.add('note');
div_note.setAttribute("markdown", "1");
div_note.append(details);
return(div_note)
};
function info_box() {
let childs = document.querySelectorAll("div.info-box");
childs.forEach(el => {
let title = el.title
let info_box = create_info_box(title, el.childNodes);
el.append(info_box)
});
};
window.onload = info_box();
</script>
When the page is is loaded window.onload
the info_box()
function is executed. The info_box()
function selects all <div class="info-box">
elements. For each element el
that it is found, it executes the create_info_box()
function, which is defined at the very top of the script. Without going into details here, this function basically creates all the single parts, the <summary>
and <details>
tags the classes and attributes and it wraps the ...content
in the middle of all this.
So where does the <div class="info-box">
come from? We create those on the fly by wrapping a section of our Rmarkdown document in a fenced pandoc div: ::: {.info-box} content goes here :::
. When the document is knitted the HTML code above will be executed and will render our boxes accordingly.
Although it now sounds pretty simple, Iβd never figured this out alone.
Another possible way of implementing the same feature is to use knitr hooks which offer a similar functionality to change the output of a document after knitting. However, I havenβt got my head around knitr hooks yet, but might give them a try when the next Rmarkdown challenge awaits.
Wrap-up
Thatβs it! While many #RStats bloggers are porting their Hugo website to quarto, Iβd be happy if one or the other Hugo user finds this package helpful - or at least the insights I gained in the process of making it. If you know betters ways of implementing this, maybe using knitr hooks, let me know in the comments below or via Mastodon, Twitter or Github!
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.