Skip to content

Commit f866d38

Browse files
authored
Support for meta tags in Dash for R (#142)
* ✨ initial support for meta tags * support arbitrary tags * 🚨 add tests * 🔬 add asserts * add reference to meta tag PR * ⏩ indent meta tags
1 parent cd33eba commit f866d38

File tree

4 files changed

+105
-5
lines changed

4 files changed

+105
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file.
33

44
## Unreleased
55
### Added
6+
- Support for adding `<meta>` tags to index [#142](https://github.com/plotly/dashR/pull/142)
67
- Hot reloading now supported in debug mode [#127](https://github.com/plotly/dashR/pull/127)
78
- Support for displaying Dash for R applications within RStudio's viewer pane when `use_viewer = TRUE`
89
- Clientside callbacks written in JavaScript are now supported [#130](https://github.com/plotly/dashR/pull/130)

R/dash.R

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#' assets_url_path = '/assets',
1212
#' assets_ignore = '',
1313
#' serve_locally = TRUE,
14+
#' meta_tags = NULL,
1415
#' routes_pathname_prefix = '/',
1516
#' requests_pathname_prefix = '/',
1617
#' external_scripts = NULL,
@@ -34,6 +35,9 @@
3435
#' cannot use this to prevent access to sensitive files. \cr
3536
#' `serve_locally` \tab \tab Whether to serve HTML dependencies locally or
3637
#' remotely (via URL).\cr
38+
#' `meta_tags` \tab \tab List of lists. HTML `<meta>`tags to be added to the index page.
39+
#' Each list element should have the attributes and values for one tag, eg:
40+
#' `list(name = 'description', content = 'My App')`.\cr
3741
#' `routes_pathname_prefix` \tab \tab a prefix applied to the backend routes.\cr
3842
#' `requests_pathname_prefix` \tab \tab a prefix applied to request endpoints
3943
#' made by Dash's front-end.\cr
@@ -158,6 +162,7 @@ Dash <- R6::R6Class(
158162
assets_url_path = '/assets',
159163
assets_ignore = '',
160164
serve_locally = TRUE,
165+
meta_tags = NULL,
161166
routes_pathname_prefix = NULL,
162167
requests_pathname_prefix = NULL,
163168
external_scripts = NULL,
@@ -181,6 +186,7 @@ Dash <- R6::R6Class(
181186
private$suppress_callback_exceptions <- suppress_callback_exceptions
182187
private$app_root_path <- getAppPath()
183188
private$app_launchtime <- as.integer(Sys.time())
189+
private$meta_tags <- meta_tags
184190

185191
# config options
186192
self$config$routes_pathname_prefix <- resolve_prefix(routes_pathname_prefix, "DASH_ROUTES_PATHNAME_PREFIX")
@@ -815,6 +821,7 @@ Dash <- R6::R6Class(
815821
# private fields defined on initiation
816822
name = NULL,
817823
serve_locally = NULL,
824+
meta_tags = NULL,
818825
assets_folder = NULL,
819826
assets_url_path = NULL,
820827
assets_ignore = NULL,
@@ -1231,17 +1238,21 @@ Dash <- R6::R6Class(
12311238
css_tags <- paste(c(css_deps,
12321239
css_external,
12331240
css_assets),
1234-
collapse = "\n")
1241+
collapse = "\n ")
12351242

12361243
scripts_tags <- paste(c(scripts_deps,
12371244
scripts_external,
12381245
scripts_assets,
12391246
scripts_invoke_renderer),
1240-
collapse = "\n")
1247+
collapse = "\n ")
12411248

1249+
meta_tags <- paste(generate_meta_tags(private$meta_tags),
1250+
collapse = "\n ")
1251+
12421252
return(list(css_tags = css_tags,
12431253
scripts_tags = scripts_tags,
1244-
favicon = favicon))
1254+
favicon = favicon,
1255+
meta_tags = meta_tags))
12451256
},
12461257

12471258
index = function() {
@@ -1257,11 +1268,14 @@ Dash <- R6::R6Class(
12571268
# retrieve script tags for serving in the index
12581269
scripts_tags <- all_tags[["scripts_tags"]]
12591270

1271+
# insert meta tags if present
1272+
meta_tags <- all_tags[["meta_tags"]]
1273+
12601274
private$.index <- sprintf(
12611275
'<!DOCTYPE html>
12621276
<html>
12631277
<head>
1264-
<meta charset="UTF-8"/>
1278+
%s
12651279
<title>%s</title>
12661280
%s
12671281
%s
@@ -1278,6 +1292,7 @@ Dash <- R6::R6Class(
12781292
</footer>
12791293
</body>
12801294
</html>',
1295+
meta_tags,
12811296
private$name,
12821297
favicon,
12831298
css_tags,

R/utils.R

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,32 @@ generate_js_dist_html <- function(href,
607607
}
608608
}
609609

610+
generate_meta_tags <- function(metas) {
611+
has_ie_compat <- any(vapply(metas, function(x)
612+
x$name == "http-equiv" && x$content == "X-UA-Compatible",
613+
logical(1)), na.rm=TRUE)
614+
has_charset <- any(vapply(metas, function(x)
615+
"charset" %in% names(x),
616+
logical(1)), na.rm=TRUE)
617+
618+
# allow arbitrary tags with varying numbers of keys
619+
tags <- vapply(metas,
620+
function(tag) sprintf("<meta %s>", paste(sprintf("%s=\"%s\"",
621+
names(tag),
622+
unlist(tag, use.names = FALSE)),
623+
collapse=" ")),
624+
character(1))
625+
626+
if (!has_ie_compat) {
627+
tags <- c('<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">', tags)
628+
}
629+
630+
if (!has_charset) {
631+
tags <- c('<meta charset=\"UTF-8\">', tags)
632+
}
633+
return(tags)
634+
}
635+
610636
# This function takes the list object containing asset paths
611637
# for all stylesheets and scripts, as well as the URL path
612638
# to search, then returns the absolute local path (when
@@ -970,7 +996,7 @@ modtimeFromPath <- function(path, recursive = FALSE, asset_path="") {
970996
}
971997
} else {
972998
# check if the path is for a directory or file, and handle accordingly
973-
if (dir.exists(path))
999+
if (length(path) == 1 && dir.exists(path))
9741000
modtime <- as.integer(max(file.info(list.files(path, full.names = TRUE))$mtime, na.rm=TRUE))
9751001
else
9761002
modtime <- as.integer(file.info(path)$mtime)

tests/integration/test_meta.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from selenium.webdriver.support.select import Select
2+
import time, os
3+
4+
5+
app = """
6+
library(dash)
7+
library(dashHtmlComponents)
8+
9+
app <- Dash$new(meta_tags = list(list(name = "description", content = "some content")))
10+
11+
app$layout(
12+
htmlDiv(children = "Hello world!",
13+
id = "hello-div"
14+
)
15+
)
16+
17+
app$run_server()
18+
"""
19+
20+
21+
def test_rstm001_test_meta(dashr):
22+
dashr.start_server(app)
23+
dashr.wait_for_text_to_equal(
24+
"#hello-div",
25+
"Hello world!"
26+
)
27+
assert dashr.find_element("meta[name='description']").get_attribute("content") == "some content"
28+
assert dashr.find_element("meta[charset='UTF-8']")
29+
assert dashr.find_element("meta[http-equiv='X-UA-Compatible']").get_attribute("content") == "IE=edge"
30+
31+
32+
app2 = """
33+
library(dash)
34+
library(dashHtmlComponents)
35+
36+
app <- Dash$new(meta_tags = list(list(charset = "ISO-8859-1"),
37+
list(name = "keywords", content = "dash,pleasant,productive"),
38+
list(`http-equiv` = 'content-type', content = 'text/html')))
39+
40+
app$layout(
41+
htmlDiv(children = "Hello world!",
42+
id = "hello-div"
43+
)
44+
)
45+
46+
app$run_server()
47+
"""
48+
49+
50+
def test_rstm002_test_meta(dashr):
51+
dashr.start_server(app2)
52+
dashr.wait_for_text_to_equal(
53+
"#hello-div",
54+
"Hello world!"
55+
)
56+
assert dashr.find_element("meta[charset='ISO-8859-1']")
57+
assert dashr.find_element("meta[name='keywords']").get_attribute("content") == "dash,pleasant,productive"
58+
assert dashr.find_element("meta[http-equiv='content-type']").get_attribute("content") == "text/html"

0 commit comments

Comments
 (0)