🔍 An RStudio addin slash regex utility belt
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

397 lines
12KB

  1. #' regexplain gadget
  2. #'
  3. #' @import miniUI
  4. #' @import shiny
  5. #' @param text Text to explore in gadget (editable using interface)
  6. #' @param start_page Open gadget to this tab, one of `"Text"`, `"Regex"`,
  7. #' `"Output"`, or `"Help"`
  8. #' @export
  9. regex_gadget <- function(text = NULL,
  10. start_page = if (is.null(text)) "Text" else "Regex") {
  11. stopifnot(requireNamespace("miniUI"), requireNamespace("shiny"))
  12. update_available <- check_version()
  13. # ---- UI ----
  14. ui <- miniPage(
  15. shiny::includeCSS(system.file("styles", "style.css", package = "regexplain")),
  16. shiny::includeCSS(system.file("styles", "gadget.css", package = "regexplain")),
  17. gadgetTitleBar(
  18. "regexplain",
  19. right = miniTitleBarButton("done", "Send Regex To Console", TRUE)
  20. ),
  21. miniTabstripPanel(
  22. selected = match.arg(start_page, c("Text", "Regex", "Output", "Help")),
  23. # --- UI - Tab - Text ----
  24. miniTabPanel(
  25. "Text", icon = icon('file-text-o'),
  26. miniContentPanel(
  27. fillCol(
  28. textAreaInputAlt('text',
  29. label = "Text to search or parse",
  30. value = paste(text, collapse = "\n"),
  31. resize = "both",
  32. width = "100%",
  33. height="90%",
  34. placeholder = "Paste, enter, or edit your sample text here.")
  35. )
  36. )
  37. ),
  38. # ---- UI - Tab - Regex ----
  39. miniTabPanel(
  40. "Regex", icon = icon('terminal'),
  41. miniContentPanel(
  42. fillCol(
  43. flex = c(1, 3),
  44. fillCol(
  45. flex = c(1, 1),
  46. textInputCode('pattern', 'Regex', width = "100%",
  47. placeholder = "Enter regex, single \\ okay"),
  48. checkboxGroupInput(
  49. 'regex_options',
  50. label = HTML(
  51. '<div style="font-size: 1.25rem;">',
  52. 'Option Groups: ',
  53. '<span style="color: #337ab7;">regexplain</span>,',
  54. '<span style="color: #5cb85c;">all</span>, ',
  55. '<span style="color: #f0ad4e;">base only</span>',
  56. '</div>'
  57. ),
  58. inline = TRUE,
  59. width = "90%",
  60. choiceValues = list(
  61. "text_break_lines",
  62. "ignore.case",
  63. "fixed",
  64. "perl",
  65. "useBytes"),
  66. choiceNames = list(
  67. HTML('<span style="color: #337ab7;">Break Lines</span>'),
  68. HTML('<span style="color: #5cb85c;">Ignore Case</span>'),
  69. HTML('<span style="color: #5cb85c;">Fixed/Literal</span>'),
  70. HTML('<span style="color: #f0ad4e;">Perl Style</span>'),
  71. HTML('<span style="color: #f0ad4e;">Use Bytes</span>')),
  72. selected = c('text_break_lines')
  73. )
  74. ),
  75. tags$div(
  76. class = "gadget-result",
  77. style = "overflow-y: scroll; height: 100%;",
  78. htmlOutput('result')
  79. )
  80. )
  81. )
  82. ),
  83. # ---- UI - Tab - Output ----
  84. miniTabPanel(
  85. "Output", icon = icon("table"),
  86. miniContentPanel(
  87. fillCol(
  88. flex = c(1, 3),
  89. inputPanel(
  90. tags$div(
  91. width = "100%;",
  92. selectInput('regexFn', label = 'Apply Function',
  93. choices = regexFn_choices),
  94. tags$span(class = "help-block",
  95. style = "font-size:1.25rem; margin-top:-10px; margin-bottom:0px; margin-left:4px;",
  96. "Adjust options in Regex tab")
  97. ),
  98. uiOutput("output_sub")
  99. ),
  100. # verbatimTextOutput('output_result', placeholder = TRUE)
  101. tags$pre(
  102. id = "output_result",
  103. class = "shiny-text-output",
  104. style = "overflow-y: scroll; height: 100%;"
  105. )
  106. )
  107. )
  108. ),
  109. # ---- UI - Tab - Help ----
  110. miniTabPanel(
  111. "Help", icon = icon("support"),
  112. help_ui("help")
  113. )
  114. )
  115. )
  116. # ---- Server ----
  117. server <- function(input, output, session) {
  118. if (!is.null(update_available)) {
  119. showModal(
  120. modalDialog(
  121. title = "Update Available \U1F389",
  122. easyClose = TRUE,
  123. footer = modalButton("OK"),
  124. tagList(
  125. tags$p(
  126. "Version", update_available$version, "is",
  127. tags$a(href = update_available$link,
  128. "available on GitHub.")
  129. ),
  130. if ("devtools" %in% installed.packages()) tags$p(
  131. "The fastest way to update is with devtools:",
  132. tags$pre(
  133. "devtools::update_packages(\"gadenbuie/regexplain\")"
  134. )
  135. ),
  136. tags$p(
  137. class = 'help-block',
  138. "This message won't be shown again during this R session."
  139. )
  140. )
  141. )
  142. )
  143. }
  144. # ---- Server - Global ----
  145. rtext <- reactive({
  146. x <- if ('text_break_lines' %in% input$regex_options) {
  147. strsplit(input$text, "\n")[[1]]
  148. } else input$text
  149. x
  150. })
  151. pattern <- reactive({
  152. sanitize_text_input(input$pattern)
  153. })
  154. alert_result <- function(msg, type = "danger") {
  155. msg <- gsub("\n", "<br>", msg)
  156. msg <- gsub("\t", "&nbsp;&nbsp;", msg)
  157. paste0("<pre class='alert alert-", type, "' ",
  158. "style='padding: 4px; margin-top: 1px; margin-bottom: 4px;'>",
  159. paste(msg, collapse = "<br>"),
  160. "</pre>")
  161. }
  162. # ---- Server - Tab - Regex ----
  163. output$result <- renderUI({
  164. if (is.null(rtext())) return(NULL)
  165. if (pattern() == "") {
  166. return(toHTML(paste('<p class="results">', escape_html(rtext()), "</p>", collapse = "")))
  167. }
  168. res <- NULL
  169. error_message <- NULL
  170. warning_message <- NULL
  171. tryCatch({
  172. res <- paste(
  173. view_regex(
  174. rtext(),
  175. pattern(),
  176. ignore.case = 'ignore.case' %in% input$regex_options,
  177. perl = 'perl' %in% input$regex_options,
  178. fixed = 'fixed' %in% input$regex_options,
  179. useBytes = 'useBytes' %in% input$regex_options,
  180. # invert = 'invert' %in% input$regex_options,
  181. render = FALSE,
  182. escape = TRUE,
  183. exact = FALSE),
  184. collapse = ""
  185. )
  186. },
  187. error = function(e) {
  188. error_message <<- alert_result(e$message, "danger")
  189. },
  190. warning = function(w) {
  191. warning_message <<- alert_result(w$message, "warning")
  192. })
  193. if (is.null(res)) res <- toHTML(
  194. paste('<p class="results">', escape_html(rtext()), "</p>", collapse = "")
  195. )
  196. toHTML(paste(error_message, warning_message, res))
  197. })
  198. # ---- Server - Tab - Output ----
  199. regexFn_replacement_val <- NULL
  200. output$output_sub <- renderUI({
  201. req(input$regexFn)
  202. if (!input$regexFn %in% regexFn_substitute) return(NULL)
  203. textInputCode('regexFn_replacement', 'Subsitution',
  204. value = regexFn_replacement_val,
  205. placeholder = "Replacement Text")
  206. })
  207. replacement <- reactive({
  208. req(input$regexFn)
  209. if (!input$regexFn %in% regexFn_substitute) {
  210. NULL
  211. } else {
  212. regexFn_replacement_val <<- input$regexFn_replacement
  213. sanitize_text_input(input$regexFn_replacement)
  214. }
  215. })
  216. output$output_result <- renderPrint({
  217. req(input$regexFn)
  218. regexPkg <- get_pkg_namespace(input$regexFn)
  219. regexFn <- getFromNamespace(input$regexFn, regexPkg)
  220. req_sub_arg <- input$regexFn %in% regexFn_substitute
  221. x <- if (regexPkg == "base") {
  222. if (req_sub_arg) {
  223. req(replacement())
  224. regexFn(pattern(), replacement(), rtext(),
  225. ignore.case = 'ignore.case' %in% input$regex_options,
  226. perl = 'perl' %in% input$regex_options,
  227. fixed = 'fixed' %in% input$regex_options,
  228. useBytes = 'useBytes' %in% input$regex_options)
  229. } else {
  230. regexFn(pattern(), rtext(),
  231. ignore.case = 'ignore.case' %in% input$regex_options,
  232. perl = 'perl' %in% input$regex_options,
  233. fixed = 'fixed' %in% input$regex_options,
  234. useBytes = 'useBytes' %in% input$regex_options)
  235. }
  236. } else if (regexPkg == "stringr") {
  237. if (req_sub_arg) {
  238. req(replacement())
  239. regexFn(
  240. rtext(),
  241. stringr::regex(
  242. pattern(),
  243. ignore_case = 'ignore.case' %in% input$regex_options,
  244. literal = 'fixed' %in% input$regex_options
  245. ),
  246. replacement()
  247. )
  248. } else {
  249. regexFn(
  250. rtext(),
  251. stringr::regex(
  252. pattern(),
  253. ignore_case = 'ignore.case' %in% input$regex_options,
  254. literal = 'fixed' %in% input$regex_options
  255. )
  256. )
  257. }
  258. } else {
  259. "Um. Not sure how I got here."
  260. }
  261. print(x)
  262. })
  263. # ---- Server - Tab - Help ----
  264. help_text <- callModule(help_server, "help")
  265. # ---- Server - Tab - Exit ----
  266. observeEvent(input$done, {
  267. # browser()
  268. if (pattern() != "") {
  269. pattern <- paste0('pattern <- "', escape_backslash(pattern()), '"')
  270. rstudioapi::sendToConsole(pattern, FALSE)
  271. }
  272. stopApp()
  273. })
  274. observeEvent(input$cancel, {
  275. stopApp()
  276. })
  277. }
  278. viewer <- shiny::paneViewer(minHeight = 1000)
  279. runGadget(ui, server, viewer = viewer)
  280. }
  281. # ---- Gadget Helper Functions and Variables ----
  282. sanitize_text_input <- function(x) {
  283. if (is.null(x) || !nchar(x)) return(x)
  284. if (grepl("\\u|\\x|\\N|\\a|\\o", x)) {
  285. try({
  286. y <- stringi::stri_unescape_unicode(x)
  287. }, silent = TRUE)
  288. if (!is.na(y)) x <- y
  289. }
  290. # x <- gsub("\u201C|\u201D", '"', x)
  291. # x <- gsub("\u2018|\u2019", "'", x)
  292. x
  293. }
  294. toHTML <- function(...) {
  295. x <- paste(..., collapse = "")
  296. x <- gsub("\n", "\\\\n", x)
  297. x <- gsub("\t", "\\\\t", x)
  298. x <- gsub("\r", "\\\\r", x)
  299. HTML(x)
  300. }
  301. regexFn_choices <- list(
  302. "Choose a function" = "",
  303. base = c(
  304. "grep",
  305. "grepl",
  306. "sub", #<<
  307. "gsub", #<<
  308. "regexpr",
  309. "gregexpr",
  310. "regexec"
  311. ),
  312. stringr = c(
  313. "str_detect",
  314. "str_locate",
  315. "str_locate_all",
  316. "str_extract",
  317. "str_extract_all",
  318. "str_match",
  319. "str_match_all",
  320. "str_replace", #<<
  321. "str_replace_all", #<<
  322. "str_split"
  323. )
  324. )
  325. regexFn_substitute <- c(
  326. paste0(c("", "g"), "sub"),
  327. paste0("str_replace", c("", "_all"))
  328. )
  329. get_pkg_namespace <- function(fn) {
  330. x <- names(purrr::keep(regexFn_choices, ~ (fn %in% .)))
  331. if (length(x) > 1) warning(fn, " matches multiple functions in regexFn_choices, please review.")
  332. x
  333. }
  334. #' Check if an updated version is available
  335. #'
  336. #' I included this because it can be difficult to tell if your RStudio Addins
  337. #' are up to date. I may add new features that you want but you won't hear about
  338. #' the updates. This function checks if an update is available, using GitHub
  339. #' tags. If an update is available, a modal dialog is shown when you start
  340. #' the regexplain gadget. This only happens once per R session, though, so feel
  341. #' free to ignore the message.
  342. #'
  343. #' @param gh_user GitHub user account
  344. #' @param gh_repo GitHub repo name
  345. #' @param this_version The currently installed version of the package
  346. #' @keywords internal
  347. check_version <- function(
  348. gh_user = "gadenbuie",
  349. gh_repo = "regexplain",
  350. this_version = packageVersion('regexplain')
  351. ) {
  352. ok_to_check <- getOption("regexplain.no.check.version", TRUE)
  353. if (!ok_to_check) return(NULL)
  354. if (!requireNamespace('jsonlite', quietly = TRUE)) return(NULL)
  355. gh_tags <- jsonlite::fromJSON(
  356. paste0("https://api.github.com/repos/", gh_user, "/", gh_repo, "/git/refs/tags"),
  357. simplifyDataFrame = TRUE
  358. )
  359. gh_tags$tag <- sub("refs/tags/", "", gh_tags$ref, fixed = TRUE)
  360. gh_tags$version <- sub("^v\\.?", "", gh_tags$tag)
  361. if (any(gh_tags$version > this_version)) {
  362. max_version <- max(gh_tags$version)
  363. max_tag <- gh_tags$tag[gh_tags$version == max_version]
  364. options(regexplain.no.check.version = FALSE)
  365. return(
  366. list(
  367. version = max_version,
  368. link = paste("https://github.com", gh_user, gh_repo, "releases/tag", max_tag, sep = "/")
  369. )
  370. )
  371. } else return(NULL)
  372. }