duckmayr
duckmayr

Reputation: 16920

How can I add arbitrary elements to the Table of Contents in Bookdown?

I am making a book via bookdown. I know it is possible to omit headings from the Table of Contents by adding the attributes {.unlisted .unnumbered}, as shown in Section 4.18 of the R Markdown Cookbook. However, how can I add arbitrary content to the Table of Contents? If I only needed to add this for the PDF output, I could use (e.g.) the LaTeX command \addcontentsline, but I need this to show in the HTML contents sidebar as well.

For example, if you set up a new default bookdown project from RStudio, it includes the file 01-intro.Rmd. The first few lines are

# Introduction {#intro}

You can label chapter and section titles using `{#label}` after them, e.g., we can reference Chapter \@ref(intro). If you do not manually label them, there will be automatic labels anyway, e.g., Chapter \@ref(methods).

Figures and tables with captions will be placed in `figure` and `table` environments, respectively.

I need to be able to create arbitrary divs that are added to the table of contents, like so:

# Introduction {#intro}

You can label chapter and section titles using `{#label}` after them, e.g., we can reference Chapter \@ref(intro). If you do not manually label them, there will be automatic labels anyway, e.g., Chapter \@ref(methods).

::: {#arbitrary-header <some other attribute?>}
Figures and tables with captions will be placed in `figure` and `table` environments, respectively.
:::

Which would add the sentence "Figures and tables with captions will be placed in figure and table environments, respectively." in both the LaTeX Table of Contents and the sidebar on the HTML output. The context of this problem is that I need to place a header inside another custom div that formats the content within a colorbox to make it stand out. Otherwise, I could of course just add another heading via ## before the sentence above.

Upvotes: 4

Views: 1107

Answers (3)

Martin C. Arnold
Martin C. Arnold

Reputation: 9668

We could use an R function that prints a colored box and adds the title to the TOC depending on the output format. For gitbook output, this is easily done using HTML and markdown. For pdf_book we may use a LaTeX environment for colored boxes like tcolorbox.

Here is the function (define in a code block in .Rmd file):

block_toc <- function(title, level, content, output) {
  if(output == "html") {
    title <- paste(paste(rep("#", level), collapse = ""), title, "{-}")
    cat('<div class = "myblock">', title, '<p>', content, '</p>\n</div>', sep = "\n")
  } else {
    level <- c("part", "chapter", "section")[level]
    cat('\\addcontentsline{toc}{', level, '}{', title, '}',
        '\n\\begin{mybox}\n\\textbf{\\noindent ', title, '}\n\\medskip\n\n', content,
        '\n\n\\end{mybox}', sep = "")
  }
}

Depending on the output format, block_toc() concatenates and prints the code for the blocks and, of course, also ensures that the title is added to the TOC. You can set the level of the TOC where the box title is added using level.

Use block_toc() like this in a chunk:

```{r, results='asis', echo=F, eval=T}
block_toc(
    title = "Central Limit Theorem",
    level = 2
    content = "The CLT states that, as $n$ goes to infinity, 
      the sample average $\\bar{X}$ converges in distribution 
      to $\\mathcal{N}(\\mu,\\sigma^2/n)$.",
    output = knitr::opts_knit$get("rmarkdown.pandoc.to")
)
```

output = knitr::opts_knit$get("rmarkdown.pandoc.to") will get and pass the current output format to the function when building the book.


Some styles for appealing boxes

Add to preamble.tex (for colored box in PDF output -- include file in YAML header). This will define a tcolorbox environment for generating blue boxes.

\usepackage{tcolorbox}
\definecolor{blue}{HTML}{D7DDEF}
\definecolor{darkblue}{HTML}{2B4E70}
\newtcolorbox{mybox}{colback=blue, colframe=darkblue}

Add to style.css (styles for HTML colored box) or include in a ```{css} code chunk:

.myblock {
 background-color: #d7ddef;
 border: solid #2b4e70;
 border-radius: 15px;
}
.myblock p, .myblock h2, .myblock h3 {
  padding: 5px 5px 5px 20px;
  margin-top: 0px !important;
}

For HTML output (gitbook) this yields

enter image description here

and for LaTeX output (pdf_book) it looks like this

enter image description here

with a corresponding entry at the section level in the TOC.

Upvotes: 2

tarleb
tarleb

Reputation: 22599

We'll be using a Lua filter for this, as those are a good way to modify R Markdown behavior. The "Bookdown Cookbook" has an excellent overview and includes a description of how to use Lua filters.

The way we are doing this is to side-step the normal TOC generator and rewrite it in Lua. Then we add our new TOC as a meta value named table-of-contents (and toc, for compatibility), which is enough to be included in the output.

So first let's dump the code to create a normal TOC:

_ENV = pandoc

local not_empty = function (x) return #x > 0 end
local section_to_toc_item

local function to_toc_item (number, text, id, subcontents)
  if number then
    text = Span({Str(number),Space()} .. text, {class='toc-section-number'})
  end
  local header_link = id == '' and text or Link(text, '#' .. id)
  local subitems = subcontents:map(section_to_toc_item):filter(not_empty)
  return List{Plain{header_link}} ..
      (#subitems == 0 and {} or {BulletList(subitems)})
end

section_to_toc_item = function (div)
  -- bail if this is not a section wrapper
  if div.t ~= 'Div' or not div.content[1] or div.content[1].t ~= 'Header' then
    return {}
  end
  local heading = div.content:remove(1)
  local number = heading.attributes.number
  -- bail if this is not supposed to be included in the toc
  if not number and heading.classes:includes 'unlisted' then
    return {}
  end

  return to_toc_item(number, heading.content, div.identifier, div.content)
end

-- return filter
return {
  { Pandoc = function (doc)
      local sections = utils.make_sections(true, nil, doc.blocks)
      local toc_items = sections:map(section_to_toc_item):filter(not_empty)
      doc.meta['table-of-contents'] = {BulletList(toc_items)}
      doc.meta.toc = doc.meta['table-of-contents']
      return doc
    end
  },
}

The code makes a few minor simplifications, but should produce an identical TOC for most documents. Now we can go on and modify the code to our liking. For example, to include divs with the class toc-line, the section_to_toc_item could be modified by adding this code at the start of the function:

section_to_toc_item = function (div)
  if div.t == 'Div' and div.classes:includes('toc-line') then
    return to_toc_item(nil, utils.blocks_to_inlines(div.content), div.identifier)
  end
  ⋮
end

Modify the code as you see fit.

Also, if you want to exclude the extra TOC lines from the normal output, you'll have to filter them out. Let the script return a second filter to do that:

return {
  { Pandoc = function (doc) ... end },
  { Div = function (div) return div.classes:includes 'toc-line' and {} or nil end }
}

Upvotes: 1

manro
manro

Reputation: 3677

Maybe this solution?

CSS-file:

  k1 {
   font-family: 'Times New Roman', Times, serif; 
   font-size: 12pt;
   text-align: justify;
  }
  
  #TOC {     
    color:black; 
    background-color: white;     
    position: fixed; 
    top: 0; 
    left: 0; 
    width: 250px;
    padding: 10px; 
    overflow:auto; 
    margin-left: -5px;
 }

  body {
    max-width: 800px;  
    margin-left:300px;
    line-height: 20px;
  }

  div#TOC li {
    list-style:none;
  }
  
  h2#toc-title {
  font-size: 24px;
  color: Red;
  }

Rmd-file:

---
title: "Arbitrary elements"

author: "Me"
date: "`r Sys.Date()`"
output:
  bookdown::html_document2:
    toc: true
    css: "arb.css"
toc-title: "Contents"    
---

# My question is it: "..."

# Introduction of our story ...

# It was a strange person ...

## The Flame was in his body ...

# <k1> Figures and tables with captions will be placed in `figure` and `table` environments, respectively. </k1> {-} 

## Where is the love, friends?

enter image description here

Upvotes: 2

Related Questions