diff --git a/R/htmltools-package.R b/R/htmltools-package.R index 293b0595..7895c67b 100644 --- a/R/htmltools-package.R +++ b/R/htmltools-package.R @@ -6,6 +6,7 @@ #' @import utils digest #' @importFrom fastmap fastmap faststack #' @importFrom rlang obj_address +#' @useDynLib htmltools, .registration = TRUE ## usethis namespace: end NULL diff --git a/R/tags.R b/R/tags.R index e35691bb..0cb0caf0 100644 --- a/R/tags.R +++ b/R/tags.R @@ -52,6 +52,9 @@ registerMethods <- function(methods) { c("knitr", "knit_print", "shiny.tag.list"), c("knitr", "knit_print", "html_dependency") )) + + # Initialize htmltools C globals + .Call(htmltools_initialize, ns_env("htmltools")) } depListToNamedDepList <- function(dependencies) { @@ -674,7 +677,6 @@ tags <- lapply(known_tags, function(tagname) { new_function( args = exprs(... = , .noWS = NULL, .renderHook = NULL), expr({ - validateNoWS(.noWS) contents <- dots_list(...) tag(!!tagname, contents, .noWS = .noWS, .renderHook = .renderHook) }), @@ -768,53 +770,17 @@ hr <- tags$hr #' added to a particular `tag` via [tagAddRenderHook()]. #' @export tag <- function(`_tag_name`, varArgs, .noWS = NULL, .renderHook = NULL) { - validateNoWS(.noWS) - # Get arg names; if not a named list, use vector of empty strings - varArgsNames <- names2(varArgs) - - # Named arguments become attribs, dropping NULL and length-0 values - named_idx <- nzchar(varArgsNames) - attribs <- dropNullsOrEmpty(varArgs[named_idx]) - - # Unnamed arguments are flattened and added as children. - # Use unname() to remove the names attribute from the list, which would - # consist of empty strings anyway. - children <- unname(varArgs[!named_idx]) - - st <- list(name = `_tag_name`, - attribs = attribs, - children = children) - - # Conditionally include the `.noWS` field. - # We do this to avoid breaking the hashes of existing tags that weren't leveraging .noWS. if (!is.null(.noWS)) { - st$.noWS <- .noWS + noWSOptions <- c("before", "after", "after-begin", "before-end", "outside", "inside") + arg_match(.noWS, noWSOptions, multiple = TRUE) } - # Conditionally include the `.renderHooks` field. - # We do this to avoid breaking the hashes of existing tags that weren't leveraging .renderHooks. - if (!is.null(.renderHook)) { - if (!is.list(.renderHook)) { - .renderHook <- list(.renderHook) - } - st$.renderHooks <- .renderHook - } - - # Return tag data structure - structure(st, class = "shiny.tag") + .Call(new_tag, `_tag_name`, varArgs, .noWS, .renderHook); } isTagList <- function(x) { is.list(x) && (inherits(x, "shiny.tag.list") || identical(class(x), "list")) } -noWSOptions <- c("before", "after", "after-begin", "before-end", "outside", "inside") -# Ensure that the provided `.noWS` string contains only valid options -validateNoWS <- function(.noWS) { - if (!all(.noWS %in% noWSOptions)) { - stop("Invalid .noWS option(s) '", paste(.noWS, collapse="', '") ,"' specified.") - } -} - #' @include utils.R tagWrite <- function(tag, textWriter, indent=0, eol = "\n") { diff --git a/src/init.c b/src/init.c index 4771e59b..cfbb7a7a 100644 --- a/src/init.c +++ b/src/init.c @@ -5,9 +5,15 @@ /* .Call calls */ extern SEXP template_dfa(SEXP); +extern SEXP new_tag(SEXP); + +// Defined below +SEXP htmltools_initialize(SEXP); static const R_CallMethodDef CallEntries[] = { {"template_dfa", (DL_FUNC) &template_dfa, 1}, + {"new_tag", (DL_FUNC) &new_tag, 4}, + {"htmltools_initialize", (DL_FUNC) &htmltools_initialize, 1}, {NULL, NULL, 0} }; @@ -16,3 +22,11 @@ void R_init_htmltools(DllInfo *dll) R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); R_useDynamicSymbols(dll, FALSE); } + +// utils.c +void htmltools_initialize_utils(SEXP); + +SEXP htmltools_initialize(SEXP ns) { + htmltools_initialize_utils(ns); + return R_NilValue; +} diff --git a/src/tag.c b/src/tag.c new file mode 100644 index 00000000..240a9547 --- /dev/null +++ b/src/tag.c @@ -0,0 +1,111 @@ +#include "utils.h" + +SEXP have_name(SEXP x) { + SEXP nms = PROTECT(Rf_getAttrib(x, R_NamesSymbol)); + R_xlen_t n = Rf_xlength(x); + SEXP out = PROTECT(Rf_allocVector(LGLSXP, n)); + + if (nms == R_NilValue) { + for (R_xlen_t i = 0; i < n; ++i) { + SET_LOGICAL_ELT(out, i, 0); + } + } else { + for (R_xlen_t i = 0; i < n; ++i) { + SEXP nm_i = STRING_ELT(nms, i); + SET_LOGICAL_ELT(out, i, nm_i != NA_STRING & nm_i != chr_empty); + } + } + + UNPROTECT(2); + return out; +} + +SEXP new_tag(SEXP tagName, SEXP varArgs, SEXP noWS, SEXP renderHook) { + R_xlen_t n = Rf_xlength(varArgs); + + // TODO validate that varArgs is a list + + // Unnamed arguments are flattened and added as children. + // Named arguments become attribs, dropping NULL and length-0 values + SEXP namedFlag = PROTECT(have_name(varArgs)); + + // Calculate number of attributes and children + R_xlen_t n_attributes = 0; + R_xlen_t n_children = n; + for (R_xlen_t i = 0; i < n; ++i) { + int arg_i_empty = Rf_xlength(VECTOR_ELT(varArgs, i)) == 0; + n_attributes = n_attributes + (arg_i_empty ? 0 : LOGICAL_ELT(namedFlag, i)); + n_children = n_children - LOGICAL_ELT(namedFlag, i); + } + + // Create attributes and children + SEXP varArgNms = Rf_getAttrib(varArgs, R_NamesSymbol); + SEXP attributes = PROTECT(Rf_allocVector(VECSXP, n_attributes)); + SEXP attribute_nms = PROTECT(Rf_allocVector(STRSXP, n_attributes)); + Rf_setAttrib(attributes, R_NamesSymbol, attribute_nms); + + SEXP children = PROTECT(Rf_allocVector(VECSXP, n_children)); + R_xlen_t i_attributes = 0; + R_xlen_t i_children = 0; + + for (R_xlen_t i = 0; i < n; ++i) { + SEXP arg_i = VECTOR_ELT(varArgs, i); + bool arg_i_empty = Rf_xlength(arg_i) == 0; + if (LOGICAL_ELT(namedFlag, i)) { + if (!arg_i_empty) { + SET_VECTOR_ELT(attributes, i_attributes, arg_i); + SEXP arg_i_nm = STRING_ELT(varArgNms, i); + SET_STRING_ELT(attribute_nms, i_attributes, arg_i_nm); + ++i_attributes; + } + } else { + SET_VECTOR_ELT(children, i_children, arg_i); + ++i_children; + } + } + + // Create tag + R_xlen_t n_fields = 3; + if (noWS != R_NilValue) { + ++n_fields; + } + if (renderHook != R_NilValue) { + ++n_fields; + } + SEXP tag = PROTECT(Rf_allocVector(VECSXP, n_fields)) ; + SEXP field_nms = PROTECT(Rf_allocVector(STRSXP, n_fields)); + Rf_setAttrib(tag, R_NamesSymbol, field_nms); + Rf_classgets(tag, tag_class); + + SET_VECTOR_ELT(tag, 0, tagName); + SET_STRING_ELT(field_nms, 0, chr_name); + SET_VECTOR_ELT(tag, 1, attributes); + SET_STRING_ELT(field_nms, 1, chr_attribs); + SET_VECTOR_ELT(tag, 2, children); + SET_STRING_ELT(field_nms, 2, chr_children); + + R_xlen_t field_i = 3; + // Conditionally include the `.noWS` field. + // We do this to avoid breaking the hashes of existing tags that weren't leveraging .noWS. + if (noWS != R_NilValue) { + SET_VECTOR_ELT(tag, field_i, noWS); + SET_STRING_ELT(field_nms, field_i, chr_nows); + ++field_i; + } + // Conditionally include the `.renderHooks` field. + // We do this to avoid breaking the hashes of existing tags that weren't leveraging .renderHooks. + if (renderHook != R_NilValue) { + SET_STRING_ELT(field_nms, field_i, chr_renderhooks); + if (TYPEOF(renderHook) == VECSXP) { + SET_VECTOR_ELT(tag, field_i, renderHook); + } else { + SEXP renderHookList = PROTECT(Rf_allocVector(VECSXP, 1)); + SET_VECTOR_ELT(renderHookList, 0, renderHook); + SET_VECTOR_ELT(tag, field_i, renderHookList); + UNPROTECT(1); + } + } + + UNPROTECT(6); + return tag; +} diff --git a/src/utils.c b/src/utils.c new file mode 100644 index 00000000..9628fcb2 --- /dev/null +++ b/src/utils.c @@ -0,0 +1,25 @@ +#include "utils.h" + +SEXP tag_class = NULL; + +SEXP chr_empty = NULL; + +SEXP chr_name = NULL; +SEXP chr_attribs = NULL; +SEXP chr_children = NULL; +SEXP chr_nows = NULL; +SEXP chr_renderhooks = NULL; + +void htmltools_initialize_utils(SEXP ns) { + tag_class = Rf_allocVector(STRSXP, 1); + R_PreserveObject(tag_class); + SET_STRING_ELT(tag_class, 0, Rf_mkChar("shiny.tag")); + + R_PreserveObject(chr_empty = Rf_mkChar("")); + + R_PreserveObject(chr_name = Rf_mkChar("name")); + R_PreserveObject(chr_attribs = Rf_mkChar("attribs")); + R_PreserveObject(chr_children = Rf_mkChar("children")); + R_PreserveObject(chr_nows = Rf_mkChar(".noWS")); + R_PreserveObject(chr_renderhooks = Rf_mkChar(".renderHooks")); +} diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 00000000..5b9fac80 --- /dev/null +++ b/src/utils.h @@ -0,0 +1,19 @@ +#ifndef HTMLTOOLS_UTILS_H +#define HTMLTOOLS_UTILS_H + +#define R_NO_REMAP +#include +#include +#include + +extern SEXP tag_class; + +extern SEXP chr_empty; + +extern SEXP chr_name; +extern SEXP chr_attribs; +extern SEXP chr_children; +extern SEXP chr_nows; +extern SEXP chr_renderhooks; + +#endif diff --git a/tests/testthat/test-tags.r b/tests/testthat/test-tags.r index 195c1f86..10424f44 100644 --- a/tests/testthat/test-tags.r +++ b/tests/testthat/test-tags.r @@ -386,6 +386,7 @@ test_that("Old tags predating rlang::list2 can still be rendered", { }) test_that("tag with noWS works",{ + skip("should tag accept only lists?") oneline <- tag("span", list(tag("strong", "Super strong", .noWS="outside"))) expect_identical(as.character(oneline), "Super strong") }) diff --git a/tests/testthat/test-textwriter.r b/tests/testthat/test-textwriter.r index 8cf83f7f..867dae79 100644 --- a/tests/testthat/test-textwriter.r +++ b/tests/testthat/test-textwriter.r @@ -94,17 +94,3 @@ describe("WSTextWriter", { expect_identical(wtw$readAll(), "b") }) }) - -describe("validateNoWS",{ - it("basically works", { - validateNoWS(NULL) - validateNoWS(noWSOptions[1]) - validateNoWS(noWSOptions[1:2]) - validateNoWS(noWSOptions) - expect_error(validateNoWS("badOption")) - expect_error(validateNoWS(c(noWSOptions, "badOption"))) - - # capitalization matters - expect_error(validateNoWS(toupper(noWSOptions[1]))) - }) -})