-
Notifications
You must be signed in to change notification settings - Fork 336
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
support for tabs in BS4 #1694
Changes from 11 commits
77f3e59
9800678
62cbdc9
e922160
8f959c0
96e10d0
68a1802
09aeba1
05389d6
5e003f3
00b18c2
9233d03
0d61641
bd7c455
7ae5091
bba3e4b
bc8314c
1a551cd
dc5f2a4
88bc25a
799a8ca
feb7139
b5e41d5
43c4695
92d8b8a
a1faef9
bdef2e0
a344565
e786c25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
# 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
@@ -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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -465,3 +465,8 @@ summary { | |
details p { | ||
margin-top: -.5rem; | ||
} | ||
|
||
/* tabsets */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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))) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the record, in rmarkdown the transformation happens via JS https://github.com/rstudio/rmarkdown/blob/145046175c721af185aa6ba3ecc9262f12dc7369/inst/rmd/h/navigation-1.1/tabsets.js#L50
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. 🤔