boB Rudis
6 years ago
19 changed files with 663 additions and 12 deletions
@ -1,14 +1,27 @@ |
|||||
# Generated by roxygen2: do not edit by hand |
# Generated by roxygen2: do not edit by hand |
||||
|
|
||||
|
S3method(print,gepetto) |
||||
|
export("%>%") |
||||
export(chrome_dump_pdf) |
export(chrome_dump_pdf) |
||||
export(chrome_read_html) |
export(chrome_read_html) |
||||
export(chrome_shot) |
export(chrome_shot) |
||||
export(chrome_version) |
export(chrome_version) |
||||
export(download_chromium) |
export(download_chromium) |
||||
|
export(gep_active) |
||||
|
export(gep_debug) |
||||
|
export(gep_render_har) |
||||
|
export(gep_render_html) |
||||
|
export(gep_render_magick) |
||||
|
export(gep_render_pdf) |
||||
|
export(gepetto) |
||||
export(get_chrome_env) |
export(get_chrome_env) |
||||
export(set_chrome_env) |
export(set_chrome_env) |
||||
|
import(httr) |
||||
import(magick) |
import(magick) |
||||
import(processx) |
import(processx) |
||||
import(tools) |
import(tools) |
||||
import(utils) |
import(utils) |
||||
import(xml2) |
import(xml2) |
||||
|
importFrom(HARtools,readHAR) |
||||
|
importFrom(jsonlite,fromJSON) |
||||
|
importFrom(magrittr,"%>%") |
||||
|
@ -0,0 +1,279 @@ |
|||||
|
#' Create a connection to a Gepetto API server |
||||
|
#' |
||||
|
#' @md |
||||
|
#' @param host where is it running? Defaults to "`localhost`" |
||||
|
#' @param port same, but what port? Defaults to `8080` since the most common |
||||
|
#' use case is that you have `gepetto` running in a Docker container. |
||||
|
#' Use `3000` if you're running it locally via `npm` as that's the default |
||||
|
#' port for development.` |
||||
|
#' @return A `gepetto` connection object |
||||
|
#' @export |
||||
|
#' @examples \dontrun{ |
||||
|
#' gepetto() |
||||
|
#' } |
||||
|
gepetto <- function(host = "localhost", port = 8080) { |
||||
|
|
||||
|
list( |
||||
|
host = host, |
||||
|
port = port |
||||
|
) -> out |
||||
|
|
||||
|
class(out) <- c("gepetto") |
||||
|
|
||||
|
out |
||||
|
|
||||
|
} |
||||
|
|
||||
|
#' Print |
||||
|
#' @md |
||||
|
#' @param x `gepetto` object |
||||
|
#' @param ... unused |
||||
|
#' @keywords internal |
||||
|
#' @export |
||||
|
print.gepetto <- function(x, ...) { |
||||
|
cat("<gepetto@", x$host, ":", x$port, ">\n", sep="") |
||||
|
} |
||||
|
|
||||
|
#' Render a page in a javascript context and serialize to HTML |
||||
|
#' |
||||
|
#' @md |
||||
|
#' @param gep a gepetto connection object |
||||
|
#' @param url the URL to fetch and render |
||||
|
#' @param width,height viewport width/height |
||||
|
#' @return HTML |
||||
|
#' @export |
||||
|
#' @examples \dontrun{ |
||||
|
#' gepetto(port=3000) %>% |
||||
|
#' gep_render_html("https://r-project.org/") |
||||
|
#' } |
||||
|
gep_render_html <- function(gep, url, width=1440, height=5000) { |
||||
|
|
||||
|
httr::GET( |
||||
|
url = sprintf("http://%s:%s/render_html", gep$host, gep$port), |
||||
|
query = list( |
||||
|
url = url, |
||||
|
width = width, |
||||
|
height = height |
||||
|
) |
||||
|
|
||||
|
) -> res |
||||
|
|
||||
|
httr::stop_for_status(res) |
||||
|
|
||||
|
out <- httr::content(res, as="text") |
||||
|
out <- xml2::read_html(out) |
||||
|
|
||||
|
out |
||||
|
|
||||
|
} |
||||
|
|
||||
|
#' Render a page in a javascript context and serialize to HAR |
||||
|
#' |
||||
|
#' TODO: Modify the `puppeteer-har` node module to allow for saving content |
||||
|
#' |
||||
|
#' @md |
||||
|
#' @param gep a gepetto connection object |
||||
|
#' @param url the URL to fetch and render |
||||
|
#' @param width,height viewport width/height |
||||
|
#' @return HAR |
||||
|
#' @note content is not returned, just HAR information |
||||
|
#' @export |
||||
|
#' @examples \dontrun{ |
||||
|
#' gepetto(port=3000) %>% |
||||
|
#' gep_render_har("https://r-project.org/") |
||||
|
#' } |
||||
|
gep_render_har <- function(gep, url, width=1440, height=5000) { |
||||
|
|
||||
|
httr::GET( |
||||
|
url = sprintf("http://%s:%s/render_har", gep$host, gep$port), |
||||
|
query = list( |
||||
|
url = url, |
||||
|
width = width, |
||||
|
height = height |
||||
|
) |
||||
|
) -> res |
||||
|
|
||||
|
httr::stop_for_status(res) |
||||
|
|
||||
|
out <- httr::content(res, as="text") |
||||
|
out <- HARtools::readHAR(out) |
||||
|
|
||||
|
out |
||||
|
|
||||
|
} |
||||
|
|
||||
|
#' Render a page in a javascript context and take a screenshot |
||||
|
#' |
||||
|
#' @md |
||||
|
#' @param gep a gepetto connection object |
||||
|
#' @param url the URL to fetch and render |
||||
|
#' @param width,height viewport width/height |
||||
|
#' @return `magick` image |
||||
|
#' @export |
||||
|
#' @examples \dontrun{ |
||||
|
#' gepetto(port=3000) %>% |
||||
|
#' gep_render_magick("https://r-project.org/") |
||||
|
#' } |
||||
|
gep_render_magick <- function(gep, url, width=1440, height=5000) { |
||||
|
httr::GET( |
||||
|
url = sprintf("http://%s:%s/render_png", gep$host, gep$por), |
||||
|
query = list( |
||||
|
url = url, |
||||
|
width = width, |
||||
|
height = height |
||||
|
) |
||||
|
) -> res |
||||
|
httr::stop_for_status(res) |
||||
|
out <- httr::content(res) |
||||
|
out <- magick::image_read(out) |
||||
|
out |
||||
|
} |
||||
|
|
||||
|
# #' Take a screenshot of the current browser page |
||||
|
# #' |
||||
|
# #' @md |
||||
|
# #' @param gep a gepetto connection object |
||||
|
# #' @return `magick` image |
||||
|
# #' @export |
||||
|
# #' @examples \dontrun{ |
||||
|
# #' gepetto(port=3000) %>% |
||||
|
# #' gep_screenshot() |
||||
|
# #' } |
||||
|
# gep_screenshot <- function(gep) { |
||||
|
# |
||||
|
# httr::GET( |
||||
|
# url = sprintf("http://%s:%s/screenshot", gep$host, gep$por), |
||||
|
# ) -> res |
||||
|
# |
||||
|
# httr::stop_for_status(res) |
||||
|
# |
||||
|
# out <- httr::content(res) |
||||
|
# out <- magick::image_read(out) |
||||
|
# out |
||||
|
# |
||||
|
# } |
||||
|
|
||||
|
#' Render a page in a javascript context and rendero to PDF |
||||
|
#' |
||||
|
#' @md |
||||
|
#' @param gep a gepetto connection object |
||||
|
#' @param url the URL to fetch and render |
||||
|
#' @param path directory & filename to save the PDF to. If `NULL` will be saved |
||||
|
#' to a tempfile and it location will be returned. |
||||
|
#' @param overwrite if `TRUE` any existing `path` (file) will be overwritten |
||||
|
#' @param width,height viewport width/height |
||||
|
#' @return object |
||||
|
#' @export |
||||
|
#' @examples \dontrun{ |
||||
|
#' gepetto(port=3000) %>% |
||||
|
#' gep_render_pdf("https://r-project.org/") |
||||
|
#' } |
||||
|
gep_render_pdf <- function(gep, url, path=NULL, overwrite=TRUE, width=1440, height=5000) { |
||||
|
|
||||
|
if (is.null(path)) { |
||||
|
path <- tempfile(fileext = ".pdf") |
||||
|
} else { |
||||
|
path <- path.expand(path) |
||||
|
} |
||||
|
|
||||
|
httr::GET( |
||||
|
url = sprintf("http://%s:%s/render_pdf", gep$host, gep$por), |
||||
|
query = list( |
||||
|
url = url, |
||||
|
width = width, |
||||
|
height = height |
||||
|
), |
||||
|
httr::write_disk(path = path) |
||||
|
) -> res |
||||
|
|
||||
|
httr::stop_for_status(res) |
||||
|
|
||||
|
path |
||||
|
|
||||
|
} |
||||
|
|
||||
|
#' Get "debug-level" information of a running gepetto server |
||||
|
#' |
||||
|
#' @md |
||||
|
#' @param gep a gepetto connection object |
||||
|
#' @return debug info |
||||
|
#' @export |
||||
|
#' @examples \dontrun{ |
||||
|
#' gepetto() %>% |
||||
|
#' gep_debug() %>% |
||||
|
#' str() |
||||
|
#' } |
||||
|
gep_debug <- function(gep) { |
||||
|
|
||||
|
httr::GET( |
||||
|
url = sprintf("http://%s:%s/_debug", gep$host, gep$port) |
||||
|
) -> res |
||||
|
|
||||
|
httr::stop_for_status(res) |
||||
|
|
||||
|
out <- httr::content(res, as="text") |
||||
|
out <- jsonlite::fromJSON(out) |
||||
|
|
||||
|
out |
||||
|
|
||||
|
} |
||||
|
|
||||
|
#' Get test whether the gepetto server is active |
||||
|
#' |
||||
|
#' @md |
||||
|
#' @param gep a gepetto connection object |
||||
|
#' @return logical (`TRUE` if alive) |
||||
|
#' @export |
||||
|
#' @examples \dontrun{ |
||||
|
#' gepetto() %>% |
||||
|
#' gep_active() |
||||
|
#' } |
||||
|
gep_active <- function(gep) { |
||||
|
|
||||
|
s_GET( |
||||
|
url = sprintf("http://%s:%s/_ping", gep$host, gep$port) |
||||
|
) -> res |
||||
|
|
||||
|
res <- stop_for_problem(res) |
||||
|
|
||||
|
httr::stop_for_status(res) |
||||
|
|
||||
|
out <- httr::content(res, as="text") |
||||
|
out <- jsonlite::fromJSON(out) |
||||
|
|
||||
|
out$status == "ok" |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
#' #' Execute Puppeteer commands |
||||
|
#' #' |
||||
|
#' #' This is a **low-level** call that makes **you** responsible for the return |
||||
|
#' #' type. Eventually there will likely be more boilerplate code for handling return |
||||
|
#' #' values. |
||||
|
#' #' |
||||
|
#' #' @md |
||||
|
#' #' @param gep a gepetto connection object |
||||
|
#' #' @param js Puppeteer js to execute in-browser |
||||
|
#' #' @references [Puppeteer API](https://github.com/GoogleChrome/puppeteer/blob/v1.7.0/docs/api.md) |
||||
|
#' #' @export |
||||
|
#' #' @examples \dontrun{ |
||||
|
#' #' gepetto() %>% |
||||
|
#' #' gep_exec() |
||||
|
#' #' } |
||||
|
#' gep_exec <- function(gep, js) { |
||||
|
#' |
||||
|
#' httr::POST( |
||||
|
#' url = sprintf("http://%s:%s/exec", gep$host, gep$port), |
||||
|
#' encode = "form", |
||||
|
#' body = js |
||||
|
#' ) -> res |
||||
|
#' |
||||
|
#' httr::stop_for_status(res) |
||||
|
#' |
||||
|
#' out <- httr::content(res, as="text") |
||||
|
#' # out <- jsonlite::fromJSON(out) |
||||
|
#' # |
||||
|
#' out |
||||
|
#' |
||||
|
#' } |
@ -0,0 +1,11 @@ |
|||||
|
#' Pipe operator |
||||
|
#' |
||||
|
#' See \code{magrittr::\link[magrittr]{\%>\%}} for details. |
||||
|
#' |
||||
|
#' @name %>% |
||||
|
#' @rdname pipe |
||||
|
#' @keywords internal |
||||
|
#' @export |
||||
|
#' @importFrom magrittr %>% |
||||
|
#' @usage lhs \%>\% rhs |
||||
|
NULL |
@ -0,0 +1,90 @@ |
|||||
|
# Less cool counterparts to purrr's side-effect capture-rs |
||||
|
# |
||||
|
# Most of the helper functions are 100% from output.R in purrr repo |
||||
|
# |
||||
|
# @param quiet Hide errors (`TRUE`, the default), or display them |
||||
|
# as they occur? |
||||
|
# @param otherwise Default value to use when an error occurs. |
||||
|
# |
||||
|
# @return `safely`: wrapped function instead returns a list with |
||||
|
# components `result` and `error`. One value is always `NULL`. |
||||
|
# |
||||
|
# `quietly`: wrapped function instead returns a list with components |
||||
|
# `result`, `output`, `messages` and `warnings`. |
||||
|
# |
||||
|
# `possibly`: wrapped function uses a default value (`otherwise`) |
||||
|
# whenever an error occurs. |
||||
|
safely <- function(.f, otherwise = NULL, quiet = TRUE) { |
||||
|
function(...) capture_error(.f(...), otherwise, quiet) |
||||
|
} |
||||
|
|
||||
|
quietly <- function(.f) { |
||||
|
function(...) capture_output(.f(...)) |
||||
|
} |
||||
|
|
||||
|
possibly <- function(.f, otherwise, quiet = TRUE) { |
||||
|
force(otherwise) |
||||
|
function(...) { |
||||
|
tryCatch(.f(...), |
||||
|
error = function(e) { |
||||
|
if (!quiet) |
||||
|
message("Error: ", e$message) |
||||
|
otherwise |
||||
|
}, |
||||
|
interrupt = function(e) { |
||||
|
stop("Terminated by user", call. = FALSE) |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
capture_error <- function(code, otherwise = NULL, quiet = TRUE) { |
||||
|
tryCatch( |
||||
|
list(result = code, error = NULL), |
||||
|
error = function(e) { |
||||
|
if (!quiet) |
||||
|
message("Error: ", e$message) |
||||
|
|
||||
|
list(result = otherwise, error = e) |
||||
|
}, |
||||
|
interrupt = function(e) { |
||||
|
stop("Terminated by user", call. = FALSE) |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
capture_output <- function(code) { |
||||
|
warnings <- character() |
||||
|
wHandler <- function(w) { |
||||
|
warnings <<- c(warnings, w$message) |
||||
|
invokeRestart("muffleWarning") |
||||
|
} |
||||
|
|
||||
|
messages <- character() |
||||
|
mHandler <- function(m) { |
||||
|
messages <<- c(messages, m$message) |
||||
|
invokeRestart("muffleMessage") |
||||
|
} |
||||
|
|
||||
|
temp <- file() |
||||
|
sink(temp) |
||||
|
on.exit({ |
||||
|
sink() |
||||
|
close(temp) |
||||
|
}) |
||||
|
|
||||
|
result <- withCallingHandlers( |
||||
|
code, |
||||
|
warning = wHandler, |
||||
|
message = mHandler |
||||
|
) |
||||
|
|
||||
|
output <- paste0(readLines(temp, warn = FALSE), collapse = "\n") |
||||
|
|
||||
|
list( |
||||
|
result = result, |
||||
|
output = output, |
||||
|
warnings = warnings, |
||||
|
messages = messages |
||||
|
) |
||||
|
} |
@ -0,0 +1,23 @@ |
|||||
|
% Generated by roxygen2: do not edit by hand |
||||
|
% Please edit documentation in R/gepetto.R |
||||
|
\name{gep_active} |
||||
|
\alias{gep_active} |
||||
|
\title{Get test whether the gepetto server is active} |
||||
|
\usage{ |
||||
|
gep_active(gep) |
||||
|
} |
||||
|
\arguments{ |
||||
|
\item{gep}{a gepetto connection object} |
||||
|
} |
||||
|
\value{ |
||||
|
logical (\code{TRUE} if alive) |
||||
|
} |
||||
|
\description{ |
||||
|
Get test whether the gepetto server is active |
||||
|
} |
||||
|
\examples{ |
||||
|
\dontrun{ |
||||
|
gepetto() \%>\% |
||||
|
gep_active() |
||||
|
} |
||||
|
} |
@ -0,0 +1,24 @@ |
|||||
|
% Generated by roxygen2: do not edit by hand |
||||
|
% Please edit documentation in R/gepetto.R |
||||
|
\name{gep_debug} |
||||
|
\alias{gep_debug} |
||||
|
\title{Get "debug-level" information of a running gepetto server} |
||||
|
\usage{ |
||||
|
gep_debug(gep) |
||||
|
} |
||||
|
\arguments{ |
||||
|
\item{gep}{a gepetto connection object} |
||||
|
} |
||||
|
\value{ |
||||
|
debug info |
||||
|
} |
||||
|
\description{ |
||||
|
Get "debug-level" information of a running gepetto server |
||||
|
} |
||||
|
\examples{ |
||||
|
\dontrun{ |
||||
|
gepetto() \%>\% |
||||
|
gep_debug() \%>\% |
||||
|
str() |
||||
|
} |
||||
|
} |
@ -0,0 +1,30 @@ |
|||||
|
% Generated by roxygen2: do not edit by hand |
||||
|
% Please edit documentation in R/gepetto.R |
||||
|
\name{gep_render_har} |
||||
|
\alias{gep_render_har} |
||||
|
\title{Render a page in a javascript context and serialize to HAR} |
||||
|
\usage{ |
||||
|
gep_render_har(gep, url, width = 1440, height = 5000) |
||||
|
} |
||||
|
\arguments{ |
||||
|
\item{gep}{a gepetto connection object} |
||||
|
|
||||
|
\item{url}{the URL to fetch and render} |
||||
|
|
||||
|
\item{width, height}{viewport width/height} |
||||
|
} |
||||
|
\value{ |
||||
|
HAR |
||||
|
} |
||||
|
\description{ |
||||
|
TODO: Modify the \code{puppeteer-har} node module to allow for saving content |
||||
|
} |
||||
|
\note{ |
||||
|
content is not returned, just HAR information |
||||
|
} |
||||
|
\examples{ |
||||
|
\dontrun{ |
||||
|
gepetto(port=3000) \%>\% |
||||
|
gep_render_har("https://r-project.org/") |
||||
|
} |
||||
|
} |
@ -0,0 +1,27 @@ |
|||||
|
% Generated by roxygen2: do not edit by hand |
||||
|
% Please edit documentation in R/gepetto.R |
||||
|
\name{gep_render_html} |
||||
|
\alias{gep_render_html} |
||||
|
\title{Render a page in a javascript context and serialize to HTML} |
||||
|
\usage{ |
||||
|
gep_render_html(gep, url, width = 1440, height = 5000) |
||||
|
} |
||||
|
\arguments{ |
||||
|
\item{gep}{a gepetto connection object} |
||||
|
|
||||
|
\item{url}{the URL to fetch and render} |
||||
|
|
||||
|
\item{width, height}{viewport width/height} |
||||
|
} |
||||
|
\value{ |
||||
|
HTML |
||||
|
} |
||||
|
\description{ |
||||
|
Render a page in a javascript context and serialize to HTML |
||||
|
} |
||||
|
\examples{ |
||||
|
\dontrun{ |
||||
|
gepetto(port=3000) \%>\% |
||||
|
gep_render_html("https://r-project.org/") |
||||
|
} |
||||
|
} |
@ -0,0 +1,27 @@ |
|||||
|
% Generated by roxygen2: do not edit by hand |
||||
|
% Please edit documentation in R/gepetto.R |
||||
|
\name{gep_render_magick} |
||||
|
\alias{gep_render_magick} |
||||
|
\title{Render a page in a javascript context and take a screenshot} |
||||
|
\usage{ |
||||
|
gep_render_magick(gep, url, width = 1440, height = 5000) |
||||
|
} |
||||
|
\arguments{ |
||||
|
\item{gep}{a gepetto connection object} |
||||
|
|
||||
|
\item{url}{the URL to fetch and render} |
||||
|
|
||||
|
\item{width, height}{viewport width/height} |
||||
|
} |
||||
|
\value{ |
||||
|
\code{magick} image |
||||
|
} |
||||
|
\description{ |
||||
|
Render a page in a javascript context and take a screenshot |
||||
|
} |
||||
|
\examples{ |
||||
|
\dontrun{ |
||||
|
gepetto(port=3000) \%>\% |
||||
|
gep_render_magick("https://r-project.org/") |
||||
|
} |
||||
|
} |
@ -0,0 +1,33 @@ |
|||||
|
% Generated by roxygen2: do not edit by hand |
||||
|
% Please edit documentation in R/gepetto.R |
||||
|
\name{gep_render_pdf} |
||||
|
\alias{gep_render_pdf} |
||||
|
\title{Render a page in a javascript context and rendero to PDF} |
||||
|
\usage{ |
||||
|
gep_render_pdf(gep, url, path = NULL, overwrite = TRUE, width = 1440, |
||||
|
height = 5000) |
||||
|
} |
||||
|
\arguments{ |
||||
|
\item{gep}{a gepetto connection object} |
||||
|
|
||||
|
\item{url}{the URL to fetch and render} |
||||
|
|
||||
|
\item{path}{directory & filename to save the PDF to. If \code{NULL} will be saved |
||||
|
to a tempfile and it location will be returned.} |
||||
|
|
||||
|
\item{overwrite}{if \code{TRUE} any existing \code{path} (file) will be overwritten} |
||||
|
|
||||
|
\item{width, height}{viewport width/height} |
||||
|
} |
||||
|
\value{ |
||||
|
object |
||||
|
} |
||||
|
\description{ |
||||
|
Render a page in a javascript context and rendero to PDF |
||||
|
} |
||||
|
\examples{ |
||||
|
\dontrun{ |
||||
|
gepetto(port=3000) \%>\% |
||||
|
gep_render_pdf("https://r-project.org/") |
||||
|
} |
||||
|
} |
@ -0,0 +1,27 @@ |
|||||
|
% Generated by roxygen2: do not edit by hand |
||||
|
% Please edit documentation in R/gepetto.R |
||||
|
\name{gepetto} |
||||
|
\alias{gepetto} |
||||
|
\title{Create a connection to a Gepetto API server} |
||||
|
\usage{ |
||||
|
gepetto(host = "localhost", port = 8080) |
||||
|
} |
||||
|
\arguments{ |
||||
|
\item{host}{where is it running? Defaults to "\code{localhost}"} |
||||
|
|
||||
|
\item{port}{same, but what port? Defaults to \code{8080} since the most common |
||||
|
use case is that you have \code{gepetto} running in a Docker container. |
||||
|
Use \code{3000} if you're running it locally via \code{npm} as that's the default |
||||
|
port for development.`} |
||||
|
} |
||||
|
\value{ |
||||
|
A \code{gepetto} connection object |
||||
|
} |
||||
|
\description{ |
||||
|
Create a connection to a Gepetto API server |
||||
|
} |
||||
|
\examples{ |
||||
|
\dontrun{ |
||||
|
gepetto() |
||||
|
} |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
% Generated by roxygen2: do not edit by hand |
||||
|
% Please edit documentation in R/utils-pipe.R |
||||
|
\name{\%>\%} |
||||
|
\alias{\%>\%} |
||||
|
\title{Pipe operator} |
||||
|
\usage{ |
||||
|
lhs \%>\% rhs |
||||
|
} |
||||
|
\description{ |
||||
|
See \code{magrittr::\link[magrittr]{\%>\%}} for details. |
||||
|
} |
||||
|
\keyword{internal} |
@ -0,0 +1,17 @@ |
|||||
|
% Generated by roxygen2: do not edit by hand |
||||
|
% Please edit documentation in R/gepetto.R |
||||
|
\name{print.gepetto} |
||||
|
\alias{print.gepetto} |
||||
|
\title{Print} |
||||
|
\usage{ |
||||
|
\method{print}{gepetto}(x, ...) |
||||
|
} |
||||
|
\arguments{ |
||||
|
\item{x}{\code{gepetto} object} |
||||
|
|
||||
|
\item{...}{unused} |
||||
|
} |
||||
|
\description{ |
||||
|
Print |
||||
|
} |
||||
|
\keyword{internal} |
Loading…
Reference in new issue