Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support for tabs in BS4 #1694

Merged
merged 29 commits into from
Jun 18, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# pkgdown (development version)

* pkgdown, for Bootstrap 4, supports tabsets in articles [as in R Markdown](https://bookdown.org/yihui/rmarkdown-cookbook/html-tabs.html) (@JamesHWade, #1667).

* pkgdown (in concert with downlit and roxygen2) is moving towards more consistent HTML structure for syntax highlighting. The goal is to always have a `<div>` with class `sourceCode` (and other classes as needed), which contains one or more `<pre>`s that has class `sourceCode` and the language, and each `<pre>` contains `<code>`. Something like this:

```html
Expand Down
118 changes: 116 additions & 2 deletions R/html-tweak.R
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,113 @@ tweak_footnotes <- function(html) {
xml2::xml_remove(container)
}

# Tabsets tweaking: find Markdown recommended in https://bookdown.org/yihui/rmarkdown-cookbook/html-tabs.html
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cderv is this transformation something that you might consider hosting in an Rmd-adjacent package at some point in the future? Or do you think the xml2 based transformation is too foreign for the Rmd ecosystem?

Copy link
Contributor

@cderv cderv Jun 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are currently discussing a bit tabset feature because of rstudio/rmarkdown#2054. We got input from @cpsievert that bslib will offer functions for creating navs / tabs UI (https://rstudio.github.io/bslib/reference/index.html#section-create-navs-and-navbars) and that we may be able to leverage that in some way in the future for rmarkdown.

Until now our direction for reworking tabset feature would be to use Lua filters rather than R directly. Also because Quarto has already a Lua filter for this (but using a fenced div syntax). But nothing is planned and started on this.

I think Lua filter would not help at all pkgdown usage currently, am I right ?

If we don't go the Lua filter road, then yes regarding xml2, I think it could be included in the Rmd ecosystem. it has no dependency and I feel we should use it more for some html post processing. But it is not already in the package tree for rmarkdown.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If users use bslib for creating navs, then the resulting divs should be protected i.e. not treated by pkgdown. The code in this PR recognizes future tabsets via the XPath query .//div[contains(@class, 'tabset')] which if I follow correctly would not pick up navs created by bslib. 🤔

# i.e. "## Heading {.tabset}" or "## Heading {.tabset .tabset-pills}"
# no matter the heading level -- the headings one level down are the tabs
# and transform to tabsets HTML a la Bootstrap

tweak_tabsets <- function(html) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it's time to start breaking this file up into smaller pieces?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh no, it was my favorite R script in pkgdown 😢 More seriously: #1725

tabsets <- xml2::xml_find_all(html, ".//div[contains(@class, 'tabset')]")
if (length(tabsets) == 0) {
maelle marked this conversation as resolved.
Show resolved Hide resolved
return()
}
purrr::walk(tabsets, tweak_tabset)
invisible(html)
}

tweak_tabset <- function(html) {
maelle marked this conversation as resolved.
Show resolved Hide resolved
id <- xml2::xml_attr(html, "id")

# Users can choose pills or tabs
nav_class <- if (grepl("tabset-pills", xml2::xml_attr(html, "class"))) {
maelle marked this conversation as resolved.
Show resolved Hide resolved
"nav-pills"
} else {
"nav-tabs"
}

# Add empty ul for nav
xml2::xml_add_child(html, "ul", class=sprintf("nav %s nav-row", nav_class), id=id)
# Add empty div for content
xml2::xml_add_child(html, "div", class="tab-content")
maelle marked this conversation as resolved.
Show resolved Hide resolved

# Identify tabs and get them in an object
tabs <- xml2::xml_find_all(html, "div[contains(@id, 'tab')]")

# Remove tabs from original HTML
xml2::xml_remove(tabs)

# Fill the ul for nav
purrr::walk(tabs, tablist_item, html = html, parent_id = id)

# Fill the div for content
purrr::walk(tabs, tablist_content, html = html, parent_id = id)

# activate first tab unless another one is already activated
# (by the attribute {.active} in the source Rmd)
nav_url <- xml2::xml_find_first(html, sprintf("//ul[@id='%s']", id))
if (!any(grepl("active", xml2::xml_attr(xml2::xml_children(nav_url), "class")))) {
tweak_class_prepend(xml2::xml_child(nav_url), "active")
}
content_div <- xml2::xml_find_first(html, sprintf("//div[@id='%s']/div", id))
if (!any(grepl("active", xml2::xml_attr(xml2::xml_children(content_div), "class")))) {
tweak_class_prepend(xml2::xml_child(content_div), "active")
}
}

# Add an item (tab) to the tablist
tablist_item <- function(tab, html, parent_id) {
id <- xml2::xml_attr(tab, "id")
text <- xml_text1(xml2::xml_child(tab))
ul_nav <- xml2::xml_find_first(html, sprintf("//ul[@id='%s']", parent_id))

xml2::xml_add_child(
ul_nav,
"a",
text,
`data-toggle` = "tab",
href = paste0("#", id),
role = "tab"
)

# Activate (if there was "{.active}" in the source Rmd)
if (grepl("active", xml2::xml_attr(tab, "class"))) {
class <- "active"
} else {
class = ""
}

# tab a's need to be wrapped in li's
xml2::xml_add_parent(
xml2::xml_find_first(html, sprintf("//a[@href='%s']", paste0("#", id))),
"li",
class = class
)
}

# Add content of a tab to a tabset
tablist_content <- function(tab, html, parent_id) {
# remove first child, that is the header
xml2::xml_remove(xml2::xml_child(tab))

# Activate (if there was "{.active}" in the source Rmd)
if (grepl("active", xml2::xml_attr(tab, "class"))) {
xml2::xml_attr(tab, "class") <- "tab-pane active"
} else {
xml2::xml_attr(tab, "class") <- "tab-pane"
}

xml2::xml_attr(tab, "role") <- "tabpanel"

content_div <- xml2::xml_find_first(
html,
sprintf("//div[@id='%s']/div", parent_id)
)

xml2::xml_add_child(content_div, tab)
}



# File level tweaks --------------------------------------------

tweak_rmarkdown_html <- function(html, input_path, pkg = pkg) {
Expand All @@ -137,13 +244,20 @@ tweak_rmarkdown_html <- function(html, input_path, pkg = pkg) {
tweak_anchors(html, only_contents = FALSE)
tweak_md_links(html)
tweak_all_links(html, pkg = pkg)
if (pkg$bs_version > 3) tweak_footnotes(html)

if (pkg$bs_version > 3) {
# Tweak footnotes
tweak_footnotes(html)

# Tweak tabsets
tweak_tabsets(html)
}

# Tweak classes of navbar
toc <- xml2::xml_find_all(html, ".//div[@id='tocnav']//ul")
xml2::xml_attr(toc, "class") <- "nav nav-pills nav-stacked"

# Mame sure all images use relative paths
# Make sure all images use relative paths
img <- xml2::xml_find_all(html, "//img")
src <- xml2::xml_attr(img, "src")
abs_src <- is_absolute_path(src)
Expand Down
5 changes: 5 additions & 0 deletions inst/assets/BS4/pkgdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -465,3 +465,8 @@ summary {
details p {
margin-top: -.5rem;
}

/* tabsets */
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another tweak that might make sense would be more padding/margin at the top of tab content.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there normally some styling around the content of the tabs? For the pills example in particular, it's hard to tell how the tab names are related to the content.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added left and bottom borders for the pills example. Its color won't be hard-coded forever, since the blslib variables PR will prevent this kind of hard-coding.

.nav-row {
flex-direction: row;
}
52 changes: 52 additions & 0 deletions tests/testthat/_snaps/html-tweak.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,55 @@
[1] <a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role= ...
[2] <div class="dropdown-menu" aria-labelledby="navbarDropdown">\n <a clas ...

# tweak_tabsets() default

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div id="results-in-tabset" class="section level2 tabset">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>


<ul class="nav nav-tabs nav-row" id="results-in-tabset">
<li class="active "><a data-toggle="tab" href="#tab-1" role="tab">Tab 1</a></li>
<li class=""><a data-toggle="tab" href="#tab-2" role="tab">Tab 2</a></li>
</ul>
<div class="tab-content">
<div id="tab-1" class="active tab-pane" role="tabpanel">

<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="tab-pane" role="tabpanel">

<p>blop</p>
</div>
</div>
</div></body></html>

# tweak_tabsets() with tab pills and second tab active

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div id="results-in-tabset" class="section level2 tabset tabset-pills">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>


<ul class="nav nav-pills nav-row" id="results-in-tabset">
<li class=""><a data-toggle="tab" href="#tab-1" role="tab">Tab 1</a></li>
<li class="active"><a data-toggle="tab" href="#tab-2" role="tab">Tab 2</a></li>
</ul>
<div class="tab-content">
<div id="tab-1" class="tab-pane" role="tabpanel">

<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="tab-pane active" role="tabpanel">

<p>blop</p>
</div>
</div>
</div></body></html>

44 changes: 44 additions & 0 deletions tests/testthat/test-html-tweak.R
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,47 @@ test_that("activate_navbar()", {
xml2::xml_find_first(navbar, ".//li[contains(@class, 'active')]")
)
})

# tabsets -------------------------------------------------------------

test_that("tweak_tabsets() default", {
html <- '<div id="results-in-tabset" class="section level2 tabset">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>
<div id="tab-1" class="section level3">
<h3 class="hasAnchor">
<a href="#tab-1" class="anchor" aria-hidden="true"></a>Tab 1</h3>
<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it important to have source code in here? Otherwise it would be better to keep the test as short and simple as possible.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find of another solution right now #1725 (comment)

<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="section level3">
<h3 class="hasAnchor">
<a href="#tab-2" class="anchor" aria-hidden="true"></a>Tab 2</h3>
<p>blop</p>
</div>
</div>'
new_html <- tweak_tabsets(xml2::read_html(html))
expect_snapshot_output(cat(as.character(new_html)))
})

test_that("tweak_tabsets() with tab pills and second tab active", {
html <- '<div id="results-in-tabset" class="section level2 tabset tabset-pills">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>
<div id="tab-1" class="section level3">
<h3 class="hasAnchor">
<a href="#tab-1" class="anchor" aria-hidden="true"></a>Tab 1</h3>
<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="section level3 active">
<h3 class="hasAnchor">
<a href="#tab-2" class="anchor" aria-hidden="true"></a>Tab 2</h3>
<p>blop</p>
</div>
</div>'
new_html <- tweak_tabsets(xml2::read_html(html))
expect_snapshot_output(cat(as.character(new_html)))
})
29 changes: 29 additions & 0 deletions vignettes/test/rendering.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,32 @@ Blablablablablabla.
# This section is unnumbered {-}

There should however be no bug here!

## Results in tabset {.tabset .tabset-pills}

### Tab 1

blablablabla

```r
1 + 1
```

### Tab 2

blop


## Results in tabset, no pills {.tabset}

### Tab 1

something nice

```{r}
plot(1:42)
```

### Tab 2 {.active}

hello