tag inserted in the middle
#' markup <- paste(sep = "\n",
#' "This is *emphasized* text in markdown.",
#' htmlPreserve(""),
#' "Here is some more *emphasized text*."
#' )
#' extracted <- extractPreserveChunks(markup)
#' markup <- extracted$value
#' # Just think of this next line as Markdown processing
#' output <- gsub("\\*(.*?)\\*", "\\1 ", markup)
#' output <- restorePreserveChunks(output, extracted$chunks)
#' output
#'
#' @export
htmlPreserve <- function(x) {
html_preserve(x, inline = "auto")
}
html_preserve <- function(x, inline = "auto") {
x <- paste(x, collapse = "\n")
# Do nothing for empty string
if (!nzchar(x)) {
return(x)
}
# rmarkdown sets this option to TRUE to leverage various benefits
# that come with preserving HTML via pandoc 2.0's raw attribute feature
# https://github.com/rstudio/rmarkdown/pull/1965#issuecomment-734804176
if (!getOption("htmltools.preserve.raw", FALSE)) {
return(sprintf("%s", x))
}
# With no other context, the presence of line break(s) determines whether a
# block or inline code chunk is used for pandoc's raw attributes (the inline
# version may add an additional tag around the HTML (which can be
# problematic, for instance, when embedding shiny inside flexdashboard)
# Thankfully knitr::knit_print() can tell us whether we're inside a inline
# code which is why this is here essentially just for non-knit usage
# https://github.com/rstudio/flexdashboard/issues/379
# https://github.com/rstudio/rmarkdown/issues/2259#issuecomment-995996958
if (identical(inline, "auto")) {
inline <- grepl(x, "\n", fixed = TRUE)
}
if (inline) {
sprintf("`%s`{=html}", x)
} else {
sprintf("\n```{=html}\n%s\n```\n", x)
}
}
# Temporarily set x in env to value, evaluate expr, and
# then restore x to its original state
withTemporary <- function(env, x, value, expr, unset = FALSE) {
if (exists(x, envir = env, inherits = FALSE)) {
oldValue <- get(x, envir = env, inherits = FALSE)
on.exit(
assign(x, oldValue, envir = env, inherits = FALSE),
add = TRUE)
} else {
on.exit(
rm(list = x, envir = env, inherits = FALSE),
add = TRUE
)
}
if (!missing(value) && !isTRUE(unset))
assign(x, value, envir = env, inherits = FALSE)
else {
if (exists(x, envir = env, inherits = FALSE))
rm(list = x, envir = env, inherits = FALSE)
}
force(expr)
}
# Evaluate an expression using Shiny's own private stream of
# randomness (not affected by set.seed).
withPrivateSeed <- local({
ownSeed <- NULL
function(expr) {
withTemporary(.GlobalEnv, ".Random.seed",
ownSeed, unset=is.null(ownSeed), {
tryCatch({
expr
}, finally = {ownSeed <<- .Random.seed})
}
)
}
})
# extract_preserve_chunks looks for regions in strval marked by
# ... and replaces each such region
# with a long unique ID. The return value is a list with $value as the string
# with the regions replaced, and $chunks as a named character vector where the
# names are the IDs and the values are the regions that were extracted.
#
# Nested regions are handled appropriately; the outermost region is what's used
# and any inner regions simply have their boundaries removed before the values
# are stashed in $chunks.
#' @return `extractPreserveChunks` returns a list with two named elements:
#' `value` is the string with the regions replaced, and `chunks` is
#' a named character vector where the names are the IDs and the values are the
#' regions that were extracted.
#' @rdname htmlPreserve
#' @export
extractPreserveChunks <- function(strval) {
# Literal start/end marker text. Case sensitive.
startmarker <- ""
endmarker <- ""
# Start and end marker length MUST be different, it's how we tell them apart
startmarker_len <- nchar(startmarker)
endmarker_len <- nchar(endmarker)
# Pattern must match both start and end markers
pattern <- ""
# It simplifies string handling greatly to collapse multiple char elements
if (length(strval) != 1)
strval <- paste(strval, collapse = "\n")
# matches contains the index of all the start and end markers
startmatches <- gregexpr(startmarker, strval, fixed = TRUE)[[1]]
endmatches <- gregexpr(endmarker, strval, fixed = TRUE)[[1]]
matches <- c(startmatches, endmatches)
o <- order(matches)
matches <- matches[o]
lengths <- c(
attr(startmatches, "match.length", TRUE),
attr(endmatches, "match.length", TRUE)
)[o]
# No markers? Just return.
if (unique(matches)[[1]] == -1)
return(list(value = strval, chunks = character(0)))
# If TRUE, it's a start; if FALSE, it's an end
boundary_type <- lengths == startmarker_len
# Positive number means we're inside a region, zero means we just exited to
# the top-level, negative number means error (an end without matching start).
# For example:
# boundary_type - TRUE TRUE FALSE FALSE TRUE FALSE
# preserve_level - 1 2 1 0 1 0
preserve_level <- cumsum(ifelse(boundary_type, 1, -1))
# Sanity check.
if (any(preserve_level < 0) || tail(preserve_level, 1) != 0) {
stop("Invalid nesting of html_preserve directives")
}
# Identify all the top-level boundary markers. We want to find all of the
# elements of preserve_level whose value is 0 and preceding value is 1, or
# whose value is 1 and preceding value is 0. Since we know that preserve_level
# values can only go up or down by 1, we can simply shift preserve_level by
# one element and add it to preserve_level; in the result, any value of 1 is a
# match.
is_top_level <- 1 == (preserve_level + c(0, preserve_level[-length(preserve_level)]))
preserved <- character(0)
top_level_matches <- matches[is_top_level]
# Iterate backwards so string mutation doesn't screw up positions for future
# iterations
for (i in seq.int(length(top_level_matches) - 1, 1, by = -2)) {
start_outer <- top_level_matches[[i]]
start_inner <- start_outer + startmarker_len
end_inner <- top_level_matches[[i+1]]
end_outer <- end_inner + endmarker_len
id <- withPrivateSeed(
paste("preserve", paste(
format(as.hexmode(sample(256, 8, replace = TRUE)-1), width=2),
collapse = ""),
sep = "")
)
preserved[id] <- gsub(pattern, "", substr(strval, start_inner, end_inner-1))
strval <- paste(
substr(strval, 1, start_outer - 1),
id,
substr(strval, end_outer, nchar(strval)),
sep="")
substr(strval, start_outer, end_outer-1) <- id
}
list(value = strval, chunks = preserved)
}
#' @param strval Input string from which to extract/restore chunks.
#' @param chunks The `chunks` element of the return value of
#' `extractPreserveChunks`.
#' @return `restorePreserveChunks` returns a character vector with the
#' chunk IDs replaced with their original values.
#' @rdname htmlPreserve
#' @export
restorePreserveChunks <- function(strval, chunks) {
strval <- enc2utf8(strval)
chunks <- enc2utf8(chunks)
for (id in names(chunks))
strval <- gsub(id, chunks[[id]], strval, fixed = TRUE, useBytes = TRUE)
Encoding(strval) <- 'UTF-8'
strval
}
#' Knitr S3 methods
#'
#' These S3 methods are necessary to allow HTML tags to print themselves in
#' knitr/rmarkdown documents.
#'
#' @name knitr_methods
#' @param x Object to knit_print
#' @param ... Additional knit_print arguments
#' @param inline Whether or not the code chunk is inline.
NULL
#' @rdname knitr_methods
#' @export
knit_print.shiny.tag <- function(x, ..., inline = FALSE) {
x <- tagify(x)
output <- surroundSingletons(x)
deps <- resolveDependencies(findDependencies(x, tagify = FALSE), resolvePackageDir = FALSE)
content <- takeHeads(output)
head_content <- doRenderTags(tagList(content$head))
meta <- if (length(head_content) > 1 || head_content != "") {
list(structure(head_content, class = "shiny_head"))
}
meta <- c(meta, deps)
knitr::asis_output(
html_preserve(format(content$ui, indent=FALSE), inline),
meta = meta)
}
#' @rdname knitr_methods
#' @export
knit_print.html <- function(x, ..., inline = FALSE) {
deps <- resolveDependencies(findDependencies(x, tagify = FALSE))
knitr::asis_output(html_preserve(as.character(x), inline),
meta = if (length(deps)) list(deps))
}
#' @rdname knitr_methods
#' @export
knit_print.shiny.tag.list <- knit_print.shiny.tag
#' @rdname knitr_methods
#' @export
knit_print.html_dependency <- knit_print.shiny.tag
#' Include Content From a File
#'
#' Load HTML, text, or rendered Markdown from a file and turn into HTML.
#'
#' These functions provide a convenient way to include an extensive amount of
#' HTML, textual, Markdown, CSS, or JavaScript content, rather than using a
#' large literal R string.
#'
#' @param path The path of the file to be included. It is highly recommended to
#' use a relative path (the base path being the Shiny application directory),
#' not an absolute path.
#'
#' @rdname include
#' @name include
#' @aliases includeHTML
#' @export
includeHTML <- function(path) {
lines <- readLines(path, warn=FALSE, encoding='UTF-8')
if (detect_html_document(lines)) {
rlang::warn(c(
"`includeHTML()` was provided a `path` that appears to be a complete HTML document.",
"x" = paste("Path:", path),
"i" = paste(
"Use `tags$iframe()` to include an HTML document.",
"You can either ensure `path` is accessible in your app or document",
"(see e.g. `shiny::addResourcePath()`) and pass the relative path to",
"the `src` argument. Or you can read the contents of `path` and pass",
"the contents to `srcdoc`."
)
))
}
return(HTML(paste8(lines, collapse='\n')))
}
detect_html_document <- function(lines) {
if (length(lines) > 1) {
lines <- paste8(lines, collapse = "\n")
}
lines <- trimws(lines)
# A complete html document starts with doctype declaration or opening
if (!grepl("^|` tag
if (!grepl("$", lines, ignore.case = TRUE)) {
return(FALSE)
}
# There are more requirements for the HTML document to be technically complete
# and valid, but the above conditions are sufficient for us to warn that the
# document should not be treated as an HTML fragment.
TRUE
}
#' @note `includeText` escapes its contents, but does no other processing.
#' This means that hard breaks and multiple spaces will be rendered as they
#' usually are in HTML: as a single space character. If you are looking for
#' preformatted text, wrap the call with [pre()], or consider using
#' `includeMarkdown` instead.
#'
#' @rdname include
#' @export
includeText <- function(path) {
lines <- readLines(path, warn=FALSE, encoding='UTF-8')
return(paste8(lines, collapse='\n'))
}
#' @note The `includeMarkdown` function requires the `markdown`
#' package.
#' @rdname include
#' @export
includeMarkdown <- function(path) {
# markdown >= v1.3 switched from markdownToHTML() to mark()
html <- if (packageVersion("markdown") < "1.3") {
markdown::markdownToHTML(path, fragment.only = TRUE)
} else {
markdown::mark(path, output = NULL)
}
Encoding(html) <- 'UTF-8'
return(HTML(html))
}
#' @param ... Any additional attributes to be applied to the generated tag.
#' @rdname include
#' @export
includeCSS <- function(path, ...) {
lines <- readLines(path, warn=FALSE, encoding='UTF-8')
args <- dots_list(...)
if (is.null(args$type))
args$type <- 'text/css'
return(do.call(tags$style,
c(list(HTML(paste8(lines, collapse='\n'))), args)))
}
#' @rdname include
#' @export
includeScript <- function(path, ...) {
lines <- readLines(path, warn=FALSE, encoding='UTF-8')
return(tags$script(HTML(paste8(lines, collapse='\n')), ...))
}
#' Include content only once
#'
#' Use `singleton` to wrap contents (tag, text, HTML, or lists) that should
#' be included in the generated document only once, yet may appear in the
#' document-generating code more than once. Only the first appearance of the
#' content (in document order) will be used.
#'
#' @param x A [tag()], text, [HTML()], or list.
#' @param value Whether the object should be a singleton.
#'
#' @export
singleton <- function(x, value = TRUE) {
attr(x, "htmltools.singleton") <- if (isTRUE(value)) TRUE else NULL
return(x)
}
#' @rdname singleton
#' @export
is.singleton <- function(x) {
isTRUE(attr(x, "htmltools.singleton"))
}
#' Validate proper CSS formatting of a unit
#'
#' Checks that the argument is valid for use as a CSS unit of length.
#'
#' `NULL` and `NA` are returned unchanged.
#'
#' Single element numeric vectors are returned as a character vector with the
#' number plus a suffix of `"px"`.
#'
#' Single element character vectors must be `"auto"`, `"fit-content"`
#' or `"inherit"`, a number, or a length calculated by the `"calc"`
#' CSS function. If the number has a suffix, it must be valid: `px`,
#' `\%`, `ch`, `em`, `rem`, `pt`, `in`, `cm`,
#' `mm`, `ex`, `pc`, `vh`, `vw`, `vmin`, or
#' `vmax`.
#' If the number has no suffix, the suffix `"px"` is appended.
#'
#'
#' Any other value will cause an error to be thrown.
#'
#' @param x The unit to validate. Will be treated as a number of pixels if a
#' unit is not specified.
#' @return A properly formatted CSS unit of length, if possible. Otherwise, will
#' throw an error.
#' @examples
#' validateCssUnit("10%")
#' validateCssUnit(400) #treated as '400px'
#' @export
validateCssUnit <- function(x) {
if (is.null(x) || is.na(x))
return(x)
if (length(x) > 1 || (!is.character(x) && !is.numeric(x)))
stop('CSS units must be a single-element numeric or character vector')
# if the input is a character vector consisting only of digits (e.g. "960"),
# coerce it to a numeric value
if (is.character(x) && nchar(x) > 0 && gsub("\\d*", "", x) == "")
x <- as.numeric(x)
pattern <-
"^(auto|inherit|fit-content|calc\\(.*\\)|((\\.\\d+)|(\\d+(\\.\\d+)?))(%|in|cm|mm|ch|em|ex|rem|pt|pc|px|vh|vw|vmin|vmax))$"
if (is.character(x) &&
!grepl(pattern, x)) {
stop('"', x, '" is not a valid CSS unit (e.g., "100%", "400px", "auto")')
} else if (is.numeric(x)) {
x <- paste(x, "px", sep = "")
}
x
}
#' CSS string helper
#'
#' Convenience function for building CSS style declarations (i.e. the string
#' that goes into a style attribute, or the parts that go inside curly braces in
#' a full stylesheet).
#'
#' CSS uses `'-'` (minus) as a separator character in property names, but
#' this is an inconvenient character to use in an R function argument name.
#' Instead, you can use `'.'` (period) and/or `'_'` (underscore) as
#' separator characters. For example, `css(font.size = "12px")` yields
#' `"font-size:12px;"`.
#'
#' To mark a property as `!important`, add a `'!'` character to the end
#' of the property name. (Since `'!'` is not normally a character that can be
#' used in an identifier in R, you'll need to put the name in double quotes or
#' backticks.)
#'
#' Argument values will be converted to strings using
#' `paste(collapse = " ")`. Any property with a value of `NULL` or
#' `""` (after paste) will be dropped.
#'
#' @param ... Named style properties, where the name is the property name and
#' the argument is the property value. See Details for conversion rules.
#' @param collapse_ (Note that the parameter name has a trailing underscore
#' character.) Character to use to collapse properties into a single string;
#' likely `""` (the default) for style attributes, and either `"\n"`
#' or `NULL` for style blocks.
#'
#' @examples
#' padding <- 6
#' css(
#' font.family = "Helvetica, sans-serif",
#' margin = paste0(c(10, 20, 10, 20), "px"),
#' "padding!" = if (!is.null(padding)) padding
#' )
#'
#' @export
css <- function(..., collapse_ = "") {
props <- dots_list(...)
if (length(props) == 0) {
return(NULL)
}
if (is.null(names(props)) || any(names(props) == "")) {
stop("cssList expects all arguments to be named")
}
# Necessary to make factors show up as level names, not numbers
props[] <- lapply(props, paste, collapse = " ")
# Drop null args
props <- props[!sapply(props, empty)]
if (length(props) == 0) {
return(NULL)
}
# Translate camelCase, snake_case, and dot.case to kebab-case
# For standard CSS properties only, not CSS variables
is_css_var <- grepl("^--", names(props))
names(props)[!is_css_var] <- standardize_property_names(names(props)[!is_css_var])
# Create "!important" suffix for each property whose name ends with !, then
# remove the ! from the property name
important <- ifelse(grepl("!$", names(props), perl = TRUE), " !important", "")
names(props) <- sub("!$", "", names(props), perl = TRUE)
paste0(names(props), ":", props, important, ";", collapse = collapse_)
}
empty <- function(x) {
length(x) == 0 || (is.character(x) && !any(nzchar(x)))
}
standardize_property_names <- function(x) {
# camelCase to kebab-case
x <- gsub("([A-Z])", "-\\1", x)
x <- tolower(x)
# snake_case and dot.case to kebab-case
gsub("[._]", "-", x)
}
htmltools/R/html_escape.R 0000644 0001762 0000144 00000002414 14600330155 015105 0 ustar ligges users
#' Escape HTML entities
#'
#' Escape HTML entities contained in a character vector so that it can be safely
#' included as text or an attribute value within an HTML document
#'
#' @param text Text to escape
#' @param attribute Escape for use as an attribute value
#'
#' @return Character vector with escaped text.
#'
#' @export
htmlEscape <- local({
.htmlSpecials <- list(
`&` = '&',
`<` = '<',
`>` = '>'
)
.htmlSpecialsPattern <- paste(names(.htmlSpecials), collapse='|')
.htmlSpecialsAttrib <- c(
.htmlSpecials,
`'` = ''',
`"` = '"',
`\r` = '
',
`\n` = '
'
)
.htmlSpecialsPatternAttrib <- paste(names(.htmlSpecialsAttrib), collapse='|')
function(text, attribute=FALSE) {
pattern <- if(attribute)
.htmlSpecialsPatternAttrib
else
.htmlSpecialsPattern
text <- enc2utf8(as.character(text))
# Short circuit in the common case that there's nothing to escape
if (!any(grepl(pattern, text, useBytes = TRUE)))
return(text)
specials <- if(attribute)
.htmlSpecialsAttrib
else
.htmlSpecials
for (chr in names(specials)) {
text <- gsub(chr, specials[[chr]], text, fixed = TRUE, useBytes = TRUE)
}
Encoding(text) <- "UTF-8"
return(text)
}
})
htmltools/R/staticimports.R 0000644 0001762 0000144 00000007724 14600330155 015537 0 ustar ligges users # Generated by staticimports; do not edit by hand.
# ======================================================================
# Imported from pkg:staticimports
# ======================================================================
# Borrowed from pkgload:::dev_meta, with some modifications.
# Returns TRUE if `pkg` was loaded with `devtools::load_all()`.
devtools_loaded <- function(pkg) {
ns <- .getNamespace(pkg)
if (is.null(ns) || is.null(ns$.__DEVTOOLS__)) {
return(FALSE)
}
TRUE
}
get_package_version <- function(pkg) {
# `utils::packageVersion()` can be slow, so first try the fast path of
# checking if the package is already loaded.
ns <- .getNamespace(pkg)
if (is.null(ns)) {
utils::packageVersion(pkg)
} else {
as.package_version(ns$.__NAMESPACE__.$spec[["version"]])
}
}
is_installed <- function(pkg, version = NULL) {
installed <- isNamespaceLoaded(pkg) || nzchar(system_file_cached(package = pkg))
if (is.null(version)) {
return(installed)
}
if (!is.character(version) && !inherits(version, "numeric_version")) {
# Avoid https://bugs.r-project.org/show_bug.cgi?id=18548
alert <- if (identical(Sys.getenv("TESTTHAT"), "true")) stop else warning
alert("`version` must be a character string or a `package_version` or `numeric_version` object.")
version <- numeric_version(sprintf("%0.9g", version))
}
installed && isTRUE(get_package_version(pkg) >= version)
}
# Borrowed from pkgload::shim_system.file, with some modifications. This behaves
# like `system.file()`, except that (1) for packages loaded with
# `devtools::load_all()`, it will return the path to files in the package's
# inst/ directory, and (2) for other packages, the directory lookup is cached.
# Also, to keep the implementation simple, it doesn't support specification of
# lib.loc or mustWork.
system_file <- function(..., package = "base") {
if (!devtools_loaded(package)) {
return(system_file_cached(..., package = package))
}
if (!is.null(names(list(...)))) {
stop("All arguments other than `package` must be unnamed.")
}
# If package was loaded with devtools (the package loaded with load_all),
# also search for files under inst/, and don't cache the results (it seems
# more likely that the package path will change during the development
# process)
pkg_path <- find.package(package)
# First look in inst/
files_inst <- file.path(pkg_path, "inst", ...)
present_inst <- file.exists(files_inst)
# For any files that weren't present in inst/, look in the base path
files_top <- file.path(pkg_path, ...)
present_top <- file.exists(files_top)
# Merge them together. Here are the different possible conditions, and the
# desired result. NULL means to drop that element from the result.
#
# files_inst: /inst/A /inst/B /inst/C /inst/D
# present_inst: T T F F
# files_top: /A /B /C /D
# present_top: T F T F
# result: /inst/A /inst/B /C NULL
#
files <- files_top
files[present_inst] <- files_inst[present_inst]
# Drop cases where not present in either location
files <- files[present_inst | present_top]
if (length(files) == 0) {
return("")
}
# Make sure backslashes are replaced with slashes on Windows
normalizePath(files, winslash = "/")
}
# A wrapper for `system.file()`, which caches the package path because
# `system.file()` can be slow. If a package is not installed, the result won't
# be cached.
system_file_cached <- local({
pkg_dir_cache <- character()
function(..., package = "base") {
if (!is.null(names(list(...)))) {
stop("All arguments other than `package` must be unnamed.")
}
not_cached <- is.na(match(package, names(pkg_dir_cache)))
if (not_cached) {
pkg_dir <- system.file(package = package)
if (nzchar(pkg_dir)) {
pkg_dir_cache[[package]] <<- pkg_dir
}
} else {
pkg_dir <- pkg_dir_cache[[package]]
}
file.path(pkg_dir, ...)
}
})
htmltools/R/html_print.R 0000644 0001762 0000144 00000010067 14600330155 015004 0 ustar ligges users #' Make an HTML object browsable
#'
#' By default, HTML objects display their HTML markup at the console when
#' printed. `browsable` can be used to make specific objects render as HTML
#' by default when printed at the console.
#'
#' You can override the default browsability of an HTML object by explicitly
#' passing `browse = TRUE` (or `FALSE`) to the `print` function.
#'
#' @param x The object to make browsable or not.
#' @param value Whether the object should be considered browsable.
#' @return `browsable` returns `x` with an extra attribute to indicate
#' that the value is browsable.
#' @export
browsable <- function(x, value = TRUE) {
attr(x, "browsable_html") <- if (isTRUE(value)) TRUE else NULL
return(x)
}
#' @return `is.browsable` returns `TRUE` if the value is browsable, or
#' `FALSE` if not.
#' @rdname browsable
#' @export
is.browsable <- function(x) {
return(isTRUE(attr(x, "browsable_html", exact=TRUE)))
}
#' Implementation of the print method for HTML
#'
#' Convenience method that provides an implementation of the
#' [base::print()] method for HTML content.
#'
#' @param html HTML content to print
#' @param background Background color for web page
#' @param viewer A function to be called with the URL or path to the generated
#' HTML page. Can be `NULL`, in which case no viewer will be invoked.
#'
#' @return Invisibly returns the URL or path of the generated HTML page.
#'
#' @export
html_print <- function(html, background = "white", viewer = getOption("viewer", utils::browseURL)) {
# define temporary directory for output
www_dir <- tempfile("viewhtml")
dir.create(www_dir)
# define output file
index_html <- file.path(www_dir, "index.html")
# save file
save_html(html, file = index_html, background = background, libdir = "lib")
# show it
if (!is.null(viewer))
viewer(index_html)
invisible(index_html)
}
#' Save an HTML object to a file
#'
#' An S3 generic method for saving an HTML-like object to a file. The default
#' method copies dependency files to the directory specified via `libdir`.
#'
#' @param html HTML content to print.
#' @param file File path or connection. If a file path containing a
#' sub-directory, the sub-directory must already exist.
#' @param ... Further arguments passed to other methods.
#'
#' @export
save_html <- function(html, file, ...) {
UseMethod("save_html")
}
#' @rdname save_html
#' @param background Background color for web page.
#' @param libdir Directory to copy dependencies to.
#' @param lang Value of the `` `lang` attribute.
#' @export
save_html.default <- function(html, file, background = "white", libdir = "lib", lang = "en", ...) {
rlang::check_dots_empty()
force(html)
force(background)
force(libdir)
# ensure that the paths to dependencies are relative to the base
# directory where the webpage is being built.
if (is.character(file)) {
dir <- normalizePath(dirname(file), mustWork = TRUE)
file <- file.path(dir, basename(file))
owd <- setwd(dir)
on.exit(setwd(owd), add = TRUE)
}
rendered <- renderTags(html)
deps <- lapply(rendered$dependencies, function(dep) {
dep <- copyDependencyToDir(dep, libdir, FALSE)
dep <- makeDependencyRelative(dep, dir, FALSE)
dep
})
bodyBegin <- if (!isTRUE(grepl("
"
}
bodyEnd <- if (!is.null(bodyBegin)) {
""
}
# build the web-page
html <- c("",
sprintf('', lang),
"",
" ",
sprintf("", htmlEscape(background)),
renderDependencies(deps, c("href", "file")),
rendered$head,
"",
bodyBegin,
rendered$html,
bodyEnd,
"")
if (is.character(file)) {
# Write to file in binary mode, so \r\n in input doesn't become \r\r\n
con <- base::file(file, open = "w+b")
on.exit(close(con), add = TRUE)
} else {
con <- file
}
# write it
writeLines(html, con, useBytes = TRUE)
}
htmltools/R/fill.R 0000644 0001762 0000144 00000006666 14600330155 013564 0 ustar ligges users #' Allow tags to intelligently fill their container
#'
#' Create fill containers and items. If a fill item is a direct child of a fill
#' container, and that container has an opinionated height, then the item is
#' allowed to grow and shrink to its container's size.
#'
#' @param x a [tag()] object. Can also be a valid [tagQuery()] input if
#' `.cssSelector` is specified.
#' @param ... currently unused.
#' @param item whether or not to treat `x` as a fill item.
#' @param container whether or not to treat `x` as a fill container. Note, this
#' will set the CSS `display` property on the tag to `flex` which can change how
#' its direct children are rendered. Thus, one should be careful not to
#' mark a tag as a fill container when it needs to rely on other `display`
#' behavior.
#' @param overwrite whether or not to override previous calls to
#' `bindFillRole()` (e.g., to remove the item/container role from a tag).
#' @param .cssSelector A character string containing a CSS selector for
#' targeting particular (inner) tag(s) of interest. For more details on what
#' selector(s) are supported, see [tagAppendAttributes()].
#'
#' @returns The original tag object (`x`) with additional attributes (and a
#' [htmlDependency()]).
#'
#' @export
#' @examples
#'
#' tagz <- div(
#' id = "outer",
#' style = css(
#' height = "600px",
#' border = "3px red solid"
#' ),
#' div(
#' id = "inner",
#' style = css(
#' height = "400px",
#' border = "3px blue solid"
#' )
#' )
#' )
#'
#' # Inner doesn't fill outer
#' if (interactive()) browsable(tagz)
#'
#' tagz <- bindFillRole(tagz, container = TRUE)
#' tagz <- bindFillRole(tagz, item = TRUE, .cssSelector = "#inner")
#'
#' # Inner does fill outer
#' if (interactive()) browsable(tagz)
#'
bindFillRole <- function(x, ..., item = FALSE, container = FALSE, overwrite = FALSE, .cssSelector = NULL) {
check_dots_empty()
hasSelection <- FALSE
query <- NULL
if (!is.null(.cssSelector)) {
try(silent = TRUE, {
query <- tagQuery(x)$find(.cssSelector)
hasSelection <- length(query$selectedTags()) > 0
})
if (!hasSelection) {
rlang::warn(
paste0(
"`bindFillRole()` didn't find any tags matching the .cssSelector: '", .cssSelector, "'. ",
"Thus, it won't apply any fill roles."
),
class = "htmltools_fill_role_selector"
)
return(x)
}
}
if (!(inherits(x, "shiny.tag") || hasSelection)) {
rlang::warn(
paste0(
"`bindFillRole()` only works on htmltools::tag() objects (e.g., div(), p(), etc.), ",
"not objects of type '", class(x)[1], "'. "
),
class = "htmltools_fill_role_object"
)
return(x)
}
x <- tagAppendAttributes(
x, .cssSelector = .cssSelector,
class = if (item) "html-fill-item",
class = if (container) "html-fill-container"
)
if (container || item) {
x <- attachDependencies(x, fillDependencies(), append = TRUE)
}
if (!overwrite) {
return(x)
}
query <- query %||% tagQuery(x)
# removeClass() removes all occurrences of a given class
if (!item) {
query <- query$removeClass("html-fill-item")
}
if (!container) {
query <- query$removeClass("html-fill-container")
}
query$allTags()
}
fillDependencies <- function() {
htmlDependency(
name = "htmltools-fill",
version = get_package_version("htmltools"),
package = "htmltools",
src = "fill",
stylesheet = "fill.css"
)
}
htmltools/R/html_dependency.R 0000644 0001762 0000144 00000050277 14600330155 015775 0 ustar ligges users #' Define an HTML dependency
#'
#' Define an HTML dependency (i.e. CSS and/or JavaScript bundled in a
#' directory). HTML dependencies make it possible to use libraries like jQuery,
#' Bootstrap, and d3 in a more composable and portable way than simply using
#' script, link, and style tags.
#'
#' @param name Library name
#' @param version Library version
#' @param src Unnamed single-element character vector indicating the full path
#' of the library directory. Alternatively, a named character string with one
#' or more elements, indicating different places to find the library; see
#' Details.
#' @param meta Named list of meta tags to insert into document head
#' @param script Script(s) to include within the document head (should be
#' specified relative to the `src` parameter).
#' @param stylesheet Stylesheet(s) to include within the document (should be
#' specified relative to the `src` parameter).
#' @param head Arbitrary lines of HTML to insert into the document head
#' @param attachment Attachment(s) to include within the document head. See
#' Details.
#' @param package An R package name to indicate where to find the `src`
#' directory when `src` is a relative path (see
#' [resolveDependencies()]).
#' @param all_files Whether all files under the `src` directory are
#' dependency files. If `FALSE`, only the files specified in
#' `script`, `stylesheet`, and `attachment` are treated as
#' dependency files.
#'
#' @return An object that can be included in a list of dependencies passed to
#' [attachDependencies()].
#'
#' @details Each dependency can be located on the filesystem, at a relative or
#' absolute URL, or both. The location types are indicated using the names of
#' the `src` character vector: `file` for filesystem directory,
#' `href` for URL. For example, a dependency that was both on disk and at
#' a URL might use `src = c(file=filepath, href=url)`.
#'
#' `script` can be given as one of the following:
#' \itemize{
#' \item a character vector specifying various scripts to include relative to the
#' value of `src`.
#' Each is expanded into its own `",
collapse = ""
)
},
FUN.VALUE = character(1)
)
return(script)
}
# html_dependencies_as_character(list(
# htmlDependency("foo", "1.0",
# c(href="http://foo.com/bar%20baz/"),
# stylesheet="x y z.css"
# )
# ))
#
# html_dependencies_as_character(list(
# htmlDependency("foo", "1.0",
# c(href="http://foo.com/bar%20baz"),
# stylesheet="x y z.css"
# )
# ))
#
# html_dependencies_as_character(list(
# htmlDependency("foo", "1.0",
# "foo bar/baz",
# stylesheet="x y z.css"
# )
# ))
#
# html_dependencies_as_character(list(
# htmlDependency("foo", "1.0",
# "foo bar/baz/",
# stylesheet="x y z.css"
# )
# ))
#
htmltools/R/tag_query.R 0000644 0001762 0000144 00000132737 14600330155 014635 0 ustar ligges users # TODO-barret followup PR
# * onRender(x, fn) - tagFunction(x, fn)
## Methods not implemented
# * `$set_selected(selected)` & `$set(selected_item, pos)` - These methods are
# not available in jQuery and is very brittle in implementation. Do not pursue!
# * With `$set(selected, pos)` not implemented, `[[<-.tagQuery` should not be
# implemented
# * With `$set(selected, pos_vector)` not implemented, `[<-.tagQuery` should not
# be implemented
# * If not doing, `[[<-.tagQuery` or `[<-.tagQuery`, then `[[.tagQuery` and
# `[.tagQuery` should not be implemented. Same with `length.tagQuery`
# * `$set_children(...)` - jQuery does not have this. Instead, you can call
# `$empty()$append(...)`
# * jQuery.val() - Get the current value of the first element in the set of
# matched elements or set the value of every matched element.
# * jQuery.text() - Get the combined text contents of each element in the set of
# matched elements, including their descendants, or set the text contents of the
# matched elements.
# * jQuery.css() - Get the value of a computed style property for the first
# element in the set of matched elements or set one or more CSS properties for
# every matched element.
# * jQuery.prop() - Get the value of a property for the first element in the set
# of matched elements or set one or more properties for every matched element.
## Skip these implementations for now as the tagQuery methods are small and composable.
## Instead write them where they are needed since they are small.
## (Just like we don't wrap dplyr code)
# tagAppendAttributesAt <- function(tag, cssSelector, ...) {
# tagQuery(tag)$find(cssSelector)$addAttrs(...)$allTags()
# }
# tagAddClassAt <- function(tag, cssSelector, class) {
# tagQuery(tag)$find(cssSelector)$addClass(class)$allTags()
# }
# tagMutateAt <- function(x, cssSelector, fn) {
# tagQuery(tag)$find(cssSelector)$each(fn)$allTags()
# }
# tagFindAt <- function(x, cssSelector) {
# tagQuery(tag)$find(cssSelector)$selectedTags()
# }
# # Design notes for tagQuery:
# ## Using stock R objects
#
# Advantages of standard R objects recursion
# * Environments must be handled with care as they are pass by reference
# * It is easy to alter the current object in place
# * Difficult to create a new search path while altering in place
#
# Disadvantages of standard R objects recursion
# * Asking for a grandparent element is difficult.
# * Altering a grandparent element and having the change stay is impossible
# * Searching would need to be done at each stage, every time
# ## Using environments elements
#
# Advantages
# * Fast to convert to a "linked list" of tag environments
# * Access to parents
# * Calculations on siblings can now be done, even after alterations have been completed
# * Once a `find(".x")` has been completed, a set of element environment pointers can be stored.
# * This makes followup alterations have the minimal O(k) complexity (where k
# is _found_ elements), not O(n) + O(k) graph search + reconstruction and k
# _found_ element alterations
#
# Disadvantages
# * MUST be careful not alter the environment object before converting back to a
# list. (Ex: Do not remove the element environment's children)
# * The item returned is a set of environments that will alter in place. We will
# need to be careful about documenting and/or safeguarding this
# ## Final design choice:
# Use environment elements
# * Being able to search and have a list of eles to immediately look at and
# alter in place is AMAZING!
# * Being able to ask for a grandparent (or obj$parent$parent) and be able to
# alter it in place is AMAZING! This has a strongly influenced by jquery.
# ----------
# # Current design decisions
# * tagQuery objects or tag environments can NOT be used in UI. These objects
# MUST be converted back to standard tag objects.
# * tagFunctions will not be altered in place
# * To alter tagFunction()s, use the `onRender(x)` method to register a method
# to be called after `as.tags(x)` is called.
# * `onRender(x, expr)` will wrap create a tag function that will resolve the
# tags before running the expr.
## rlang::obj_address()
# Use to get a unique key for stacks
# Use `env$envKey` over `rlang::obj_address()`; 10x speed improvement
# Use for `has()` functionality
envirMap <- function() {
map <- fastmap()
list(
keys = map$keys,
asList = function() {
unname(map$as_list())
},
has = function(envir) {
map$has(envir$envKey)
},
add = function(envir) {
map$set(envir$envKey, TRUE)
},
remove = function(envir) {
map$remove(envir$envKey)
}
)
}
# Use for consistent `asList()` order
envirStack <- function() {
stack <- faststack()
list(
push = stack$push,
asList = stack$as_list,
uniqueList = function() {
unique(stack$as_list())
}
)
}
# (Used for `unique_envirStack()` only. Do not use directly!)
# Provides same interface as `envirStack()`, but checks for duplicates when
# when items are on their way in (with `push()`) instead of on the way out
# (with `uniqueList()`). This is faster when size is ~500 and above.
envirStackUni_ <- function() {
map <- fastmap()
stack <- faststack()
list(
push = function(env) {
key <- env$envKey
if (!map$has(key)) {
# mark the key as _seen_
map$set(key, TRUE)
# add the env
stack$push(env)
}
},
uniqueList = stack$as_list
)
}
# Use to retrieve unique environments (eg: `tq$parent()`)
# Provides same interface as `envirStack()`, but switches to the faster
# `envirStackUni_()` implementation when size hits 500.
envirStackUnique <- function() {
stack <- envirStack()
count <- 0
list(
push = function(env) {
count <<- count + 1
if (count == 500) {
# convert the current stack to a `envirStackUni_()`
newStack <- envirStackUni_()
walk(stack$asList(), newStack$push)
stack <<- newStack
}
stack$push(env)
},
uniqueList = function() {stack$uniqueList()}
)
}
# Copy all attributes that can be manually set
# ?attr
# Note that some attributes (namely ‘class’, ‘comment’, ‘dim’,
# ‘dimnames’, ‘names’, ‘row.names’ and ‘tsp’) are treated specially
# and have restrictions on the values which can be set.
copyAttributes <- function(from, to) {
attrVals <- attributes(from)
attrNames <- names(attrVals)
for (i in seq_along(attrNames)) {
attrName <- attrNames[i]
switch(
attrName,
class = , comment =, dim =, dimnames =, names =, row.names =, tsp = NULL,
{
# Copy over the attribute
attr(to, attrName) <- attrVals[[i]]
}
)
}
to
}
# Convert a list to an environment and keep class and attribute information
safeListToEnv <- function(x, classToAdd = NULL) {
xList <- x
ret <- list2env(xList, new.env(parent = emptyenv()))
ret <- copyAttributes(from = xList, to = ret)
oldClass(ret) <- c(classToAdd, oldClass(xList))
ret
}
# Convert any mixture of standard tag structures and tag environments into just
# tag environments.
#
# This method is heavily used within `tagQuery()$rebuild()` to enforce all
# standard tag objects are upgraded to tag environments.
#
# If the object is already a tag environment, it will recurse the conversion for
# each of the children
#
# Extras done:
# * Flatten all attributes by combining duplicate keys
# * Flatten the tag's children to a single list * Check for circular
# dependencies of tag environments
#
# (Do not export to encourage direct use of `tagQuery()`)
asTagEnv <- function(x) {
if (isTagQuery(x)) {
stop("`tagQuery()` object can not be provided to `asTagEnv()`")
}
if (!isTagEnv(x) && !isTag(x)) {
# force all methods to send in tags, lists / tagLists are not allowed
stop("`asTagEnv()` can only accept tag envs or tag objects. It does not accept `lists()` or `tagLists()`")
}
asTagEnv_(x, parent = x$parent)
}
# Checking for cycles is not performed as it is slow. With tagQuery methods not really
# opening the door for cycles to occur, it would be the user doing dangerous things.
# At this point, they should understand when a stack overflow occurs.
asTagEnv_ <- function(x, parent = NULL) {
isTagVal <- isTag(x)
isTagEnvVal <- isTagEnv(x)
if (isTagVal || isTagEnvVal) {
if (!isTagEnvVal) {
xList <- x
x <- safeListToEnv(xList, "shiny.tag.env")
# add parent env and key
x$parent <- parent
x$envKey <- obj_address(x)
}
if (!is.character(x[["name"]])) {
stop("A tag environment has lost its `$name`. Did you remove it?")
}
# This alters the env, but these fields should exist!
if (is.null(x[["attribs"]])) x$attribs <- list(placeholder = NULL)[0] # Empty named list
if (is.null(x[["children"]])) x$children <- list()
# Recurse through children
if (length(x$children) != 0) {
# Possible optimization... name the children tags to the formatted values.
# * Allows for faster child look up later.
# * Comes with the cost of always formatting the env values even if children names are not needed.
# Attributes may be dropped
# * Could replace with `x$children[] <- ....`
# * Leaving as is to see if people mis-use the children field
# Simplify the structures by flatting the tags
# Does NOT recurse to grand-children etc.
children <- flattenTagsRaw(x$children)
# Use a `for-loop` over `lapply` to avoid `lapply` overhead
for (i in seq_along(children)) {
child <- children[[i]]
if (!is.null(child)) {
children[[i]] <- asTagEnv_(child, parent = x)
}
}
x$children <- children
}
}
x
}
# This method MUST undo everything done in `asTagEnv(x)`
# Do not export to encourage direct use of `tagQuery()$selectedTags()`
# Only allow for tag environments to be passed in.
tagEnvToTags <- function(x) {
if (!isTagEnv(x)) {
stop("`tagEnvToTags(x)` must start with a tag environment")
}
tagEnvToTags_(x)
}
# Allows for all types of objects that can be put in a tag environment's `$children` field.
# Ex: tag environment, "text", 5, tagFunctions, etc.
tagEnvToTags_ <- function(x) {
if (isTagEnv(x)) {
xEl <- x
# Pull the names `name`, `attribs`, and `children` first to match `tag()` name order
envNames <- ls(envir = xEl, all.names = TRUE, sorted = FALSE)
newNames <- c(
"name", "attribs", "children",
if (length(envNames) > 5) {
# Pull remaining names if they exist
removeFromSet(envNames, c("name", "attribs", "children", "parent", "envKey"))
}
)
# Use mget to pull names in order to avoid always shuffling the values
x <- mget(newNames, xEl)
x <- copyAttributes(from = xEl, to = x)
oldClass(x) <- removeFromSet(oldClass(xEl), "shiny.tag.env")
# Recurse through children
children <- x$children
# Use a `for-loop` over `lapply` to avoid overhead
for (i in seq_along(children)) {
child <- children[[i]]
if (!is.null(child)) {
children[[i]] <- tagEnvToTags_(child)
}
}
x$children <- children
}
x
}
isTagEnv <- function(x) {
inherits(x, "shiny.tag.env")
}
isTagQuery <- function(x) {
inherits(x, "shiny.tag.query")
}
assertNotTagEnvLike <- function(x, fnName) {
if (isTagEnv(x)) {
stop("Tag environment objects (i.e., `tagQuery()`'s tag structure) are not allowed to be used as if they are regular `tag()` objects. Did you forget to call `$allTags()` or `$selectedTags()`?", call. = FALSE)
}
if (isTagQuery(x)) {
stop("`tagQuery()` objects are not allowed to be used as if they are regular `tag()` objects. Did you forget to call `$allTags()` or `$selectedTags()`?", call. = FALSE)
}
invisible()
}
shinyTagEnvStr <- ""
#' @export
as.tags.shiny.tag.env <- function(x, ...) {
stop("Method not allowed", call. = TRUE)
# as.tags(tagEnvToTags(x), ...)
}
#' @export
print.shiny.tag.env <- function(x, ...) {
cat(shinyTagEnvStr, "\n")
print(tagEnvToTags(x), ...)
}
#' @export
format.shiny.tag.env <- function(x, ...) {
format(tagEnvToTags(x), ...)
}
#' @export
as.character.shiny.tag.env <- function(x, ...) {
as.character(tagEnvToTags(x), ...)
}
#' @export
str.shiny.tag.env <- function(object, ...) {
cat(shinyTagEnvStr, "\n")
str(tagEnvToTags(object), ...)
}
#' @export
as.tags.shiny.tag.query <- function(x, ...) {
tagQueryAsTagErr()
}
#' @export
print.shiny.tag.query <- function(x, ...) {
tagQ <- x
cat("`$allTags()`:\n")
allTags <- tagQ$allTags()
print(allTags)
selectedTags <- tagQ$selectedTags()
cat("\n`$selectedTags()`:")
if (length(selectedTags) == 0) {
cat(" (Empty selection)\n")
} else {
# Convert allTags to same style of object as selected tags
if (!isTagList(allTags)) allTags <- tagList(allTags)
allTags <- tagListPrintAsList(!!!allTags)
if (identical(allTags, selectedTags)) {
cat(" `$allTags()`\n")
} else {
cat("\n")
print(selectedTags)
}
}
invisible(x)
}
#' @export
format.shiny.tag.query <- function(x, ...) {
tagQueryAsTagErr()
}
#' @export
as.character.shiny.tag.query <- function(x, ...) {
tagQueryAsTagErr()
}
tagQueryAsTagErr <- function() {
stop(
"`tagQuery()` objects can not be written directly as HTML tags.",
"Call either `$allTags()` or `$selectedTags()` to extract the tags of interest.",
call. = FALSE
)
}
#' Query and modify HTML tags
#'
#' `r lifecycle::badge("experimental")`\cr\cr `tagQuery()` provides a
#' [`jQuery`](https://jquery.com/) inspired interface for querying and modifying
#' [tag()] (and [tagList()]) objects.
#'
#' @section Altered Tag structure:
#'
#' For performance reasons, the input tag structure to `tagQuery()` will be
#' altered into a consistently expected shape.
#'
#' Some alterations include:
#' * tags flattening their `$children` fields into a single `list()`
#' * tags relocating any attribute `html_dependency() to be located in `$children`
#' * `tagList()`-like structures relocating any attribute html dependency to
#' be a entry in its list structure.
#'
#' While the resulting tag shape has possibly changed,
#' `tagQuery()`'s' resulting tags will still render
#' to the same HTML value (ex: [`renderTags()`]) and
#' HTML dependencies (ex: [`findDependencies()`]).
#'
#' @param tags A [tag()], [tagList()], or [list()] of tags.
#' @return A class with methods that are described below. This class can't be
#' used directly inside other [tag()] or a [renderTags()] context, but
#' underlying HTML tags may be extracted via `$allTags()` or
#' `$selectedTags()`.
#' @export
tagQuery <- function(tags) {
if (isTagQuery(tags)) {
# Return tag query object as is
return(tags)
}
# Make a new tag query object from the root element of `tags`
# * Set the selected to `list(tags)`
if (isTagEnv(tags)) {
# Rebuild pseudo root tag
pseudoRoot <- asTagEnv(
findPseudoRootTag(tags)
)
return(
tagQuery_(pseudoRoot, list(tags))
)
}
# If `tags` is a list of tagEnvs...
# * Make sure they share the same root element and
# * Set the selected elements to `tags`
if (!isTag(tags) && (is.list(tags) || isTagList(tags))) {
# If it is a list, flatten them for easier/consisten inspection
tags <- flattenTagsRaw(tags)
tagsIsTagEnv <- vapply(tags, isTagEnv, logical(1))
# If one of the elements is a tag env, verify that all tagEnvs share the same root.
if (any(tagsIsTagEnv)) {
if (any(!tagsIsTagEnv)) {
notTagEnvPos <- which(!tagsIsTagEnv)
# It is not known how a middle of the tree tagEnv should be combined with a standard tag
stop(
"`tagQuery(tags=)` can not be a mix of tag environments and standard tag objects.\n",
"Items at positions `c(", paste0(notTagEnvPos, collapse = ", "), ")` ",
"are not tag environments."
)
}
pseudoRootStack <- envirStackUnique()
walk(tags, function(el) {
pseudoRootStack$push(findPseudoRootTag(el))
})
pseudoRoots <- pseudoRootStack$uniqueList()
if (length(pseudoRoots) != 1) {
stop("All tag environments supplied to `tagQuery()` must share the same root element.")
}
# Rebuild pseudo root tag
pseudoRoot <- asTagEnv(pseudoRoots[[1]])
return(
tagQuery_(pseudoRoot, tags)
)
}
}
# Convert standard tags to tag envs
root <- asTagEnv(
wrapWithPseudoRootTag(tags)
)
# Select the top level tags
selected <- tagQueryFindResetSelected(root)
if (length(selected) == 0) {
stop(
"The initial set of tags supplied to `tagQuery()` must have at least 1 standard tag object.",
" Ex: `div()`"
)
}
tagQuery_(root, selected)
}
#' @rdname tagQuery
#' @aliases NULL
#' @usage NULL
tagQuery_ <- function(
pseudoRoot,
# Using a trailing `_` to avoid name collisions
selected_
) {
if (!isPseudoRootTag(pseudoRoot)) {
stop("`tagQuery_(pseudoRoot=)` must be a pseudoRoot tag environment")
}
# Use `var_` names to avoid namespace collision
# Make sure all elements are tag envs
rebuild_ <- function() {
# safe to do as `pseudoRoot` will never be turned into a standard list
asTagEnv(pseudoRoot)
}
newTagQuery <- function(selected) {
tagQuery_(pseudoRoot, selected)
}
setSelected <- function(selected) {
selected <- selected %||% list()
if (!is.list(selected)) {
stop("`selected` must be a `list()`")
}
selected <- FilterI(selected, f = function(el, i) {
if (!isTagEnv(el)) {
stop(
"`setSelected(selected=)` received a list item at position `", i, "`",
" that was not a tag environment"
)
}
!isPseudoRootTag(el)
})
selected
}
selected_ <- setSelected(selected_)
self <-
structure(
class = "shiny.tag.query",
list(
#' @details
#'
#' # Vignette
#'
#' To get started with using `tagQuery()`, visit
#' .
#'
#' # Methods
#'
#' Unless otherwise stated, `tagQuery()` methods accept a character
#' vector as input.
#'
#' ## Query methods
#'
#' Query methods identify particular subsets of the root tag using CSS
#' selectors (or R functions).
#'
#' ### Children
#'
#' * `$find(cssSelector)`: Get the descendants of
#' each selected tag, filtered by a `cssSelector`.
find = function(cssSelector) {
newTagQuery(
tagQueryFindAll(selected_, cssSelector)
)
},
#' * `$children(cssSelector = NULL)`: Get the direct
#' children of each selected tag, optionally filtered by a
#' `cssSelector`.
children = function(cssSelector = NULL) {
newTagQuery(
tagQueryFindChildren(selected_, cssSelector)
)
},
#' ### Siblings
#'
#' * `siblings(cssSelector = NULL)`: Get the
#' siblings of each selected tag, optionally filtered by a
#' `cssSelector`.
siblings = function(cssSelector = NULL) {
newTagQuery(
tagQueryFindSiblings(selected_, cssSelector)
)
},
#' ### Parents
#'
#' * `$parent(cssSelector = NULL)`: Get the parent
#' of each selected tag, optionally filtered by a `cssSelector`.
parent = function(cssSelector = NULL) {
newTagQuery(
tagQueryFindParent(selected_, cssSelector)
)
},
#' * `$parents(cssSelector = NULL)`: Get the
#' ancestors of each selected tag, optionally filtered by a
#' `cssSelector`.
parents = function(cssSelector = NULL) {
newTagQuery(
tagQueryFindParents(selected_, cssSelector)
)
},
#' * `$closest(cssSelector = NULL)`: For each selected tag, get the closest
#' ancestor tag (including itself) satisfying a `cssSelector`. If
#' `cssSelector = NULL`, it is equivalent to calling `$selectedTags()`.
closest = function(cssSelector = NULL) {
newTagQuery(
tagQueryFindClosest(selected_, cssSelector)
)
},
#' ### Custom filter
#'
#' * `$filter(fn)`: Filter the selected tags to those for which `fn(x,
#' i)` returns `TRUE`. In addition to an R function with two arguments
#' (the selected tag `x` and the index `i`), `fn` may also be a valid
#' CSS selector.
filter = function(fn) {
newSelected <- tagQueryFindFilter(selected_, fn)
rebuild_()
newTagQuery(newSelected)
},
#' ### Length
#'
#' * `$length()`: Number of tags that have been selected.
length = function() {
length(selected_)
},
#' ### Reset
#'
#' * `$resetSelected()`: Reset selected tags to the `$allTags()` tag. Useful
#' in combination with `$replaceWith()` since it empties the selection.
resetSelected = function() {
newTagQuery(
tagQueryFindResetSelected(pseudoRoot)
)
},
#' ## Modify methods
#'
#' Unlike query methods, modify methods modify the `tagQuery()` object.
#'
#' ### Attributes
#'
#' * `$addClass(class)`: Adds class(es) to each selected tag.
addClass = function(class) {
tagQueryClassAdd(selected_, class)
self
},
#' * `$removeClass(class)`: Removes class(es) to each selected tag.
removeClass = function(class) {
tagQueryClassRemove(selected_, class)
self
},
#' * `$toggleClass(class)`: Adds class(es) that don't already exist and
#' removes class(es) that do already exist (for each selected tag).
toggleClass = function(class) {
tagQueryClassToggle(selected_, class)
self
},
#' * `$hasClass(class)`: Does each selected tag have all the provided
#' class(es)?
hasClass = function(class) {
tagQueryClassHas(selected_, class)
},
#' * `$addAttrs(...)`: Add a set of attributes to each selected tag.
addAttrs = function(...) {
tagQueryAttrsAdd(selected_, ...)
self
},
#' * `$removeAttrs(attrs)`: Remove a set of attributes from each
#' selected tag.
removeAttrs = function(attrs) {
tagQueryAttrsRemove(selected_, attrs)
self
},
#' * `$hasAttrs(attr)`: Do each selected tags have all of the attributes?
hasAttrs = function(attrs) {
tagQueryAttrsHas(selected_, attrs)
},
#' ### Children
#'
#' * `$append(...)`: For each selected tag, insert `...` **after** any
#' existing children.
append = function(...) {
tagQueryChildrenAppend(selected_, ...)
self
},
#' * `$prepend(...)`: For each selected tag, insert `...` **before** any
#' existing children.
prepend = function(...) {
tagQueryChildrenPrepend(selected_, ...)
self
},
#' ### Siblings
#'
#' * `$after(...)`: Add all `...` objects as siblings after each of the
#' selected tags.
after = function(...) {
tagQuerySiblingAfter(selected_, ...)
self
},
#' * `$before(...)`: Add all `...` objects as siblings before each of
#' the selected tags.
before = function(...) {
tagQuerySiblingBefore(selected_, ...)
self
},
#' ### Custom
#'
#' * `$each(fn)`: Modify each selected tag with a function `fn`. `fn`
#' should accept two arguments: the first is the selected tag and second
#' is the selected tags position index. Since the selected tag is a
#' reference, any modifications to it will also modify the `tagQuery()`
#' object.
each = function(fn) {
if (length(selected_) > 0) {
tagQueryEach(selected_, fn)
rebuild_()
}
self
},
#' ## Replace methods
#'
#' * `$replaceWith(...)`: Replace all selected tags with `...` in the
#' root tag and clear the selection.
replaceWith = function(...) {
tagQuerySiblingReplaceWith(selected_, ...)
newTagQuery(list())
},
#' * `$remove(...)`: Remove all selected tags from the root tag and
#' clear the current selection.
remove = function() {
tagQuerySiblingRemove(selected_)
# Remove items from selected info
newTagQuery(list())
},
#' * `$empty()`: Remove any children of each selected tag. Use this
#' method before calling `$append(...)` to replace the children of
#' each selected tag, with other content.
empty = function() {
tagQueryChildrenEmpty(selected_)
self
},
#' ## Extract HTML tags
#'
#' * `$allTags()`: Return the (possibly modified) root `tags`.
allTags = function() {
tagQueryTopLevelTags(pseudoRoot)
},
#' * `$selectedTags()`: Return a [tagList()] of the currently selected
#' tags.
selectedTags = function() {
tagQuerySelectedAsTags(selected_)
}
#' @examples
#' tagQ <- tagQuery(div(a()))
#' tagQ$find("a")$addClass("foo")
#' tagQ
#'
#' # To learn more, visit https://rstudio.github.io/htmltools/articles/tagQuery.html
)
)
self
}
validatePosition <- function(position, selected) {
if (!is.numeric(position)) {
stop("`position` must be a numeric value")
}
if (length(position) != 1) {
stop("`position` must have a length equal to 1")
}
if (position <= 0) {
stop("`position` must be greater than 0")
}
if (position > length(selected)) {
stop(
"`position` must be less than or equal to the length of the selected elements: ",
length(selected)
)
}
}
validateFnCanIterate <- function(fn) {
if (!is.function(fn)) {
stop("`fn` must be a function")
}
fnFormals <- formals(fn)
if (! ("..." %in% names(fnFormals))) {
if (length(fnFormals) < 2) {
stop(
"`fn(selected_i, i)` must be a function that accepts at least two arguments: ",
"`selected[[i]]` and `i` "
)
}
}
}
isPseudoRootTag <- function(x) {
name <- x$name
isTag(x) && !is.null(name) && isTRUE(name == "TagQueryPseudoRoot")
}
findPseudoRootTag <- function(el) {
while (!is.null(el$parent)) {
el <- el$parent
}
el
}
# Wrap the top level tags in the tagQuery() in a `tagQuery` tag object.
# This allows for appending and prepending elements to the top level tags.
# (Don't fight the structures... embrace them!)
wrapWithPseudoRootTag <- function(x) {
tagSetChildren(
tag("TagQueryPseudoRoot", list()),
x
)
}
# Return a tag env, tagList(tag envs), or NULL
tagQueryGetRoot <- function(root) {
children <- root$children
len <- length(children)
if (len == 1) {
children[[1]]
} else if (len > 1) {
tagList(!!!children)
} else {
# no children?
NULL
}
}
# Return a list of the manually selected elements
tagQuerySelected <- function(selected) {
if (length(selected) == 1 && isPseudoRootTag(selected[[1]])) {
list()
} else {
selected
}
}
# # Return the `i`th position of the manually selected elements
# tagQueryGet <- function(selected, position) {
# selected <- tagQuerySelected(selected)
# validatePosition(position, selected)
# selected[[position]]
# }
# Return the top level tags as a tagList or a single tag
tagQueryTopLevelTags <- function(pseudoRoot) {
children <- tagEnvToTags(pseudoRoot)$children
len <- length(children)
if (len == 1) {
# single top level tag
children[[1]]
} else {
# 0 or >1 top leve tags
tagList(!!!children)
}
}
tagListPrintAsList <- function(...) {
x <- tagList(...)
attr(x, "print.as.list") <- TRUE
x
}
tagQuerySelectedAsTags <- function(selected) {
# return as a `tagList()` with a special attr that will cause it to print like a list
tagListPrintAsList(!!!lapply(selected, tagEnvToTags))
}
as_character2 <- function(...) {
as.character(
# MUST call `unlist()` to allow for vector items in `list2()`
unlist(
list2(...),
use.names = FALSE
)
)
}
FilterI <- function (f, x) {
ind <- as.logical(
Map(x, seq_along(x), f = f)
)
x[which(ind)]
}
# Call `.f(x[[i]], ...)` for all values of i
walk <- function(.x, .f, ...) {
for (i in seq_along(.x)) {
.f(.x[[i]], ...)
}
NULL
}
walk2 <- function(.x, .y, .f, ...) {
if (length(.x) != length(.y)) {
stop(".x and .y must be the same length.")
}
for (i in seq_along(.x)) {
.f(.x[[i]], .y[[i]], ...)
}
NULL
}
# Call `.f(x[[i]])` in reverse order
# walk_rev <- function(.x, .f, ...) {
# for (i in rev(seq_along(.x))) {
# .f(.x[[i]], ...)
# }
# NULL
# }
# Calls `.f(x[[i]], i, ...)`
walkI <- function(.x, .f, ...) {
for (i in seq_along(.x)) {
.f(.x[[i]], i, ...)
}
NULL
}
# Calls `.f(x[[i]], i, ...)` in reverse order
walkIRev <- function(.x, .f, ...) {
for (i in rev(seq_along(.x))) {
.f(.x[[i]], i, ...)
}
NULL
}
# Return function that will verify elements before performing `func(els, fn)`
selectedWalkGen <- function(func) {
force(func)
function(els, fn) {
if (is.null(els)) return(list())
if (!is.list(els)) {
stop("A list() must be supplied")
}
if (!is.function(fn)) {
stop("`fn` must be a function")
}
# Make sure each item in list is a tag env
walkI(els, function(el, i) {
if (!is.null(el)) {
if (isTag(el) && !isTagEnv(el)) {
str(el)
stop(
"Object in position `", i, "` is a regular `tag()` and not a tag environment.",
"\nDid you forget to call `$rebuild()`?"
)
}
}
})
func(els, fn)
}
}
tagQueryWalk <- selectedWalkGen(walk)
# selectedWalkRev <- selectedWalkGen(walkRev)
selectedWalkI <- selectedWalkGen(walkI)
selectedWalkIRev <- selectedWalkGen(walkIRev)
tagQueryLapply <- selectedWalkGen(lapply)
# Perform `fn` on each el in els
tagQueryEach <- function(els, fn) {
validateFnCanIterate(fn)
selectedWalkI(els, fn)
}
# For each el in els, go to el parent and find el's position
# Then call `fn(parent, el, elPos)`
# Perform this matching in reverse order
tagQueryMatchChildRev <- function(els, func) {
tagQueryWalk(els, function(el) {
if (!isTagEnv(el)) return()
elKey <- el$envKey
elParent <- el$parent
# Walk in reverse to be able to remove all matches in a single pass
selectedWalkIRev(elParent$children, function(child, childPos) {
if (!isTagEnv(child)) return()
childKey <- child$envKey
if (elKey == childKey) {
func(elParent, el, childPos)
# Make sure to rebuild the parent tag into tag envs
# Their internal structures will have changed
asTagEnv(elParent)
}
})
})
}
# Remove each el in els from their parent.
# Also remove parent pointer from within el
tagQuerySiblingRemove <- function(els) {
tagQueryMatchChildRev(els, function(elParent, el, childPos) {
# remove parent / child relationship
el$parent <- NULL
elParent$children[[childPos]] <- NULL
})
}
# Add siblings after each el
tagQuerySiblingAfter <- function(els, ...) {
tagQueryMatchChildRev(els, function(elParent, el, childPos) {
tagInsertChildren(elParent, after = childPos, ...)
})
}
# Add siblings before each el
tagQuerySiblingBefore <- function(els, ...) {
tagQueryMatchChildRev(els, function(elParent, el, childPos) {
tagInsertChildren(elParent, after = childPos - 1, ...)
})
}
# Replace all `el` objects with `...`
tagQuerySiblingReplaceWith <- function(els, ...) {
tagQueryMatchChildRev(els, function(elParent, el, childPos) {
# Remove the current element
el$parent <- NULL
elParent$children[[childPos]] <- NULL
# Replace with ... content where the child was
tagInsertChildren(elParent, after = childPos - 1, ...)
})
}
tagQueryChildrenSet <- function(els, ...) {
tagQueryWalk(els, function(el) {
if (!isTagEnv(el)) return()
tagSetChildren(el, ...)
# Make sure to rebuild the el and its children
asTagEnv(el)
})
}
tagQueryChildrenEmpty <- function(els) {
# Do not include any arguments.
# `dots_list()` returns an empty named list()
tagQueryChildrenSet(els)
}
tagQueryChildrenAppend <- function(els, ...) {
tagQueryWalk(els, function(el) {
if (!isTagEnv(el)) return()
tagInsertChildren(el, after = length(el$children), ...)
# Make sure to rebuild the el and its children
asTagEnv(el)
})
}
tagQueryChildrenPrepend <- function(els, ...) {
tagQueryChildrenInsert(els, after = 0, ...)
}
tagQueryChildrenInsert <- function(els, after, ...) {
tagQueryWalk(els, function(el) {
if (!isTagEnv(el)) return()
tagInsertChildren(el, after = after, ...)
# Make sure to rebuild the el and its children
asTagEnv(el)
})
}
tagEnvRemoveAttribs <- function(el, attrs) {
el$attribs[names(el$attribs) %in% attrs] <- NULL
el
}
# Add attribute values
tagQueryAttrsAdd <- function(els, ...) {
tagQueryWalk(els, function(el) {
if (!isTagEnv(el)) return()
tagAppendAttributes(el, ...)
})
}
# Remove attribute values
tagQueryAttrsRemove <- function(els, attrs) {
attrs <- as_character2(attrs)
if (length(attrs) < 1) return()
if (!is.character(attrs)) {
stop("`attrs` must be a charcter vector of attributes to remove")
}
tagQueryWalk(els, function(el) {
if (!isTagEnv(el)) return()
tagEnvRemoveAttribs(el, attrs)
})
}
# Check if els have attributes
tagQueryAttrsHas <- function(els, attrs) {
attrs <- as_character2(attrs)
if ((length(attrs) == 0) || (!is.character(attrs))) {
stop("`attrs` must be a character vector", call. = FALSE)
}
unlist(
tagQueryLapply(els, function(el) {
if (!isTagEnv(el)) return(FALSE)
for (attr in attrs) {
if (!tagHasAttribute(el, attr)) {
return(FALSE)
}
}
# All attrs found
return(TRUE)
}),
use.names = FALSE
)
}
prepCssClass <- function(class) {
class <- as_character2(class)
if (length(class) == 0 || !is.character(class)) {
stop("`class` must resolve to a character value with a length of at least 1")
}
class
}
getCssClass <- function(class) {
splitCssClass(prepCssClass(class))
}
splitCssClass <- function(class) {
if (!is.character(class)) {
stop("tagGetAttribute(x, \"class\") did not return a character value")
}
if (length(class) > 1) {
class <- paste0(class, collapse = " ")
}
strsplit(class, "\\s+")[[1]]
}
joinCssClass <- function(classes) {
if (length(classes) == 0) {
NULL
} else {
paste0(classes, collapse = " ")
}
}
# return list of logical values telling if the classes exists
tagQueryClassHas <- function(els, class) {
# Quit early if class == NULL | character(0)
if (length(class) == 0) {
return(rep(FALSE, length(els)))
}
classes <- getCssClass(class)
unlist(
tagQueryLapply(els, function(el) {
if (!isTagEnv(el)) return(FALSE)
classVal <- tagGetAttribute(el, "class")
if (isNonConformClassValue(classVal)) {
return(FALSE)
}
elClasses <- splitCssClass(classVal)
all(classes %in% elClasses)
}),
use.names = FALSE
)
}
removeFromSet <- function(set, vals) {
# removes the call to `unique()` with `setdiff`
set[match(set, vals, 0L) == 0L]
}
isNonConformClassValue <- function(classVal) {
length(classVal) == 0 ||
(!is.character(classVal)) ||
anyNA(classVal)
}
tagEnvSetClassAttrib <- function(el, classes) {
class <- joinCssClass(classes)
classAttribPos <- which(names(el$attribs) == "class")
isClassLen <- length(classAttribPos)
if (isClassLen == 0) {
# Store new class value
return(
tagAppendAttributes(el, class = class)
)
}
# isClassLen > 0
if (isClassLen > 1) {
# Remove other occurrences of class
el$attribs[classAttribPos[-1]] <- NULL
}
# Overwrite "class" attrib
el$attribs[[classAttribPos[1]]] <- class
el
}
# add classes that don't already exist
tagQueryClassAdd <- function(els, class) {
# Quit early if class == NULL | character(0)
if (length(class) == 0) return()
classes <- getCssClass(class)
tagQueryWalk(els, function(el) {
if (!isTagEnv(el)) return()
classVal <- tagGetAttribute(el, "class")
if (isNonConformClassValue(classVal)) {
tagAppendAttributes(el, class = joinCssClass(classes))
} else {
elClasses <- splitCssClass(classVal)
newClasses <- c(elClasses, removeFromSet(classes, elClasses))
tagEnvSetClassAttrib(el, newClasses)
}
})
}
# remove classes that exist
tagQueryClassRemove <- function(els, class) {
# Quit early if class == NULL | character(0)
if (length(class) == 0) return()
classes <- getCssClass(class)
tagQueryWalk(els, function(el) {
if (!isTagEnv(el)) return()
classVal <- tagGetAttribute(el, "class")
if (isNonConformClassValue(classVal)) return()
elClasses <- splitCssClass(classVal)
newClasses <- removeFromSet(elClasses, classes)
tagEnvSetClassAttrib(el, newClasses)
})
}
# toggle class existence depending on if they already exist or not
tagQueryClassToggle <- function(els, class) {
# Quit early if class == NULL | character(0)
if (length(class) == 0) return()
classes <- getCssClass(class)
tagQueryWalk(els, function(el) {
if (!isTagEnv(el)) return()
classVal <- tagGetAttribute(el, "class")
if (isNonConformClassValue(classVal)) return()
elClasses <- splitCssClass(classVal)
hasClass <- (classes %in% elClasses)
if (any(hasClass)) {
elClasses <- removeFromSet(elClasses, classes)
}
if (any(!hasClass)) {
elClasses <- c(elClasses, classes[!hasClass])
}
tagEnvSetClassAttrib(el, elClasses)
})
}
# Return a list of `root$children`.
# This may change if root ends up becoming a list of elements
tagQueryFindResetSelected <- function(pseudoRoot) {
if (!isTagEnv(pseudoRoot)) {
stop("`pseudoRoot` must be a tag environment")
}
Filter(pseudoRoot$children, f = isTagEnv)
}
# Return a list of the unique set of parent elements
tagQueryFindParent <- function(els, cssSelector = NULL) {
parentStack <- envirStackUnique()
pushFn <- pushFnWrapper(parentStack, cssSelector)
tagQueryWalk(els, function(el) {
if (!isTagEnv(el)) return()
pushFn(el$parent)
})
parentStack$uniqueList()
}
# Return a list of the unique set of ancestor elements
# * By only looking for elements that have not been seen before, searching is as
# lazy as possible
# * Must traverse all parents; If cssSelector exists, only return found parents
# that match selector.
# * Search using depth-first. This does not match jQuery's implementation.
tagQueryFindParents <- function(els, cssSelector = NULL) {
# Use the map for `has()` and stack for `values()`
ancestorsMap <- envirMap()
ancestorsStack <- envirStackUnique()
# func to add to the ancestor stack
pushFn <- pushFnWrapper(ancestorsStack, cssSelector)
# For every element
tagQueryWalk(els, function(el) {
# Make sure it is a tag environment
if (!isTagEnv(el)) return()
# While traversing up the parents...
while (!is.null(el <- el$parent)) {
# If the element has been seen before...
if (ancestorsMap$has(el)) {
# Stop traversing, as any matching parent found would be removed
# (unique info only)
return()
}
# Mark the ancestor as visited
ancestorsMap$add(el)
# Add the element to the return set
pushFn(el)
}
})
ancestorsStack$uniqueList()
}
# Return a unique list of the closest ancestor elements that match the css selector
# Should behave VERY similarly to $parents()
tagQueryFindClosest <- function(els, cssSelector = NULL) {
if (is.null(cssSelector)) {
return(els)
}
selector <- cssSelectorToSelector(cssSelector)
# use the map for `has()` and stack for `values()`
ancestorsMap <- envirMap()
closestStack <- envirStackUnique()
# For every element
tagQueryWalk(els, function(el) {
# Make sure it is a tag environment
if (!isTagEnv(el)) return()
# While traversing up the parents...
while (!is.null(el)) {
# If the element has been seen before...
if (ancestorsMap$has(el)) {
# Stop traversing, as any matching parent found would be removed
# (unique info only)
return()
}
# Mark the ancestor as visited
ancestorsMap$add(el)
# If it is a match...
if (elMatchesSelector(el, selector)) {
# Add to return value
closestStack$push(el)
return()
}
# set to parent element and repeat
el <- el$parent
}
})
closestStack$uniqueList()
}
# Get all unique children tag envs
tagQueryFindChildren <- function(els, cssSelector = NULL) {
childrenStack <- envirStackUnique()
pushFn <- pushFnWrapper(childrenStack, cssSelector)
tagQueryWalk(els, function(el) {
if (!isTagEnv(el)) return()
tagQueryWalk(el$children, pushFn)
})
childrenStack$uniqueList()
}
# Return all unique siblings of each el in els
tagQueryFindSiblings <- function(els, cssSelector = NULL) {
siblingStack <- envirStackUnique()
pushFn <- pushFnWrapper(siblingStack, cssSelector)
tagQueryWalk(els, function(el) {
if (!isTagEnv(el)) return()
elKey <- el$envKey
tagQueryWalk(el$parent$children, function(sibling) {
if (!isTagEnv(sibling)) return()
siblingKey <- sibling$envKey
if (elKey != siblingKey) {
pushFn(sibling)
}
})
})
siblingStack$uniqueList()
}
# Filter the selected elements using a function
# The answer of `fn(el, i)` should work in an `if` block
tagQueryFindFilter <- function(els, fn) {
if (is.character(fn)) {
selector <- cssSelectorToSelector(fn)
fn <- function(el, i) {
elMatchesSelector(el, selector)
}
}
validateFnCanIterate(fn)
filterStack <- envirStackUnique()
selectedWalkI(els, function(el, i) {
if (fn(el, i)) {
filterStack$push(el)
}
})
filterStack$uniqueList()
}
# Convert a CSS selection character value to a selector object
# @param cssSelector A character value representing a CSS search pattern
# @return A single item of a selector list. (See `asSelectorList()`).
# A single-element CSS selector object with full CSS element match information.
# (Child selectors are not allowed in single-element selectors)
cssSelectorToSelector <- function(cssSelector) {
selector <-
if (isSelector(cssSelector)) {
cssSelector
} else {
selectorList <- asSelectorList(cssSelector)
if (length(selectorList) > 1) {
stop(
"Can only match using a simple CSS selector. ",
"Looking for descendant elements is not allowed."
)
}
selectorList[[1]]
}
selector
}
pushFnWrapper <- function(stack, cssSelector) {
if (is.null(cssSelector)) {
stack$push
} else {
selector <- cssSelectorToSelector(cssSelector)
function(el) {
if (elMatchesSelector(el, selector)) {
stack$push(el)
}
}
}
}
elMatchesSelector <- function(el, selector) {
if (!isTagEnv(el)) return(FALSE)
if (!isSelector(selector)) {
stop("`elMatchesSelector(selector=)` must be an object of class `\"shinySelector\"`")
}
if (selector$type == SELECTOR_EVERYTHING) {
return(TRUE)
}
# match on element
if (!is.null(selector$element)) {
# bad element match
if (el$name != selector$element) {
return(FALSE)
}
}
# match on id
if (!is.null(selector$id)) {
# bad id match
if ( !identical(tagGetAttribute(el, "id"), selector$id)) {
return(FALSE)
}
}
# match on class values
if (!is.null(selector$classes)) {
elClass <- tagGetAttribute(el, "class")
if (
isNonConformClassValue(elClass) ||
# missing a class value in tag
! all(
selector$classes %in% splitCssClass(elClass)
)
) {
return(FALSE)
}
}
# No other matches fail. Mark as a match
TRUE
}
tagQueryFindDescendants <- function(els, selector) {
if (!isSelector(selector)) {
selector <- cssSelectorToSelector(selector)
}
foundStack <- envirStackUnique()
# For every element...
tagQueryWalk(els, function(el) {
if (!isTagEnv(el)) return()
# Ignore the element and
# Walk through each child...
tagQueryWalk(el$children, function(child) {
# Find descendant matching the `selector`
tagQueryFindDescendants_(child, selector, foundStack$push)
})
})
foundStack$uniqueList()
}
tagQueryFindDescendants_ <- function(el, selector, fn) {
if (isTagEnv(el)) {
isMatch <- elMatchesSelector(el, selector)
# If it was a match
if (isMatch) {
fn(el)
}
# If there are children and remaining selectors,
# Recurse through without matching
# (Only allowed if `>` is not found)
if (length(el$children) > 0) {
walk(
el$children,
tagQueryFindDescendants_,
fn = fn,
selector = selector
)
}
} else if (is.list(el)) {
# For each item in the list like object, recurse through
walk(el, tagQueryFindDescendants_, fn = fn, selector = selector)
} else if (is.null(el) || is.atomic(el) || is.function(el) || is.language(el)) {
# Can not match on atomics or functions
return()
} else {
message("tagQueryFindDescendants_() - Unknown Type! This has not happened before:")
str(el)
stop("Unknown type in tagQueryFindDescendants_()")
}
invisible()
}
# Find all elements within `els` that match the `selector`
tagQueryFindAll <- function(els, selector) {
selectorList <- asSelectorList(selector)
curEls <- els
walk(selectorList, function(selector) {
curEls <<-
if (selector$traversal == SELECTOR_CHILD) {
tagQueryFindChildren(curEls, selector)
} else {
# any descendant traversal
tagQueryFindDescendants(curEls, selector)
}
})
curEls
}
htmltools/R/images.R 0000644 0001762 0000144 00000015773 14603012577 014112 0 ustar ligges users #' Capture a plot as a saved file
#'
#' Easily generates a .png file (or other graphics file) from a plotting
#' expression.
#'
#' @param expr A plotting expression that generates a plot (or yields an object
#' that generates a plot when printed, like a ggplot2). We evaluate this
#' expression after activating the graphics device (`device`).
#' @param filename The output filename. By default, a temp file with `.png`
#' extension will be used; you should provide a filename with a different
#' extension if you provide a non-PNG graphics device function.
#' @param device A graphics device function; by default, this will be either
#' [grDevices::png()], [ragg::agg_png()], or [Cairo::CairoPNG()], depending on
#' your system and configuration. See [defaultPngDevice()].
#' @param width,height,res,... Additional arguments to the `device` function.
#'
#' @seealso [plotTag()] saves plots as a self-contained ` `
#' tag.
#'
#' @examplesIf rlang::is_interactive()
#' # Default settings
#' res <- capturePlot(plot(cars))
#'
#' # View result
#' browseURL(res)
#'
#' # Clean up
#' unlink(res)
#'
#' # Custom width/height
#' pngpath <- tempfile(fileext = ".png")
#' capturePlot(plot(pressure), pngpath, width = 800, height = 375)
#' browseURL(pngpath)
#' unlink(pngpath)
#'
#' # Use a custom graphics device (e.g., SVG)
#' if (capabilities("cairo")) {
#' svgpath <- capturePlot(
#' plot(pressure),
#' tempfile(fileext = ".svg"),
#' grDevices::svg,
#' width = 8, height = 3.75
#' )
#' browseURL(svgpath)
#' unlink(svgpath)
#' }
#'
#' @export
capturePlot <- function(expr, filename = tempfile(fileext = ".png"),
device = defaultPngDevice(), width = 400, height = 400, res = 72,
...) {
if (!is.function(device)) {
stop(call. = FALSE, "The `device` argument should be a function, e.g. `grDevices::png`")
}
expr <- rlang::enquo(expr)
tempFile <- missing(filename)
args <- rlang::list2(width = width, height = height, res = res)
argnms <- names(formals(device))
if (!"..." %in% argnms) {
# Only include `width`, `height`, and `res` if the corresponding formal
# parameters are present.
args <- args[names(args) %in% argnms]
}
args <- c(list(filename = filename), args, rlang::list2(...))
do.call(device, args)
dev <- grDevices::dev.cur()
on.exit(grDevices::dev.off(dev), add = TRUE)
# Call plot.new() so that even if no plotting operations are performed at
# least we have a blank background. N.B. we need to set the margin to 0
# temporarily before plot.new() because when the plot size is small (e.g.
# 200x50), we will get an error "figure margin too large", which is triggered
# by plot.new() with the default (large) margin. However, this does not
# guarantee user's code in `expr` will not trigger the error -- they may have
# to set par(mar = smaller_value) before they draw base graphics.
op <- graphics::par(mar = rep(0, 4))
# Prevent examples() from prompting
grDevices::devAskNewPage(FALSE)
tryCatch(graphics::plot.new(), finally = graphics::par(op))
tryCatch({
result <- withVisible(rlang::eval_tidy(expr))
if (result$visible) {
capture.output(print(result$value))
}
filename
}, error = function(e) {
try({
if (tempFile && file.exists(filename))
unlink(filename)
})
stop(e)
})
}
#' Capture a plot as a self-contained ` ` tag
#'
#' @param expr A plotting expression that generates a plot (or yields an object
#' that generates a plot when printed, like a ggplot2).
#' @param alt A single-element character vector that contains a text description
#' of the image. This is used by accessibility tools, such as screen readers
#' for vision impaired users.
#' @param device A graphics device function; by default, this will be either
#' [grDevices::png()], [ragg::agg_png()], or [Cairo::CairoPNG()], depending on
#' your system and configuration. See [defaultPngDevice()].
#' @param width,height The width/height that the generated tag should be
#' displayed at, in logical (browser) pixels.
#' @param pixelratio Indicates the ratio between physical and logical units of
#' length. For PNGs that may be displayed on high-DPI screens, use `2`;
#' for graphics devices that express width/height in inches (like
#' [grDevices::svg()], try `1/72` or `1/96`.
#' @param mimeType The MIME type associated with the `device`. Examples are
#' `image/png`, `image/tiff`, `image/svg+xml`.
#' @param deviceArgs A list of additional arguments that should be included when
#' the `device` function is invoked.
#' @param attribs A list of additional attributes that should be included on the
#' generated ` ` (e.g. `id`, `class`).
#' @param suppressSize By default, `plotTag` will include a `style`
#' attribute with `width` and `height` properties specified in
#' pixels. If you'd rather specify the image size using other methods (like
#' responsive CSS rules) you can use this argument to suppress width
#' (`"x"`), height (`"y"`), or both (`"xy"`) properties.
#'
#' @return A [browsable()] HTML ` ` tag object. Print it at
#' the console to preview, or call [as.character()] on it to view the HTML
#' source.
#'
#' @seealso [capturePlot()] saves plots as an image file.
#'
#' @examplesIf rlang::is_interactive()
#' img <- plotTag({
#' plot(cars)
#' }, "A plot of the 'cars' dataset", width = 375, height = 275)
#'
#' img
#'
#' if (capabilities("cairo")) {
#' plotTag(
#' plot(pressure), "A plot of the 'pressure' dataset",
#' device = grDevices::svg, width = 375, height = 275, pixelratio = 1/72,
#' mimeType = "image/svg+xml"
#' )
#' }
#'
#' @export
plotTag <- function(expr, alt, device = defaultPngDevice(), width = 400, height = 400,
pixelratio = 2, mimeType = "image/png", deviceArgs = list(), attribs = list(),
suppressSize = c("none", "x", "y", "xy")) {
suppressSize <- match.arg(suppressSize)
if (suppressSize == "xy") {
suppressSize <- c("x", "y")
}
file <- rlang::eval_tidy(rlang::expr(capturePlot({{expr}},
device = device,
width = width * pixelratio,
height = height * pixelratio,
res = 72 * pixelratio,
!!!deviceArgs)))
on.exit(unlink(file), add = TRUE)
browsable(tags$img(
src = base64enc::dataURI(file = file, mime = mimeType),
style = css(
width = if (!"x" %in% suppressSize) validateCssUnit(width),
height = if (!"y" %in% suppressSize) validateCssUnit(height)
),
alt = alt,
!!!attribs
))
}
#' Determine the best PNG device for your system
#'
#' Returns the best PNG-based graphics device for your system, in the opinion of
#' the `htmltools` maintainers. On Mac,
#' [grDevices::png()] is used; on all other
#' platforms, either [ragg::agg_png()] or
#' [Cairo::CairoPNG()] are used if their packages
#' are installed. Otherwise, [grDevices::png()] is
#' used.
#'
#' @return A graphics device function.
#'
#' @export
defaultPngDevice <- function() {
if (capabilities("aqua")) {
grDevices::png
} else if (is_installed("ragg")) {
ragg::agg_png
} else if (is_installed("Cairo")) {
Cairo::CairoPNG
} else {
grDevices::png
}
}
htmltools/R/colors.R 0000644 0001762 0000144 00000034121 14600330155 014122 0 ustar ligges users #' Parse CSS color strings
#'
#' Parses/normalizes CSS color strings, and returns them as strings in
#' `"#RRGGBB"` and/or `"#RRGGBBAA"` format. Understands hex colors in 3, 4, 6,
#' and 8 digit forms, `rgb()`/`rgba()`, `hsl()`/`hsla()`, and color keywords.
#'
#' Note that `parseCssColors` may return colors in `#RRGGBBAA` format. Such
#' values are not understood by Internet Explorer, and must be converted to
#' `rgba(red, green, blue, alpha)` format to be safe for the web.
#'
#' @param str CSS color strings
#' @param mustWork If true, invalid color strings will cause an error; if false,
#' then the result will contain `NA` for invalid colors.
#' @return A vector of strings in `#RRGGBB` or `#RRGGBBAA` format (the latter is
#' only used for colors whose alpha values are less than `FF`), or `NA` for
#' invalid colors when `mustWork` is false. Such strings are suitable for
#' use in plots, or parsing with [col2rgb()] (be sure to pass `alpha = TRUE`
#' to prevent the alpha channel from being discarded).
#'
#' @examples
#' parseCssColors(c(
#' "#0d6efd",
#' "#DC35457F",
#' "rgb(32,201,151)",
#' " rgba( 23 , 162 , 184 , 0.5 ) ",
#' "hsl(261, 51%, 51%)",
#' "cornflowerblue"
#' ))
#' @export
parseCssColors <- function(str, mustWork = TRUE) {
# Logic below assumes a character string with non-missing values
# Note that an empty string is not a valid color, so parsing fails
# on NA input values, and thus, will be converted back to NA
# when `mustWork = FALSE`
isNA <- is.na(str)
if (!(is.character(str) || all(isNA))) {
stop("`str` must be a character vector (or NA).")
}
str[isNA] <- ""
# Strip insignificant whitespace
str <- color_strip_ws(str)
strategies <- list(
# #RRGGBBAA and #RRGGBB
list(
pattern = "^#([[:xdigit:]]{2})([[:xdigit:]]{2})([[:xdigit:]]{2})([[:xdigit:]]{2})?$",
decoders = list(
decode_hex, # red
decode_hex, # green
decode_hex, # blue
decode_optional(decode_hex, 0xFF) # alpha, optional
),
encoder = encode_hex
),
# #RGBA and #RGB
list(
pattern = "^#([[:xdigit:]])([[:xdigit:]])([[:xdigit:]])([[:xdigit:]])?$",
decoders = list(
decode_hex, # red
decode_hex, # green
decode_hex, # blue
decode_optional(decode_hex, 0xFF) # alpha, optional
),
encoder = encode_hex
),
# rgb() and rgba()
list(
pattern = paste0("^rgba?\\(",
"(", regex_float, "),",
"(", regex_float, "),",
"(", regex_float, ")",
regex_non_capturing_group(",(", regex_float, ")"), "?",
"\\)$"),
decoders = list(
decode_float_255, # red [0-255]
decode_float_255, # green [0-255]
decode_float_255, # blue [0-255]
decode_optional(decode_float_1, 0xFF) # alpha [0-1], optional
),
encoder = encode_hex
),
# hsl() and hsla()
list(
pattern = paste0("^hsla?\\(",
"(", regex_float, "),",
"(", regex_float, ")%,",
"(", regex_float, ")%",
regex_non_capturing_group(",(", regex_float, ")"), "?",
"\\)$"),
decoders = list(
decode_float_identity, # hue [0-360]
decode_float_identity, # saturation [0-255]
decode_float_identity, # lightness [0-255]
decode_optional(decode_float_1, 0xFF) # alpha [0-1], optional
),
encoder = encode_hsl
),
# color keywords
list(
pattern = paste0("^", regex_color_keyword, "$"),
decoders = list(decode_color_keyword),
encoder = encode_hex
)
)
success <- rep_len(FALSE, length(str))
result <- rep_len(NA_character_, length(str))
for (strat in strategies) {
if (all(success)) {
break
}
res <- match_and_decode(
str[!success],
strat$pattern,
!!!strat$decoders
)
if (any(res$matching_rows)) {
result[!success][res$matching_rows] <- strat$encoder(res$values)
success[!success][res$matching_rows] <- TRUE
}
}
if (mustWork && any(!success)) {
stop(sprintf("CSS color value(s) could not be parsed: '%s'", paste0(str[!success], collapse = "', '")))
}
result
}
# Strips whitespace that isn't significant in the parsing of CSS colors.
color_strip_ws <- function(str) {
str <- gsub("\\s+", " ", str)
str <- gsub("^ | $", "", str)
str <- gsub(" ?, ?", ",", str)
str <- gsub("\\( ", "(", str)
str <- gsub(" \\)", ")", str)
str
}
#' Match and decode a string
#'
#' Given a vector of strings, applies a regex that contains one or more
#' capturing groups. Each group's matching substrings are then passed to a
#' decoder function, which is responsible for returning decoded values. (One
#' decoder function is provided per capturing roup.)
#'
#' After decoding, all of the decoded values are cbind-ed together. The caller
#' needs to know which elements in `str` actually matched; therefore, the actual
#' return value is a list with names `matching_rows` and `values` (the latter is
#' only present if one or more rows actually matched).
#'
#' @param pattern Regex that contains the same number of capturing groups as
#' unnamed arguments in `...`. The capturing groups MUST be non-overlapping.
#' @param ... Functions for decoding each capturing group
#' @noRd
match_and_decode <- function(str, pattern, ...) {
# Example:
# str <- c("#123456", "#ABCDEF")
# pattern <- "#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})"
# args <- list(decode_hex, decode_hex, decode_hex)
args <- rlang::list2(...)
stopifnot(all(vapply(args, is.function, logical(1))))
m <- regexec(pattern, str, ignore.case = TRUE)
matching_rows <- vapply(m, function(x) { x[1] >= 0 }, logical(1)) # Ex: c(T,T)
matches <- regmatches(str[matching_rows], m[matching_rows])
if (length(matches) == 0) {
return(list(
matching_rows = matching_rows
))
}
col_count <- length(matches[[1]])
str_matrix <- matrix(unlist(matches), ncol = col_count, byrow = TRUE)
# Drop the first column, which is the entire matched string; we only want the
# capturing groups
str_matrix <- str_matrix[,-1,drop=FALSE]
# Number of function arguments should match number of regex's capturing groups
stopifnot(length(args) == ncol(str_matrix))
# Ex: str_matrix
# [,1] [,2] [,3]
# [1,] "12" "34" "56"
# [2,] "AB" "CD" "EF"
vals <- lapply(seq_len(ncol(str_matrix)), function(i) {
# Ex: decode_hex(c("12", "AB")) => c(18, 171)
args[[i]](str_matrix[,i])
})
results <- do.call(cbind, vals)
# Ex: results
# [,1] [,2] [,3]
# [1,] 18 52 86
# [2,] 171 205 239
return(list(
matching_rows = matching_rows,
values = results
))
}
decode_optional <- function(func, default_value) {
force(func)
force(default_value)
function(str) {
result <- rep_len(default_value, length(str))
has_value <- nzchar(str, keepNA = FALSE) & !is.na(str)
result[has_value] <- func(str[has_value])
result
}
}
# Takes a vector of strings whose elements represent a SINGLE hex color channel
# (one or two hexadecimal digits) and return an integer in the range [0-255].
decode_hex <- function(str) {
stopifnot(all(nchar(str) %in% c(1,2)))
# Single hex digits get doubled up
str <- ifelse(nchar(str) == 1, paste0(str, str), str)
res <- strtoi(str, 16)
stopifnot(!anyNA(res))
res
}
# Convert strings of floating point numbers [0-255] to integer values in the
# same range. Valid values outside the range will be clamped. Invalid values
# will raise errors.
decode_float_255 <- function(str) {
as.integer(pmax(0, pmin(255, round(decode_float_identity(str)))))
}
# Convert strings of floating point numbers [0-1] to integer values [0-255].
# Valid values outside the range will be clamped. Invalid values will raise
# errors.
decode_float_1 <- function(str) {
as.integer(pmax(0, pmin(255, round(decode_float_identity(str) * 255))))
}
decode_float_identity <- function(str) {
stopifnot(all(grepl(paste0("^", regex_float, "$"), str)))
as.numeric(str)
}
encode_hex <- function(values) {
if (length(values) == 0) {
return(character(0))
}
if (!is.matrix(values)) {
stop("encode_hex requires a matrix argument")
}
if (ncol(values) < 3) {
stop("encode_hex called with too few columns")
}
if (ncol(values) > 4) {
stop("encode_hex called with too many columns")
}
if (!is.numeric(values)) {
stop("encode_hex requires numeric values")
}
if (!is.integer(values)) {
values <- round(values)
}
if (any(values > 255) || any(values < 0)) {
stop("encode_hex values out of bounds")
}
red <- values[,1]
green <- values[,2]
blue <- values[,3]
alpha <- if (ncol(values) > 3) {
values[,4]
} else {
0xFF
}
colors <- ifelse(alpha == 0xFF,
sprintf("#%02X%02X%02X", red, green, blue),
sprintf("#%02X%02X%02X%02X", red, green, blue, alpha)
)
colors
}
# Convert HTML color keywords (plus "transparent") to integer matrix with 3
# columns (r, g, b) and length(str) rows. Errors on invalid strings.
decode_color_keyword <- function(str) {
color <- css_color_keywords[tolower(str)]
if (anyNA(color)) {
stop("Invalid color keyword(s)")
}
unname(t(grDevices::col2rgb(color, alpha = TRUE)))
}
encode_hsl <- function(values) {
if (length(values) == 0) {
return(character(0))
}
if (!is.matrix(values)) {
stop("encode_hsl requires a matrix argument")
}
if (ncol(values) < 3) {
stop("encode_hsl called with too few columns")
}
if (ncol(values) > 4) {
stop("encode_hsl called with too many columns")
}
if (!is.numeric(values)) {
stop("encode_hsl requires numeric values")
}
# https://www.w3.org/TR/css-color-3/#hsl-color
H <- values[,1]
S <- values[,2] / 100
L <- values[,3] / 100
alpha <- if (ncol(values) > 3) {
values[,4]
} else {
0xFF
}
# Clamp
H <- (((H %% 360) + 360) %% 360) / 360
S <- pmax(0, pmin(1, S))
L <- pmax(0, pmin(1, L))
hue_to_rgb <- function(m1, m2, h) {
h <- ifelse(h < 0, h + 1,
ifelse(h > 1, h - 1,
h))
ifelse(h * 6 < 1, m1+(m2-m1)*h*6,
ifelse(h * 2 < 1, m2,
ifelse(h * 3 < 2, m1+(m2-m1)*(2/3-h)*6,
m1)))
}
M2 <- ifelse(L <= 0.5,
L * (S + 1),
L + S - L * S
)
M1 <- L * 2 - M2
rgb <- cbind(
hue_to_rgb(M1, M2, H+1/3),
hue_to_rgb(M1, M2, H ),
hue_to_rgb(M1, M2, H-1/3)
) * 255
rgb <- cbind(rgb, alpha)
encode_hex(rgb)
}
css_color_keywords <- c(
"transparent" = "#00000000",
"aliceblue" = "#F0F8FF", "antiquewhite" = "#FAEBD7", "aqua" = "#00FFFF", "aquamarine" = "#7FFFD4", "azure" = "#F0FFFF", "beige" = "#F5F5DC", "bisque" = "#FFE4C4", "black" = "#000000", "blanchedalmond" = "#FFEBCD", "blue" = "#0000FF", "blueviolet" = "#8A2BE2", "brown" = "#A52A2A", "burlywood" = "#DEB887", "cadetblue" = "#5F9EA0", "chartreuse" = "#7FFF00", "chocolate" = "#D2691E", "coral" = "#FF7F50", "cornflowerblue" = "#6495ED", "cornsilk" = "#FFF8DC", "crimson" = "#DC143C", "cyan" = "#00FFFF", "darkblue" = "#00008B", "darkcyan" = "#008B8B", "darkgoldenrod" = "#B8860B", "darkgray" = "#A9A9A9", "darkgreen" = "#006400", "darkgrey" = "#A9A9A9", "darkkhaki" = "#BDB76B", "darkmagenta" = "#8B008B", "darkolivegreen" = "#556B2F", "darkorange" = "#FF8C00", "darkorchid" = "#9932CC", "darkred" = "#8B0000", "darksalmon" = "#E9967A", "darkseagreen" = "#8FBC8F", "darkslateblue" = "#483D8B", "darkslategray" = "#2F4F4F", "darkslategrey" = "#2F4F4F", "darkturquoise" = "#00CED1", "darkviolet" = "#9400D3", "deeppink" = "#FF1493", "deepskyblue" = "#00BFFF", "dimgray" = "#696969", "dimgrey" = "#696969", "dodgerblue" = "#1E90FF", "firebrick" = "#B22222", "floralwhite" = "#FFFAF0", "forestgreen" = "#228B22", "fuchsia" = "#FF00FF", "gainsboro" = "#DCDCDC", "ghostwhite" = "#F8F8FF", "gold" = "#FFD700", "goldenrod" = "#DAA520", "gray" = "#808080", "green" = "#008000", "greenyellow" = "#ADFF2F", "grey" = "#808080", "honeydew" = "#F0FFF0", "hotpink" = "#FF69B4", "indianred" = "#CD5C5C", "indigo" = "#4B0082", "ivory" = "#FFFFF0", "khaki" = "#F0E68C", "lavender" = "#E6E6FA", "lavenderblush" = "#FFF0F5", "lawngreen" = "#7CFC00", "lemonchiffon" = "#FFFACD", "lightblue" = "#ADD8E6", "lightcoral" = "#F08080", "lightcyan" = "#E0FFFF", "lightgoldenrodyellow" = "#FAFAD2", "lightgray" = "#D3D3D3", "lightgreen" = "#90EE90", "lightgrey" = "#D3D3D3", "lightpink" = "#FFB6C1", "lightsalmon" = "#FFA07A", "lightseagreen" = "#20B2AA", "lightskyblue" = "#87CEFA", "lightslategray" = "#778899", "lightslategrey" = "#778899", "lightsteelblue" = "#B0C4DE", "lightyellow" = "#FFFFE0", "lime" = "#00FF00", "limegreen" = "#32CD32", "linen" = "#FAF0E6", "magenta" = "#FF00FF", "maroon" = "#800000", "mediumaquamarine" = "#66CDAA", "mediumblue" = "#0000CD", "mediumorchid" = "#BA55D3", "mediumpurple" = "#9370DB", "mediumseagreen" = "#3CB371", "mediumslateblue" = "#7B68EE", "mediumspringgreen" = "#00FA9A", "mediumturquoise" = "#48D1CC", "mediumvioletred" = "#C71585", "midnightblue" = "#191970", "mintcream" = "#F5FFFA", "mistyrose" = "#FFE4E1", "moccasin" = "#FFE4B5", "navajowhite" = "#FFDEAD", "navy" = "#000080", "oldlace" = "#FDF5E6", "olive" = "#808000", "olivedrab" = "#6B8E23", "orange" = "#FFA500", "orangered" = "#FF4500", "orchid" = "#DA70D6", "palegoldenrod" = "#EEE8AA", "palegreen" = "#98FB98", "paleturquoise" = "#AFEEEE", "palevioletred" = "#DB7093", "papayawhip" = "#FFEFD5", "peachpuff" = "#FFDAB9", "peru" = "#CD853F", "pink" = "#FFC0CB", "plum" = "#DDA0DD", "powderblue" = "#B0E0E6", "purple" = "#800080", "rebeccapurple" = "#663399", "red" = "#FF0000", "rosybrown" = "#BC8F8F", "royalblue" = "#4169E1", "saddlebrown" = "#8B4513", "salmon" = "#FA8072", "sandybrown" = "#F4A460", "seagreen" = "#2E8B57", "seashell" = "#FFF5EE", "sienna" = "#A0522D", "silver" = "#C0C0C0", "skyblue" = "#87CEEB", "slateblue" = "#6A5ACD", "slategray" = "#708090", "slategrey" = "#708090", "snow" = "#FFFAFA", "springgreen" = "#00FF7F", "steelblue" = "#4682B4", "tan" = "#D2B48C", "teal" = "#008080", "thistle" = "#D8BFD8", "tomato" = "#FF6347", "turquoise" = "#40E0D0", "violet" = "#EE82EE", "wheat" = "#F5DEB3", "white" = "#FFFFFF", "whitesmoke" = "#F5F5F5", "yellow" = "#FFFF00", "yellowgreen" = "#9ACD32"
)
regex_non_capturing_group <- function(...) { paste0("(?:", ..., ")")}
regex_float <- "[-+]?[0-9]*\\.?[0-9]+"
regex_color_keyword <- paste0("(", paste0(names(css_color_keywords), collapse = "|"), ")")
htmltools/R/utils.R 0000644 0001762 0000144 00000013422 14600330155 013762 0 ustar ligges users # @staticimports pkg:staticimports
# system_file is_installed
# Implements a "whitespace eating" writer.
#
# WSTextWriter relies on the caller distinguishing between writes of important
# content, and writes of whitespace that may or may not be elided (`.$write()`
# vs `.$writeWS()`).
#
# At any point, `eatWS` may be called, which will cause any recent `writeWS`
# operations (i.e. those since either the beginning of time, or the most recent
# `write` operation) to be undone, AND for any future `writeWS` calls to be
# ignored. A call to `write` will be respected, and will restore normal
# behavior.
#
# Text is automatically converted to UTF-8 before being written.
#' @param bufferSize The initial size of the buffer in which writes are stored.
#' The buffer will be periodically cleared, if possible, to cache the writes
#' as a string. If the buffer cannot be cleared (because of the need to be
#' able to backtrack to fulfill an `eatWS()` call), then the buffer size will
#' be doubled.
#' @noRd
WSTextWriter <- function(bufferSize=1024) {
if (bufferSize < 3) {
stop("Buffer size must be at least 3")
}
# The buffer into which we enter all the writes.
buffer <- character(bufferSize)
# The index storing the position in the buffer of the most recent write.
marked <- 0
# The index storing the position in the buffer of the most recent write or writeWS.
position <- 0
# TRUE if we're eating whitespace right now, in which case calls to writeWS are no-ops.
suppressing <- FALSE
# Collapses the text in the buffer to create space for more writes. The first
# element in the buffer will be the concatenation of any writes up to the
# current marker. The second element in the buffer will be the concatenation
# of all writes after the marker.
collapseBuffer <- function() {
# Collapse the writes in the buffer up to the marked position into the first buffer entry
nonWS <- ""
if (marked > 0) {
nonWS <- paste(buffer[seq_len(marked)], collapse="")
}
# Collapse any remaining whitespace
ws <- ""
remaining <- position - marked
if (remaining > 0) {
# We have some whitespace to collapse. Collapse it into the second buffer entry.
ws <- paste(buffer[seq(from=marked+1,to=marked+remaining)], collapse="")
}
buffer[1] <<- nonWS
buffer[2] <<- ws
position <<- 2
marked <<- 1
}
# Logic to do the actual write
writeImpl <- function(text) {
# force `text` to evaluate and check that it's the right shape
# TODO: We could support vectors with multiple elements here and perhaps
# find some way to combine with `paste8()`. See
# https://github.com/rstudio/htmltools/pull/132#discussion_r302280588
if (length(text) != 1 || !is.character(text)) {
stop("Text to be written must be a length-one character vector")
}
# Are we at the end of our buffer?
if (position == length(buffer)) {
collapseBuffer()
}
# The text that is written to this writer will be converted to
# UTF-8 using enc2utf8. The rendered output will always be UTF-8
# encoded.
enc <- enc2utf8(text)
# Move the position pointer and store the (encoded) write
position <<- position + 1
buffer[position] <<- enc
}
# The actual object returned
list(
# Write content. Updates the marker and stops suppressing whitespace writes.
#
# @param text Single element character vector
write = function(text) {
writeImpl(text)
suppressing <<- FALSE
marked <<- position
},
# Write whitespace. If eatWS() was called and its effect has not been
# canceled, then this method no-ops.
# @param text Single element character vector containing only
# whitespace characters
writeWS = function(text) {
if (suppressing) {
return()
}
writeImpl(text)
},
# Return the contents of the TextWriter, as a single element character
# vector, from the beginning to the current writing position (normally this
# is the end of the last write or writeWS, unless eatWS() was called).
readAll = function() {
# Collapse everything in the buffer up to `position`
paste(buffer[seq_len(position)], collapse="")
},
# Removes both recent and upcoming whitespace writes
eatWS = function() {
# Reset back to the most recent marker
position <<- marked
suppressing <<- TRUE
}
)
}
# Given a vector/list, return TRUE if any elements are named, FALSE otherwise.
anyNamed <- function(x) {
# Zero-length vector
if (length(x) == 0) return(FALSE)
nms <- names(x)
# List with no name attribute
if (is.null(nms)) return(FALSE)
# List with name attribute; check for any ""
any(nzchar(nms))
}
# Given a vector/list, return TRUE if any elements are unnamed, FALSE otherwise.
anyUnnamed <- function(x) {
# Zero-length vector
if (length(x) == 0) return(FALSE)
nms <- names(x)
# List with no name attribute
if (is.null(nms)) return(TRUE)
# List with name attribute; check for any ""
any(!nzchar(nms))
}
# Get source filename(s) out of a script, stylesheet, or attachment entry of an
# htmlDependency object. The spec is here:
# https://github.com/rstudio/shiny/blob/474f1400/srcts/src/shiny/render.ts#L79-L115
# This returns a character vector of filenames.
# `attr` should be "src" for script, and "href" for stylesheet and attachment
find_dep_filenames <- function(x, attr = "src") {
# In the case below, the structure is "abc" or c("abc", "xyz")
if (is.character(x)) return(x)
if (is.list(x)) {
# In the case below, the structure is list(src="abc")
if (!is.null(x[[attr]])) return(x[[attr]])
# If we get here, the structure is list(list(src="abc"), list(src="xyz")).
return(unlist(lapply(x, find_dep_filenames)))
}
# If we get here, we didn't find anything.
character(0)
}
htmltools/R/htmltools-package.R 0000644 0001762 0000144 00000000530 14600330155 016234 0 ustar ligges users #' @keywords internal
"_PACKAGE"
## usethis namespace: start
#' @import rlang
#' @import utils digest
#' @importFrom fastmap fastmap faststack
#' @importFrom rlang obj_address
## usethis namespace: end
NULL
# For usethis::use_release_issue()
release_bullets <- function() {
c(
"Update static imports: `staticimports::import()`"
)
}
htmltools/R/template.R 0000644 0001762 0000144 00000012071 14600330155 014434 0 ustar ligges users #' Process an HTML template
#'
#' Process an HTML template and return a tagList object. If the template is a
#' complete HTML document, then the returned object will also have class
#' `html_document`, and can be passed to the function
#' [renderDocument()] to get the final HTML text.
#'
#' @param filename Path to an HTML template file. Incompatible with
#' `text_`.
#' @param ... Variable values to use when processing the template.
#' @param text_ A string to use as the template, instead of a file. Incompatible
#' with `filename`.
#' @param document_ Is this template a complete HTML document (`TRUE`), or
#' a fragment of HTML that is to be inserted into an HTML document
#' (`FALSE`)? With `"auto"` (the default), auto-detect by searching
#' for the string `""` within the template.
#'
#' @seealso [renderDocument()]
#' @export
#' @useDynLib htmltools, .registration = TRUE
htmlTemplate <- function(filename = NULL, ..., text_ = NULL, document_ = "auto") {
if (!xor(is.null(filename), is.null(text_))) {
stop("htmlTemplate requires either `filename` or `text_`.")
}
if (!is.null(filename)) {
html <- readChar(filename, file.info(filename)$size, useBytes = TRUE)
Encoding(html) <- "UTF-8"
} else if(!is.null(text_)) {
text_ <- paste8(text_, collapse = "\n")
html <- enc2utf8(text_)
}
pieces <- .Call(template_dfa, html)
Encoding(pieces) <- "UTF-8"
# Create environment to evaluate code, as a child of the global env. This
# environment gets the ... arguments assigned as variables.
vars <- dots_list(...)
if ("headContent" %in% names(vars)) {
stop("Can't use reserved argument name 'headContent'.")
}
vars$headContent <- function() HTML("")
env <- list2env(vars, parent = globalenv())
# All the odd-numbered pieces are HTML; all the even-numbered pieces are code
pieces <- mapply(
pieces,
rep_len(c(FALSE, TRUE), length.out = length(pieces)),
FUN = function(piece, isCode) {
if (isCode) {
eval(parse(text = piece), env)
} else {
HTML(piece, .noWS = "outside")
}
},
SIMPLIFY = FALSE
)
result <- tagList(pieces)
if (document_ == "auto") {
document_ = grepl("", html, ignore.case = TRUE)
}
if (document_) {
# The html.document class indicates that it's a complete document, and not
# just a set of tags.
class(result) <- c("html_document", class(result))
}
result
}
#' Render an html_document object
#'
#' This function renders `html_document` objects, and returns a string with
#' the final HTML content. It calls the [renderTags()] function to
#' convert any shiny.tag objects to HTML. It also finds any any web dependencies
#' (created by [htmlDependency()]) that are attached to the tags, and
#' inserts those. To do the insertion, this function finds the string
#' `""` in the document, and replaces it with the web
#' dependencies.
#'
#' @param x An object of class `html_document`, typically generated by the
#' [htmlTemplate()] function.
#' @param deps Any extra web dependencies to add to the html document. This can
#' be an object created by [htmlDependency()], or a list of such
#' objects. These dependencies will be added first, before other dependencies.
#' @param processDep A function that takes a "raw" html_dependency object and
#' does further processing on it. For example, when `renderDocument` is
#' called from Shiny, the function [shiny::createWebDependency()] is
#' used; it modifies the href and tells Shiny to serve a particular path on
#' the filesystem.
#'
#' @return An [HTML()] string, with UTF-8 encoding.
#'
#' @export
renderDocument <- function(x, deps = NULL, processDep = identity) {
if (!inherits(x, "html_document")) {
stop("Object must be an object of class html_document")
}
if (inherits(deps, "html_dependency")) {
deps <- list(deps)
}
result <- renderTags(x)
# Figure out dependencies
deps <- c(deps, result$dependencies)
deps <- resolveDependencies(deps)
deps <- lapply(deps, processDep)
depStr <- paste(sapply(deps, function(dep) {
sprintf("%s[%s]", dep$name, dep$version)
}), collapse = ";")
depHtml <- renderDependencies(deps, "href")
# Put content in the section
head_content <- paste0(
' \n',
sprintf(' \n',
paste(result$singletons, collapse = ',')
),
sprintf(' \n',
depStr
),
depHtml,
c(result$head, recursive = TRUE)
)
# Need to mark result as UTF-8. If body is ASCII, it will be marked with
# encoding "unknown". If the head has UTF-8 characters and is marked as
# "UTF-8", the output string here will have the correct UTF-8 byte sequences,
# but will be marked as "unknown", which causes the wrong text to be
# displayed. See https://github.com/rstudio/shiny/issues/1395
res <- sub("", head_content, result$html, fixed = TRUE)
Encoding(res) <- "UTF-8"
res
}
htmltools/R/known_tags.R 0000644 0001762 0000144 00000013653 14600330155 015002 0 ustar ligges users ## Generated by `./scripts/generate_known_tags.R`: do not edit by hand
## Please call `source("./scripts/generate_known_tags.R", echo = TRUE, prompt.echo = ">")` to update
known_tags <- c(
"a", # html svg
"abbr", # html
"address", # html
"animate", # svg
"animateMotion", # svg
"animateTransform", # svg
"area", # html
"article", # html
"aside", # html
"audio", # html
"b", # html
"base", # html
"bdi", # html
"bdo", # html
"blockquote", # html
"body", # html
"br", # html
"button", # html
"canvas", # html
"caption", # html
"circle", # svg
"cite", # html
"clipPath", # svg
"code", # html
"col", # html
"colgroup", # html
"color-profile", # svg
"command", #
"data", # html
"datalist", # html
"dd", # html
"defs", # svg
"del", # html
"desc", # svg
"details", # html
"dfn", # html
"dialog", # html
"discard", # svg
"div", # html
"dl", # html
"dt", # html
"ellipse", # svg
"em", # html
"embed", # html
"eventsource", #
"feBlend", # svg
"feColorMatrix", # svg
"feComponentTransfer",# svg
"feComposite", # svg
"feConvolveMatrix", # svg
"feDiffuseLighting", # svg
"feDisplacementMap", # svg
"feDistantLight", # svg
"feDropShadow", # svg
"feFlood", # svg
"feFuncA", # svg
"feFuncB", # svg
"feFuncG", # svg
"feFuncR", # svg
"feGaussianBlur", # svg
"feImage", # svg
"feMerge", # svg
"feMergeNode", # svg
"feMorphology", # svg
"feOffset", # svg
"fePointLight", # svg
"feSpecularLighting", # svg
"feSpotLight", # svg
"feTile", # svg
"feTurbulence", # svg
"fieldset", # html
"figcaption", # html
"figure", # html
"filter", # svg
"footer", # html
"foreignObject", # svg
"form", # html
"g", # svg
"h1", # html
"h2", # html
"h3", # html
"h4", # html
"h5", # html
"h6", # html
"hatch", # svg
"hatchpath", # svg
"head", # html
"header", # html
"hgroup", # html
"hr", # html
"html", # html
"i", # html
"iframe", # html
"image", # svg
"img", # html
"input", # html
"ins", # html
"kbd", # html
"keygen", #
"label", # html
"legend", # html
"li", # html
"line", # svg
"linearGradient", # svg
"link", # html
"main", # html
"map", # html
"mark", # html
"marker", # svg
"mask", # svg
"menu", # html
"meta", # html
"metadata", # svg
"meter", # html
"mpath", # svg
"nav", # html
"noscript", # html
"object", # html
"ol", # html
"optgroup", # html
"option", # html
"output", # html
"p", # html
"param", # html
"path", # svg
"pattern", # svg
"picture", # html
"polygon", # svg
"polyline", # svg
"pre", # html
"progress", # html
"q", # html
"radialGradient", # svg
"rb", # html
"rect", # svg
"rp", # html
"rt", # html
"rtc", # html
"ruby", # html
"s", # html
"samp", # html
"script", # html svg
"section", # html
"select", # html
"set", # svg
"slot", # html
"small", # html
"solidcolor", # svg
"source", # html
"span", # html
"stop", # svg
"strong", # html
"style", # html svg
"sub", # html
"summary", # html
"sup", # html
"svg", # svg
"switch", # svg
"symbol", # svg
"table", # html
"tbody", # html
"td", # html
"template", # html
"text", # svg
"textarea", # html
"textPath", # svg
"tfoot", # html
"th", # html
"thead", # html
"time", # html
"title", # html svg
"tr", # html
"track", # html
"tspan", # svg
"u", # html
"ul", # html
"use", # svg
"var", # html
"video", # html
"view", # svg
"wbr" # html
)
htmltools/R/selector.R 0000644 0001762 0000144 00000022071 14600330155 014442 0 ustar ligges users
# as future expansion becomes a thing, look into `selectr::parse(selector)`
# https://github.com/sjp/selectr/blob/master/R/parser.R
# selectr:::parse("#a.warning > b.mine:not(.theres) d")[[1]]$show()
#> CombinedSelector[CombinedSelector[Class[Hash[Element[*]#a].warning] > Negation[Class[Element[b].mine]:not(Class[Element[*].theres])]] Element[d]]
## ^^ R6 output
SELECTOR_EVERYTHING <- "everything"
SELECTOR_REGULAR <- "regular"
SELECTOR_SPACE <- "space"
SELECTOR_CHILD <- "child"
selectorClass <- "htmltools.selector"
selectorListClass <- "htmltools.selector.list"
isSelector <- function(x) {
inherits(x, selectorClass)
}
isSelectorList <- function(x) {
inherits(x, selectorListClass)
}
# only handles id and classes
asSelector <- function(selector) {
if (isSelector(selector) || isSelectorList(selector)) {
return(selector)
}
# make sure it's a trimmed string
selector <- txt_trim(paste0(selector, collapse = " "))
if (txt_detect(selector, ",", fixed = TRUE)) {
stop("CSS selectors that contain `,` aren't (yet) implemented.", call. = FALSE)
}
if (txt_detect(selector, "[", fixed = TRUE)) {
stop("CSS selectors that contain `[` aren't (yet) implemented.", call. = FALSE)
}
if (txt_detect(selector, "~", fixed = TRUE)) {
stop("CSS selectors that contain `~` aren't (yet) implemented.", call. = FALSE)
}
if (txt_detect(selector, "+", fixed = TRUE)) {
stop("CSS selectors that contain `+` aren't (yet) implemented.", call. = FALSE)
}
if (txt_detect(selector, ":", fixed = TRUE)) {
stop(
"Pseudo CSS selectors (e.g., `:first-child`, `:not()`, etc)",
" aren't (yet) implemented.",
call. = FALSE
)
}
# Check here to avoid inf recursion
if (txt_detect(selector, ">", fixed = TRUE)) {
# If there is a `>`, pad it with spaces
if (txt_detect(selector, "(^>)|(>$)")) {
stop(
"Direct children selector, `>`, must not be the first element or last element",
" in a css selector. Please add more selector information, such as `*`."
)
}
# While there are any consecutive `> >` items...
while(txt_detect(selector, ">\\s*>")) {
# If there are any `>>`, replace them with `> * >`
selector <- txt_replace_all(selector, ">\\s*>", "> * >")
}
# Split by `>` and convert to selectors
# Alter parts (execpt first) to say they are a direct child
# Return selector list
selectorItems <- lapply(strsplit(selector, ">")[[1]], asSelector)
selectorListItems <- Map(
selectorItems,
seq_along(selectorItems),
f = function(selectorItem, i) {
if (isSelector(selectorItem)) {
if (i > 1) selectorItem$traversal <- SELECTOR_CHILD
asSelectorList(selectorItem)
} else {
if (i > 1) selectorItem[[1]]$traversal <- SELECTOR_CHILD
selectorItem
}
}
)
selectorList <- asSelectorList(
unlist(selectorListItems, recursive = FALSE, use.names = FALSE)
)
return(selectorList)
}
# Split into a selector parts and recurse one more time
if (txt_detect(selector, "\\s")) {
selectorItems <- lapply(strsplit(selector, "\\s+")[[1]], asSelector)
selectorList <- asSelectorList(selectorItems)
return(selectorList)
}
# https://www.w3.org/TR/selectors-3/#selectors
type <- NULL
traversal <- SELECTOR_SPACE
element <- NULL
id <- NULL
classes <- NULL
if (isTRUE(selector == "*")) {
type <- SELECTOR_EVERYTHING
} else {
type <- SELECTOR_REGULAR
## Not needed as the regex values below work around this.
# # if there is more than a `*`, such as `*.warning`, treat as `.warning`
# if (txt_detect(selector, "^\\*"))
# selector <- sub("^\\*", "", selector)
# if (grepl("^\\*", selector)) {
# stop("malformed css selector. Found at least two `**` that were not separated by a space")
# }
# }
## https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
## identifiers (including element names, classes, and IDs in selectors) can
## contain only the characters [a-zA-Z0-9] and ISO 10646 characters U+00A0
## and higher, plus the hyphen (-) and the underscore (_); they cannot
## start with a digit, two hyphens, or a hyphen followed by a digit.
## Identifiers can also contain escaped characters and any ISO 10646
## character as a numeric code (see next item). For instance, the
## identifier "B&W?" may be written as "B\&W\?" or "B\26 W\3F".
## Here we use simpler (maybe not accurate) regexes:
## Start with a normal letter, an underscore, or a hyphen
## End when we hit the start for either:
## `.` - a class selector
## `:` - a pseudo-class selector
## `[` - an attribute selector
## `#` - an id selector
valid_name_regex <- "[a-zA-Z_-][^.:[#]*"
## From https://www.w3.org/TR/CSS2/selector.html#selector-syntax
## A simple selector is either a type selector (or universal selector)
## followed immediately by zero or more attribute selectors, ID selectors,
## or pseudo-classes, in any order
# elements always come at the start of the selector
element_regex <- paste0("^", valid_name_regex)
# id starts with a #
id_regex <- paste0("#", valid_name_regex)
# class starts with a period (escaped for regex)
classes_regex <- paste0("\\.", valid_name_regex)
element <- txt_match_first(selector, element_regex)
if (!is.null(element)) {
selector <- txt_remove(selector, element_regex)
}
tmpId <- txt_match_first(selector, id_regex)
if (!is.null(tmpId)) {
id <- txt_remove(tmpId, "^#")
selector <- txt_remove(selector, tmpId, fixed = TRUE)
}
classes <- txt_remove(txt_match_all(selector, classes_regex), "^\\.")
if (length(classes) == 0) {
classes <- NULL
}
}
structure(class = selectorClass, list(
element = element,
id = id,
classes = classes,
type = type,
traversal = traversal
))
}
asSelectorList <- function(selector) {
if (isSelectorList(selector)) {
return(selector)
}
if (is.character(selector)) {
selector <- asSelector(selector)
}
if (isSelector(selector)) {
selector <- list(selector)
}
if (!is.list(selector)) {
stop("Do not know how to convert non list object into a `htmltools.selector.list`")
}
isSelectorVals <- vapply(selector, isSelector, logical(1))
if (!all(isSelectorVals)) {
stop("Can only convert a list of selectors to a `htmltools.selector.list`")
}
structure(class = selectorListClass, selector)
}
#' @export
format.htmltools.selector <- function(x, ...) {
paste0(
c(
if (x$traversal == SELECTOR_CHILD) "> ",
if (x$type == SELECTOR_EVERYTHING) {
"*"
} else {
paste0(c(
x$element,
if (!is.null(x$id)) paste0("#", x$id),
if (!is.null(x$classes)) paste0(".", x$classes)
))
}
),
collapse = ""
)
}
#' @export
format.htmltools.selector.list <- function(x, ...) {
paste0(as.character(lapply(x, format, ...)), collapse = " ")
}
#' @export
print.htmltools.selector <- function(x, ...) {
cat("// htmltools css selector\n")
cat(format(x, ...), "\n")
}
#' @export
print.htmltools.selector.list <- function(x, ...) {
cat("// htmltools css selector list\n")
cat(format(x, ...), "\n")
}
# When `fixed = TRUE`, `sub()`, `gsub()`, `grepl()` perform ~4x faster
# #> bench::mark(grepl("* ", "A B * C"), grepl("* ", "A B * C", fixed = TRUE))
# expression min median
#
# 1 grepl("* ", "A B * C") 3.91µs 5.23µs
# 2 grepl("* ", "A B * C", fixed = TRUE) 1.1µs 1.34µs
txt_replace <- function(text, pattern, replacement, fixed = FALSE) {
sub(pattern = pattern, replacement = replacement, x = text, perl = !fixed, fixed = fixed)
}
txt_replace_all <- function(text, pattern, replacement, fixed = FALSE) {
gsub(pattern = pattern, replacement = replacement, x = text, perl = !fixed, fixed = fixed)
}
txt_remove <- function(x, pattern, ...) {
txt_replace(x, pattern, "", ...)
}
txt_remove_all <- function(x, pattern, ...) {
txt_replace_all(x, pattern, "", ...)
}
trim_leading <- function(text) {
txt_remove_all(text, pattern = "^\\s+")
}
trim_trailing <- function(text) {
txt_remove_all(text, pattern = "\\s+$")
}
txt_trim <- function(text, side = "both") {
if (side == "both" || side == "left") {
text <- trim_leading(text)
}
if (side == "both" || side == "right") {
text <- trim_trailing(text)
}
text
}
txt_detect <- function(text, pattern, fixed = FALSE) {
grepl(pattern = pattern, x = text, perl = !fixed, fixed = fixed)
}
# finds first, NOT all
txt_match_first <- function(x, pattern, ...) {
regInfo <- regexpr(pattern, x, ...)
if (length(regInfo) == 1 && regInfo == -1) {
return(NULL)
}
regmatches(x, regInfo)
}
# return a vector of matches or NULL
txt_match_all <- function(x, pattern, ...) {
if (length(x) != 1) {
stop("`x` must have a length of 1")
}
regInfo <- gregexpr(pattern, x, ...)
first <- regInfo[[1]]
if (length(first) == 1 && first == -1) {
return(NULL)
}
regmatches(x, regInfo)[[1]]
}
htmltools/src/ 0000755 0001762 0000144 00000000000 15113673446 013100 5 ustar ligges users htmltools/src/init.c 0000644 0001762 0000144 00000000631 14600330155 014172 0 ustar ligges users #include
#include
#include // for NULL
#include
/* .Call calls */
extern SEXP template_dfa(SEXP);
static const R_CallMethodDef CallEntries[] = {
{"template_dfa", (DL_FUNC) &template_dfa, 1},
{NULL, NULL, 0}
};
void R_init_htmltools(DllInfo *dll)
{
R_registerRoutines(dll, NULL, CallEntries, NULL, NULL);
R_useDynamicSymbols(dll, FALSE);
}
htmltools/src/template.c 0000644 0001762 0000144 00000012007 15113673362 015054 0 ustar ligges users #include
// A macro similar to SET_STRING_ELT, it assumes a string vector protected with
// PROTECT_WITH_INDEX, will automatically grow it if needed.
#define SET_STRING_ELT2(X, I, VAL, P_IDX) { \
R_xlen_t len = Rf_xlength(X); \
R_xlen_t i = I; \
if (i >= len) { \
len *= 2; \
REPROTECT(X = Rf_lengthgets(X, len), P_IDX); \
} \
SET_STRING_ELT(X, i, VAL); \
}
Rboolean str_is_ASCII(const char *str) {
const char *p;
for(p = str; *p; p++) {
if((unsigned int)*p > 0x7F) return FALSE;
}
return TRUE;
}
// Break template text into character vector. The first element element of the
// resulting vector is HTML, the next is R code, and they continue alternating.
// [[export]]
SEXP template_dfa(SEXP x_sxp) {
typedef enum {
html,
code,
html_oneOpenBracket,
code_oneCloseBracket,
code_string1,
code_string1_backslash,
code_string2,
code_string2_backslash,
code_backtick,
code_backtick_backslash,
code_percentOp,
code_comment,
code_comment_oneCloseBracket
} State;
if (Rf_xlength(x_sxp) != 1) {
Rf_error("Input HTML must be a character vector of length 1");
}
SEXP input_sxp = STRING_ELT(x_sxp, 0);
if (!(Rf_getCharCE(input_sxp) == CE_UTF8 || str_is_ASCII(CHAR(input_sxp)))) {
Rf_warning("Input HTML must have a UTF-8 encoding");
}
SEXP str;
SEXP pieces = Rf_allocVector(STRSXP, 10);
R_xlen_t pieces_num = 0;
PROTECT_INDEX pieces_idx;
PROTECT_WITH_INDEX(pieces, &pieces_idx);
const char* input = CHAR(input_sxp);
int pieceStartIdx = 0;
R_xlen_t len = Rf_xlength(input_sxp);
char c;
State state = html;
for (R_xlen_t i=0; i < len; i++) {
c = input[i];
switch (state) {
case html:
switch (c) {
case '{':
state = html_oneOpenBracket; break;
}
break;
case html_oneOpenBracket:
switch (c) {
case '{':
state = code;
str = PROTECT(Rf_mkCharLenCE(input + pieceStartIdx, i - pieceStartIdx - 1, CE_UTF8));
SET_STRING_ELT2(pieces, pieces_num++, str, pieces_idx);
UNPROTECT(1);
pieceStartIdx = i + 1;
break;
default:
state = html;
}
break;
case code:
switch (c) {
case '}':
state = code_oneCloseBracket; break;
case '\'':
state = code_string1; break;
case '"':
state = code_string2; break;
case '`':
state = code_backtick; break;
case '%':
state = code_percentOp; break;
case '#':
state = code_comment; break;
}
break;
case code_oneCloseBracket:
switch (c) {
case '}':
state = html;
str = PROTECT(Rf_mkCharLenCE(input + pieceStartIdx, i - pieceStartIdx - 1, CE_UTF8));
SET_STRING_ELT2(pieces, pieces_num++, str, pieces_idx);
UNPROTECT(1);
pieceStartIdx = i + 1;
break;
default: state = code;
}
break;
case code_string1:
switch (c) {
case '\\':
state = code_string1_backslash; break;
case '\'':
state = code; break;
}
break;
case code_string1_backslash:
state = code_string1;
break;
case code_string2:
switch (c) {
case '\\':
state = code_string2_backslash; break;
case '\"':
state = code; break;
}
break;
case code_string2_backslash:
state = code_string2;
break;
case code_backtick:
switch (c) {
case '\\':
state = code_backtick_backslash; break;
case '`':
state = code; break;
}
break;
case code_backtick_backslash:
state = code_backtick;
break;
case code_percentOp:
switch (c) {
case '%':
state = code; break;
}
break;
case code_comment:
switch (c) {
case '}':
state = code_comment_oneCloseBracket; break;
case '\n':
state = code; break;
}
break;
case code_comment_oneCloseBracket:
switch (c) {
case '}':
state = html;
str = PROTECT(Rf_mkCharLenCE(input + pieceStartIdx, i - pieceStartIdx - 1, CE_UTF8));
SET_STRING_ELT2(pieces, pieces_num++, str, pieces_idx);
UNPROTECT(1);
pieceStartIdx = i + 1;
break;
default:
state = code;
}
break;
}
}
if (!(state == html || state == html_oneOpenBracket)) {
Rf_error("HTML template did not end in html state (missing closing \"}}\").");
}
// Add ending HTML piece
str = PROTECT(Rf_mkCharLenCE(input + pieceStartIdx, len - pieceStartIdx, CE_UTF8));
SET_STRING_ELT2(pieces, pieces_num++, str, pieces_idx);
UNPROTECT(1);
if (pieces_num < Rf_xlength(pieces)) {
// Resize the vector to the actual number of pieces
REPROTECT(pieces = Rf_xlengthgets(pieces, pieces_num), pieces_idx);
}
UNPROTECT(1);
return pieces;
}
htmltools/NAMESPACE 0000644 0001762 0000144 00000005331 14600330155 013515 0 ustar ligges users # Generated by roxygen2: do not edit by hand
S3method(as.character,html)
S3method(as.character,shiny.tag)
S3method(as.character,shiny.tag.env)
S3method(as.character,shiny.tag.list)
S3method(as.character,shiny.tag.query)
S3method(as.tags,character)
S3method(as.tags,default)
S3method(as.tags,html)
S3method(as.tags,html_dependency)
S3method(as.tags,list)
S3method(as.tags,shiny.tag)
S3method(as.tags,shiny.tag.env)
S3method(as.tags,shiny.tag.function)
S3method(as.tags,shiny.tag.list)
S3method(as.tags,shiny.tag.query)
S3method(format,html)
S3method(format,htmltools.selector)
S3method(format,htmltools.selector.list)
S3method(format,shiny.tag)
S3method(format,shiny.tag.env)
S3method(format,shiny.tag.list)
S3method(format,shiny.tag.query)
S3method(print,html)
S3method(print,html_dependency)
S3method(print,htmltools.selector)
S3method(print,htmltools.selector.list)
S3method(print,shiny.tag)
S3method(print,shiny.tag.env)
S3method(print,shiny.tag.list)
S3method(print,shiny.tag.query)
S3method(save_html,default)
S3method(str,shiny.tag.env)
export("htmlDependencies<-")
export(HTML)
export(a)
export(as.tags)
export(attachDependencies)
export(bindFillRole)
export(br)
export(browsable)
export(capturePlot)
export(code)
export(copyDependencyToDir)
export(css)
export(defaultPngDevice)
export(div)
export(doRenderTags)
export(em)
export(extractPreserveChunks)
export(findDependencies)
export(h1)
export(h2)
export(h3)
export(h4)
export(h5)
export(h6)
export(hr)
export(htmlDependencies)
export(htmlDependency)
export(htmlEscape)
export(htmlPreserve)
export(htmlTemplate)
export(html_print)
export(img)
export(includeCSS)
export(includeHTML)
export(includeMarkdown)
export(includeScript)
export(includeText)
export(is.browsable)
export(is.singleton)
export(knit_print.html)
export(knit_print.html_dependency)
export(knit_print.shiny.tag)
export(knit_print.shiny.tag.list)
export(makeDependencyRelative)
export(p)
export(parseCssColors)
export(plotTag)
export(pre)
export(renderDependencies)
export(renderDocument)
export(renderTags)
export(resolveDependencies)
export(restorePreserveChunks)
export(save_html)
export(singleton)
export(span)
export(strong)
export(subtractDependencies)
export(suppressDependencies)
export(surroundSingletons)
export(tag)
export(tagAddRenderHook)
export(tagAppendAttributes)
export(tagAppendChild)
export(tagAppendChildren)
export(tagFunction)
export(tagGetAttribute)
export(tagHasAttribute)
export(tagInsertChildren)
export(tagList)
export(tagQuery)
export(tagSetChildren)
export(tags)
export(takeSingletons)
export(urlEncodePath)
export(validateCssUnit)
export(withTags)
import(digest)
import(rlang)
import(utils)
importFrom(fastmap,fastmap)
importFrom(fastmap,faststack)
importFrom(rlang,obj_address)
useDynLib(htmltools, .registration = TRUE)
htmltools/NEWS.md 0000644 0001762 0000144 00000033700 15113345760 013405 0 ustar ligges users # htmltools 0.5.9
* Fix test for testthat 3.3.0. (#442)
# htmltools 0.5.8.1
* `capturePlot()`s examples are now only run when `interactive()`. (#429)
# htmltools 0.5.8
## Improvements
* The fill CSS attached to fillable containers and fill items with `bindFillRole()` now uses a [CSS cascade layer](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Cascade_layers) named `htmltools` to reduce the precedence order of the fill CSS. (#425)
* Improved documentation for boolean attributes in `tagAppendAttributes()` to note that they can be set via an `NA` value, e.g. `tagAppendAttributes(div(), contenteditable = NA)` creates `
`. (thanks @russHyde, #427)
## Bug fixes
* `bindFillRole()` now attaches its `HTMLDependency()` to fill items, thus reducing the possibility of filling layout breaking due to missing CSS. (#421)
# htmltools 0.5.7
## New Features
* `save_html()` is now an S3 generic, allowing for more customization over how certain classes are saved to an HTML file. (#411)
## Improvements
* Fill items no longer set `overflow: auto` or `width: 100%` by default. (#401)
* `css()` now fully supports setting custom CSS properties (or CSS variables) via inline styles. When the name of a value passed to `css()` starts with `--`, it will be treated as a custom CSS property and absolutely no changes will be made to the variable. For example, `css("--font_size" = "3em")` returns `--font_size:3em;` while `css(font_size = "3em")` will return `font-size:3em`. (#402)
## Bug fixes
* `{htmltools}` now requires `{rlang}` version 1.0.0 or higher. (#403)
# htmltools 0.5.6.1
## Improvements
* `tagQuery()` no longer throws an error when attempting to traverse a NULL value with r-devel. (#407)
# htmltools 0.5.6
## Possibly breaking changes
* Closed #386: Fillable containers no longer set `overflow: auto` by default. Instead, they set `min-width` and `min-height` to `0` to ensure that fill items a constrained in the fillable container without clipping their direct children. (#387)
* Closed #370: Non-fill items in fillable containers no longer grow or shrink and instead respect their intrinsic size. Use `height` to control the height of non-fill items in fillable containers and `min-height` and `max-height` on fill items to limit how much they are allowed to grow or shrink within a fillable container. (#391)
## Minor improvements
* Closed #375: calling `htmlDependency()` or a function that returns an `htmlDependency()` object (e.g., `fontawesome::fa_html_dependency()`) in an R chunk in an R Markdown or knitr-powered Quarto document will now include the dependency rather than printing the object structure. If you want to print the object structure, you can use `print()` or `str()`. (#376)
* Closed #124: `includeHTML()` will now issue a warning if it detects that the file passed to it contains a complete HTML document. `includeHTML()` is designed to include HTML fragments where the contents of the file can be written directly into the current app or document, but subtle errors can occur when the file contains a complete HTML document. In most cases, you should instead use `tags$iframe()` to embed external documents. (#382)
# htmltools 0.5.5
## Bug fixes
* Closed #355: `tagQuery()` was failing to select elements with tag names that contained hyphens. (@slodge, #302)
* Closed #366: `tagQuery()`'s `find()` method no longer errors out when tags contain language objects. (#366)
# htmltools 0.5.4
## New Features
* Added a new `bindFillRole()` function for modifying `tag()` object(s) into tags that are allowed to grow and shrink when their parent is opinionated about their height. See `help(bindFillRole, "htmltools")` for documentation and examples. Note the primary motivation for adding these functions is to power `{bslib}`'s new `card()` API (in particular, [responsive sizing](https://rstudio.github.io/bslib/articles/cards.html#responsive-sizing)) as well as the new `fill` arguments in `shiny::plotOutput()`, `shiny::imageOutput()`, `shiny::uiOutput()`, `htmlwidgets::sizingPolicy()`, and `htmlwidgets::shinyWidgetOutput()`. (#343)
## Bug fixes
* Closed #331: `copyDependencyToDir()` creates `outputDir` recursively, which happens in Quarto or when `lib_dir` points to a nested directory. (@gadenbuie, #332)
* Closed #346: `tagQuery()`'s `$remove()`, `$after()`, `$before()`, `$replaceWith()` had a bug that prevented expected behavior when sibling children values where not tag elements. (#348)
# htmltools 0.5.3
## Breaking changes
* Closed #305: `htmlPreserve()` no longer uses _inline_ code blocks for Pandoc's raw attribute feature when used inside a _non_-inline knitr/rmarkdown code chunk, and as a result, in this case, an additional `` tag is no longer wrapped around the HTML content. (#306)
## Bug fixes
* Closed #301: `tagQuery()` was failing to copy all `tagList()` html dependencies within nest child tag lists. `tagQuery()` will now relocate html dependencies as child objects. (#302)
* Closed #290: htmltools previously did not specify which version of fastmap to use, and would fail to install with an old version of fastmap. (#291)
* `copyDependencyToDir()` no longer creates empty directories for dependencies that do not have any files. (@gadenbuie, #276)
* Closed #320: `copyDependencyToDir()` now works with dependencies with specified attributes. (@dmurdoch, #321)
# htmltools 0.5.2
## Breaking Changes
* Closed #205: When calling `tagGetAttribute(x)` on an object with a non-atomic attribute, a list of untouched values will be returned. It is still recommended to only store character values inside attributes. (#212)
## New Features & Improvements
* `{htmltools}` now has its own `{pkgdown}` site hosted at .
* The new `tagQuery()` function provides a [jQuery](https://jquery.com/) inspired interface to query and/or modify HTML `tag()` (e.g., `div()`) or `tagList()` objects. To learn more, see the [{pkgdown} article](https://rstudio.github.io/htmltools/articles/tagQuery.html). (#208)
* Added `tagAddRenderHook()` for delaying modification of a tag object until it is rendered. A list of render-time hooks may also be added via the new `.renderHook` argument added to all `tag()` functions. (#215)
* Closed #243: Added `withTags(.noWS)` to change the default whitespace behavior for all tags within the call to `withTags()`. (#245)
* Closed #251: Added `.cssSelector` parameters to tag modifying functions such as `tagAppendChildren()` or `tagAppendChildren()`. The `.cssSelector` allows you to target particular (inner) tags of interest. See `tagAppendChildren()` for examples. (#224)
* Closed #225: Added `tagInsertChildren()` to be able to insert child tag objects at a particular location. (#224)
## Bug Fixes
* When retrieving a tag attribute using `tagGetAttribute(tag, attr)`, `NA` values will be removed before combining remaining attribute values. If all attribute values are `NA`, then a single `NA` value will be returned. (#212)
* Closed #197: Fixed rendering of boolean attributes in `"
# from getting an tag inserted in the middle
markup <- paste(sep = "\n",
"This is *emphasized* text in markdown.",
htmlPreserve(""),
"Here is some more *emphasized text*."
)
extracted <- extractPreserveChunks(markup)
markup <- extracted$value
# Just think of this next line as Markdown processing
output <- gsub("\\\\*(.*?)\\\\*", "\\\\1 ", markup)
output <- restorePreserveChunks(output, extracted$chunks)
output
}
htmltools/man/HTML.Rd 0000644 0001762 0000144 00000001470 14600330155 014104 0 ustar ligges users % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/tags.R
\name{HTML}
\alias{HTML}
\title{Mark Characters as HTML}
\usage{
HTML(text, ..., .noWS = NULL)
}
\arguments{
\item{text}{The text value to mark with HTML}
\item{...}{Any additional values to be converted to character and
concatenated together}
\item{.noWS}{Character vector used to omit some of the whitespace that would
normally be written around this HTML. Valid options include \code{before},
\code{after}, and \code{outside} (equivalent to \code{before} and
\code{end}).}
}
\value{
The input \code{text}, but marked as HTML.
}
\description{
Marks the given text as HTML, which means the \link{tag} functions will know
not to perform HTML escaping on it.
}
\examples{
el <- div(HTML("I like turtles "))
cat(as.character(el))
}
htmltools/man/tagAppendChild.Rd 0000644 0001762 0000144 00000004216 14600330155 016210 0 ustar ligges users % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/tags.R
\name{tagAppendChild}
\alias{tagAppendChild}
\alias{tagAppendChildren}
\alias{tagSetChildren}
\alias{tagInsertChildren}
\title{Modify tag contents}
\usage{
tagAppendChild(tag, child, .cssSelector = NULL)
tagAppendChildren(tag, ..., .cssSelector = NULL, list = NULL)
tagSetChildren(tag, ..., .cssSelector = NULL, list = NULL)
tagInsertChildren(tag, after, ..., .cssSelector = NULL, list = NULL)
}
\arguments{
\item{tag}{a \link{tag} object.}
\item{child}{A child element to append to a parent tag.}
\item{.cssSelector}{A character string containing a \href{https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors}{CSS selector}
for targeting particular (inner) tags of interest. At the moment, only a
combination of
\href{https://www.w3.org/TR/CSS22/selector.html#type-selectors}{type} (e.g,
\code{div}), \href{https://www.w3.org/TR/CSS22/selector.html#class-html}{class}
(e.g., \code{.my-class}),
\href{https://www.w3.org/TR/CSS22/selector.html#id-selectors}{id} (e.g.,
\verb{#myID}), and
\href{https://www.w3.org/TR/CSS22/selector.html#universal-selector}{universal}
(\code{*}) selectors within a given \href{https://www.w3.org/TR/CSS22/selector.html#selector-syntax}{simple selector} is
supported. Note, if \code{.cssSelector} is used, the returned tags will have
their \verb{$children} fields flattened to a single \code{list()} via \code{\link[=tagQuery]{tagQuery()}}.}
\item{...}{a collection of \code{child} elements.}
\item{list}{Deprecated. Use \verb{!!!} instead to splice into \code{...}.}
\item{after}{an integer value (i.e., subscript) referring to the child position to append after.}
}
\description{
Modify the contents (aka children) of a \link{tag} object.
}
\examples{
html <- div(a(), h1())
tagAppendChild(html, span())
tagAppendChild(html, .cssSelector = "a", span())
tagAppendChildren(html, span(), p())
tagAppendChildren(html, .cssSelector = "a", span(), p())
tagSetChildren(html, span(), p())
tagInsertChildren(html, after = 1, span(), p())
}
\seealso{
\code{\link[=tagAppendAttributes]{tagAppendAttributes()}}, \code{\link[=tagQuery]{tagQuery()}}
}
htmltools/man/knitr_methods.Rd 0000644 0001762 0000144 00000001341 14600330155 016207 0 ustar ligges users % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/tags.R
\name{knitr_methods}
\alias{knitr_methods}
\alias{knit_print.shiny.tag}
\alias{knit_print.html}
\alias{knit_print.shiny.tag.list}
\alias{knit_print.html_dependency}
\title{Knitr S3 methods}
\usage{
knit_print.shiny.tag(x, ..., inline = FALSE)
knit_print.html(x, ..., inline = FALSE)
knit_print.shiny.tag.list(x, ..., inline = FALSE)
knit_print.html_dependency(x, ..., inline = FALSE)
}
\arguments{
\item{x}{Object to knit_print}
\item{...}{Additional knit_print arguments}
\item{inline}{Whether or not the code chunk is inline.}
}
\description{
These S3 methods are necessary to allow HTML tags to print themselves in
knitr/rmarkdown documents.
}
htmltools/man/urlEncodePath.Rd 0000644 0001762 0000144 00000000642 14600330155 016075 0 ustar ligges users % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/html_dependency.R
\name{urlEncodePath}
\alias{urlEncodePath}
\title{Encode a URL path}
\usage{
urlEncodePath(x)
}
\arguments{
\item{x}{A character vector.}
}
\description{
Encode characters in a URL path. This is the same as
\code{\link[utils:URLencode]{utils::URLencode()}} with \code{reserved = TRUE} except that
\code{/} is preserved.
}
htmltools/man/singleton.Rd 0000644 0001762 0000144 00000001230 14600330155 015334 0 ustar ligges users % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/tags.R
\name{singleton}
\alias{singleton}
\alias{is.singleton}
\title{Include content only once}
\usage{
singleton(x, value = TRUE)
is.singleton(x)
}
\arguments{
\item{x}{A \code{\link[=tag]{tag()}}, text, \code{\link[=HTML]{HTML()}}, or list.}
\item{value}{Whether the object should be a singleton.}
}
\description{
Use \code{singleton} to wrap contents (tag, text, HTML, or lists) that should
be included in the generated document only once, yet may appear in the
document-generating code more than once. Only the first appearance of the
content (in document order) will be used.
}
htmltools/man/renderDocument.Rd 0000644 0001762 0000144 00000003027 14600330155 016316 0 ustar ligges users % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/template.R
\name{renderDocument}
\alias{renderDocument}
\title{Render an html_document object}
\usage{
renderDocument(x, deps = NULL, processDep = identity)
}
\arguments{
\item{x}{An object of class \code{html_document}, typically generated by the
\code{\link[=htmlTemplate]{htmlTemplate()}} function.}
\item{deps}{Any extra web dependencies to add to the html document. This can
be an object created by \code{\link[=htmlDependency]{htmlDependency()}}, or a list of such
objects. These dependencies will be added first, before other dependencies.}
\item{processDep}{A function that takes a "raw" html_dependency object and
does further processing on it. For example, when \code{renderDocument} is
called from Shiny, the function \code{\link[shiny:createWebDependency]{shiny::createWebDependency()}} is
used; it modifies the href and tells Shiny to serve a particular path on
the filesystem.}
}
\value{
An \code{\link[=HTML]{HTML()}} string, with UTF-8 encoding.
}
\description{
This function renders \code{html_document} objects, and returns a string with
the final HTML content. It calls the \code{\link[=renderTags]{renderTags()}} function to
convert any shiny.tag objects to HTML. It also finds any any web dependencies
(created by \code{\link[=htmlDependency]{htmlDependency()}}) that are attached to the tags, and
inserts those. To do the insertion, this function finds the string
\code{""} in the document, and replaces it with the web
dependencies.
}
htmltools/man/defaultPngDevice.Rd 0000644 0001762 0000144 00000001247 14600330155 016553 0 ustar ligges users % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/images.R
\name{defaultPngDevice}
\alias{defaultPngDevice}
\title{Determine the best PNG device for your system}
\usage{
defaultPngDevice()
}
\value{
A graphics device function.
}
\description{
Returns the best PNG-based graphics device for your system, in the opinion of
the \code{htmltools} maintainers. On Mac,
\code{\link[grDevices:png]{grDevices::png()}} is used; on all other
platforms, either \code{\link[ragg:agg_png]{ragg::agg_png()}} or
\code{\link[Cairo:Cairo]{Cairo::CairoPNG()}} are used if their packages
are installed. Otherwise, \code{\link[grDevices:png]{grDevices::png()}} is
used.
}
htmltools/man/findDependencies.Rd 0000644 0001762 0000144 00000001043 14600330155 016563 0 ustar ligges users % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/tags.R
\name{findDependencies}
\alias{findDependencies}
\title{Collect attached dependencies from HTML tag object}
\usage{
findDependencies(tags, tagify = TRUE)
}
\arguments{
\item{tags}{A tag-like object to search for dependencies.}
\item{tagify}{Whether to tagify the input before searching for dependencies.}
}
\value{
A list of \code{\link[=htmlDependency]{htmlDependency()}} objects.
}
\description{
Walks a hierarchy of tags looking for attached dependencies.
}
htmltools/man/figures/ 0000755 0001762 0000144 00000000000 14600330155 014513 5 ustar ligges users htmltools/man/figures/plotly-taglist.png 0000644 0001762 0000144 00000020272 14600330155 020214 0 ustar ligges users PNG
IHDR Z T v 7PLTEwDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ĕ}U >IDATxca
A
DКJA*bQ{S[Z^[=19"CP ?%&5l|>?8EEv$$$$%%% - - -IhIhIhIBKBKBKZZZВВВ$$$$%%u)gFh%%%ZZZZhIhIhIh%%%ZZZZh -$$ZhIhIh%ВВBKh%%BKBKB-5LjЪ/MےɴM2-UQu|YD∕}cZj"c$9zUÈЪ"un>:%*3V'
ZF{aݦZuDKhUPcSwxVh
^$EZMQSh
m;L)8^eDKhUh¾aZ"ճ8PeDKh^sd֧QWczSeIcRhmӴ#wΫ*h QKOul7XVޡ^xc{IrY|\+2Fh0bdI/Nh Q:^P:ۘTZU/2>êwN`-5VZɚXޟ$mh#ZBkz%VOyaDKhVһavČ5ūUF8:ؙ:ۄЪYZ,ZhIh%ZhIhIh%ZhIh%ZhIh%ZhIh%ZhIh%ZhIh%ZhIh%ZhIh%ZhIh{\Zh -$Zh -$$ZhIhIh%ZhIh%ZoZh -$Zh -$Zh -$Zh -$Zh -$Zh -$Zh %ZhIh%ZhIh%ZhIh%ZhIh%ZhIV%h -ZZh -ZZh -ZZh -ZZhiR*{xo#ZBl=[WL-;JNےɴ͘FV1wW_Ѽ8beF(/rNW1h :IO ?5nꈖЪZg
=ySl:%v s7-la~9VZUn0)UYZ~đqʈЪ1;ws,xY=UGVuxƝFjwniu]~uU*#ZBJ_^w#sUGr@X[9>8VeDKhUwYt|Y|\,KV>Kh QU80;bƚ#S#ZBk̎}DKhmBKh%BKh%BKh%BKh%J=-DZB-uh^h -Z5k-B-BKh@BKh%BKh%ZhIh%Z5h -Zh -ZB;5EKh%BKh%,&x%Z熖BKB-BKhBKh%BKh-B-JFKh%Zh%Zh -ZߊֹGKh%ЪZh -ZB-4YiZ`&_aq20-к"ZBkU o|~Zh jǁh -Zh q*saohŎBKhZ#BKhM /Q_PZZhuQLkh|;K-$UPq&߫"-BKh]pZ|<ooC-Ҥu&hUXXhݺ%iBk";,yqʾ UxIovL*}vBu},>$GhumwSCk|]Ax>"Fh/,lhք^:^coB+Ip(Z`&z"ZԱEl& IPR+wuL3==Wjw
h44J.DQ)ۙzwgW~F}H;X?
JVo1r5Q]"3YZUkPX= Bkz$66FOКdfꝓ@(5AI2<BkYZ=-}9ZY3O(\;=@yl_PZ?jn]̺TTM[-9\jJi]]:-sgS'۳sN̥#4=g[#VcK<>jђZ+ewn:39{7suZh
D~__Js؞ x45w%_bsϵ.Tٵ&RDԭq_55r{-*_ρ E
iڞlʋ[ix
);Ӵ95)ٔNJx/}y}AO!|YhjhO>/uU#KSsu+.̦d{j-^zfZ5Ah9M90iXc %sܞ4ڿѝ/=ȼV
z:.L˶xk=Ұ5-upjh;6.YavQ-
qlIH+ih֮ZGDߊ}7d4z>s[Gنxoœg-J[ҴnߢnZ/dE[Z{lvТxTzq_џbf6'uZo5tfZ/OOOFYM':.|~YJAӢ.S9ƹQk:k;2ͫ6xED{TӚ:,\x꧙MsϔoV|XFequDzèt몶ВKhIhIhIhIBKBKBKZZZВВВ$$$$%%% - - -IhIhIhIBKBKBKe?iCBKmo3NUWs϶[Ϗ]Om{'c's:<
m]n{G:y>7
e
wڝfwًhnw/Ŝ~kֿ7aŖ
i{w$|si7\wzvX}Z3a;oވ(\-oGĮr_@+vsqZhRWłrFXȹ%hJM{ʮxH{h
}Q08ZzaZ
F{]EhG]ML9Lk{?1aTR:"v#ylUƘov'hE<=xQ=IKkOK//.ZMY-i]~ZǭM~sigrXd2ILj[;dKg5CԬh՛$:i3I|BK>t9.չҏ>2G̈"ܪv#Z:/iz+2kޜ~Ɏ"χNDK=o>y~磓guڃhIhIhIBKBKBKZZZВВВ$$$$%%% - - -IhIhIhIBKBKBKZZZВВВ$$$$%%% - - -IhIhIhIBKBKBKZZZВВВ$$$$%%% - - -IhIhIhIBKBKBKRi]T#ВВВBKBKBKB- - - -$$$ВВВBKh%%BKBKB-ZB- - -ZZZh -$$ZhIhIh%ВВBKh%%BKBKB-uAi
yZhIhIhIh%%%BKBKB-ZB- - -ZZZh -$$ZhIhISZ'1%*iӲE9d2}eDKhUloDh_Ѽ8beFVžHX|8I^k0%*xH`]ϫh ICփ^X)VZ:ԝ-kij/ª#ZBB+IVStŔ#ZBB⎤Dk0G
k{"WZ߰oֱel&TZߜy?iԕTslZǤ'iuGϝWUZ'AToı*B9 CpOWdh ѮaZ5^(};TZt'#t16%^(,e|$33U%#ZBk5?IbFXi,KV>È+wkW٫eE%w3;-u6-ZB- - -ZZZh -$$ZhIhIh%ВВBKh%%BKBKB$ВВВBKBKBKB-ZB- - BKBKB-gKh%ВВBKh%%BKBKB-ZB- - -ZZZh -$$ZhIhIh%ВВBKh%%BKBKB-ti}=c<7-U+Ԗ%]'mdZfL#ZB|ٿ͊b~O+/h^o#ZBko9W\'ѫbFhpXv7}^uDKhU3¼)VZU;ux0h C7MQŔ#ZBjm`L)=ǫh ӻs7"ճ8Pe=<_ZΪZ;uYx-5AuGg]!f=8Eyx,>.,tEfг &x'f{քHl(,l5a՚@(5AI2<BkYZ=-}9ZY3};PO`oon~*t3[ӯcg-nS}1FwRkW77>qO
Z#ҮIػßnW;>xihV_;hl}ioCk;n*tfh=3%d7#NޟO[3k&a5v[L/пƏW^)u֖W:ך=ՕA3k<*,\ou뫔oI9Z]]'К<+n'pa鞸cƬtoyj):֩5q-Z2H^bFqߢ4cFGʷ