🔍 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.

490 lines
16KB

  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 = "Standard RegEx, e.g. \\w+_\\d{2,4}\\s+"),
  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. generate_help_ui(cheatsheet_only = FALSE)
  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. if (!requireNamespace(regexPkg, quietly = TRUE)) {
  220. return(cat(
  221. paste0(
  222. "The package `", regexPkg, "` is not installed.\n",
  223. "To preview results from this package, please run\n\n",
  224. " install.packages(\"", regexPkg, "\")"
  225. )
  226. ))
  227. }
  228. regexFn <- getFromNamespace(input$regexFn, regexPkg)
  229. req_sub_arg <- input$regexFn %in% regexFn_substitute
  230. x <- if (regexPkg == "base") {
  231. if (req_sub_arg) {
  232. req(replacement())
  233. regexFn(pattern(), replacement(), rtext(),
  234. ignore.case = 'ignore.case' %in% input$regex_options,
  235. perl = 'perl' %in% input$regex_options,
  236. fixed = 'fixed' %in% input$regex_options,
  237. useBytes = 'useBytes' %in% input$regex_options)
  238. } else {
  239. regexFn(pattern(), rtext(),
  240. ignore.case = 'ignore.case' %in% input$regex_options,
  241. perl = 'perl' %in% input$regex_options,
  242. fixed = 'fixed' %in% input$regex_options,
  243. useBytes = 'useBytes' %in% input$regex_options)
  244. }
  245. } else if (regexPkg == "stringr") {
  246. if (req_sub_arg) {
  247. req(replacement())
  248. regexFn(
  249. rtext(),
  250. stringr::regex(
  251. pattern(),
  252. ignore_case = 'ignore.case' %in% input$regex_options,
  253. literal = 'fixed' %in% input$regex_options
  254. ),
  255. replacement()
  256. )
  257. } else {
  258. regexFn(
  259. rtext(),
  260. stringr::regex(
  261. pattern(),
  262. ignore_case = 'ignore.case' %in% input$regex_options,
  263. literal = 'fixed' %in% input$regex_options
  264. )
  265. )
  266. }
  267. } else if (regexPkg == "rematch2") {
  268. regexFn(rtext(), pattern(),
  269. ignore.case = 'ignore.case' %in% input$regex_options,
  270. perl = 'perl' %in% input$regex_options,
  271. fixed = 'fixed' %in% input$regex_options,
  272. useBytes = 'useBytes' %in% input$regex_options)
  273. } else {
  274. "Um. Not sure how I got here."
  275. }
  276. print(x)
  277. })
  278. # ---- Server - Tab - Help ----
  279. source(system.file("shiny", "help_server.R", package = "regexplain"), local = TRUE)
  280. observeEvent(input$help_try_this, {
  281. tagList(
  282. tags$p("Try these examples."),
  283. tags$h4("Parse Github Repos"),
  284. tags$p("Click",
  285. actionLink("help_try_this_github", "this link"),
  286. "to try out the GitHub repo regex challenge."
  287. ),
  288. tags$h4("CSS Unit Validation"),
  289. tags$p("CSS units can be integer or decimal numbers with units such as",
  290. "in, cm, mm, em, ex, pt, px, etc.",
  291. "Try to determine if", actionLink("help_try_this_css_text", "these units"),
  292. "are", actionLink("help_try_this_css_pattern", "are valid."))
  293. ) %>%
  294. as.character() %>%
  295. help_text()
  296. })
  297. observeEvent(input$help_try_this_github, {
  298. github_repos <- c(
  299. "metacran/crandb",
  300. "jeroenooms/curl@v0.9.3",
  301. "jimhester/covr#47",
  302. "hadley/dplyr@*release",
  303. "r-lib/remotes@550a3c7d3f9e1493a2ba"
  304. )
  305. owner_rx <- "(?:(?<owner>[^/]+)/)?"
  306. repo_rx <- "(?<repo>[^/@#]+)"
  307. subdir_rx <- "(?:/(?<subdir>[^@#]*[^@#/]))?"
  308. ref_rx <- "(?:@(?<ref>[^*].*))"
  309. pull_rx <- "(?:#(?<pull>[0-9]+))"
  310. release_rx <- "(?:@(?<release>[*]release))"
  311. subtype_rx <- sprintf("(?:%s|%s|%s)?", ref_rx, pull_rx, release_rx)
  312. github_rx <- sprintf(
  313. "^(?:%s%s%s%s|(?<catchall>.*))$",
  314. owner_rx, repo_rx, subdir_rx, subtype_rx
  315. )
  316. updateTextAreaInput(session, "text", value = paste(github_repos, collapse = "\n"))
  317. updateTextInput(session, 'pattern', value = github_rx)
  318. updateCheckboxGroupInput(session, "regex_options", selected = c('text_break_lines', 'perl'))
  319. showNotification("Example Loaded! Go to RegEx Tab", type = 'message')
  320. })
  321. observeEvent(input$help_try_this_css_text, {
  322. css_units <- c(
  323. "125%","16pt","2cm","7em","3ex","24pt",
  324. ".15in","20pc","5.9vw","3.0vh","2vmin"
  325. )
  326. updateTextAreaInput(session, "text", value = paste(css_units, collapse = "\n"))
  327. })
  328. observeEvent(input$help_try_this_css_pattern, {
  329. pattern <- "^(auto|inherit|((\\.\\d+)|(\\d+(\\.\\d+)?))(%|in|cm|mm|em|ex|pt|pc|px|vh|vw|vmin|vmax))$"
  330. updateTextInput(session, "pattern", value = pattern)
  331. updateCheckboxGroupInput(session, 'regex_options', selected = c('text_break_lines', 'perl'))
  332. showNotification("Pattern loaded! Go to RegEx tab", type = "message")
  333. })
  334. # ---- Server - Tab - Exit ----
  335. observeEvent(input$done, {
  336. if (pattern() != "") {
  337. pattern <- paste0('pattern <- "', escape_backslash(pattern()), '"')
  338. if ("regexFn_replacement" %in% names(input) && replacement() != "") {
  339. pattern <- paste0(
  340. pattern, "\n",
  341. 'replacement <- "', escape_backslash(replacement()), '"'
  342. )
  343. }
  344. rstudioapi::sendToConsole(pattern, FALSE)
  345. }
  346. stopApp()
  347. })
  348. observeEvent(input$cancel, {
  349. stopApp()
  350. })
  351. }
  352. viewer <- shiny::paneViewer(minHeight = 1000)
  353. runGadget(ui, server, viewer = viewer)
  354. }
  355. # ---- Gadget Helper Functions and Variables ----
  356. sanitize_text_input <- function(x) {
  357. if (is.null(x) || !nchar(x)) return(x)
  358. rx_unicode <- "\\u[0-9a-f]{4,8}"
  359. rx_hex <- "\\\\x[0-9a-f]{2}|\\\\x\\{[0-9a-f]{1,6}\\}"
  360. rx_octal <- "\\\\[0][0-7]{1,3}"
  361. rx_escape <- paste(rx_unicode, rx_hex, rx_octal, sep = "|")
  362. if (grepl(rx_escape, x, ignore.case = TRUE)) {
  363. try({
  364. y <- stringi::stri_unescape_unicode(x)
  365. }, silent = TRUE)
  366. if (!is.na(y)) x <- y
  367. }
  368. # x <- gsub("\u201C|\u201D", '"', x)
  369. # x <- gsub("\u2018|\u2019", "'", x)
  370. x
  371. }
  372. toHTML <- function(...) {
  373. x <- paste(..., collapse = "")
  374. x <- gsub("\n", "\\\\n", x)
  375. x <- gsub("\t", "\\\\t", x)
  376. x <- gsub("\r", "\\\\r", x)
  377. HTML(x)
  378. }
  379. regexFn_choices <- list(
  380. "Choose a function" = "",
  381. base = c(
  382. "grep",
  383. "grepl",
  384. "sub", #<<
  385. "gsub", #<<
  386. "regexpr",
  387. "gregexpr",
  388. "regexec"
  389. ),
  390. stringr = c(
  391. "str_detect",
  392. "str_locate",
  393. "str_locate_all",
  394. "str_extract",
  395. "str_extract_all",
  396. "str_match",
  397. "str_match_all",
  398. "str_replace", #<<
  399. "str_replace_all", #<<
  400. "str_split"
  401. ),
  402. "rematch2" = c(
  403. "re_match",
  404. "re_match_all",
  405. "re_exec",
  406. "re_exec_all"
  407. )
  408. )
  409. regexFn_substitute <- c(
  410. paste0(c("", "g"), "sub"),
  411. paste0("str_replace", c("", "_all"))
  412. )
  413. get_pkg_namespace <- function(fn) {
  414. x <- names(purrr::keep(regexFn_choices, ~ (fn %in% .)))
  415. if (length(x) > 1) warning(fn, " matches multiple functions in regexFn_choices, please review.")
  416. x
  417. }
  418. #' Check if an updated version is available
  419. #'
  420. #' I included this because it can be difficult to tell if your RStudio Addins
  421. #' are up to date. I may add new features that you want but you won't hear about
  422. #' the updates. This function checks if an update is available, using GitHub
  423. #' tags. If an update is available, a modal dialog is shown when you start
  424. #' the regexplain gadget. This only happens once per R session, though, so feel
  425. #' free to ignore the message.
  426. #'
  427. #' @param gh_user GitHub user account
  428. #' @param gh_repo GitHub repo name
  429. #' @param this_version The currently installed version of the package
  430. #' @keywords internal
  431. check_version <- function(
  432. gh_user = "gadenbuie",
  433. gh_repo = "regexplain",
  434. this_version = packageVersion('regexplain')
  435. ) {
  436. ok_to_check <- getOption("regexplain.no.check.version", TRUE)
  437. if (!ok_to_check) return(NULL)
  438. if (!requireNamespace('jsonlite', quietly = TRUE)) return(NULL)
  439. get_json <- purrr::possibly(jsonlite::fromJSON, NULL)
  440. gh_tags <- get_json(
  441. paste0("https://api.github.com/repos/", gh_user, "/", gh_repo, "/git/refs/tags"),
  442. simplifyDataFrame = TRUE
  443. )
  444. if (!is.null(gh_tags)) {
  445. gh_tags$tag <- sub("refs/tags/", "", gh_tags$ref, fixed = TRUE)
  446. gh_tags$version <- sub("^v\\.?", "", gh_tags$tag)
  447. }
  448. if (!is.null(gh_tags) && any(gh_tags$version > this_version)) {
  449. max_version <- max(gh_tags$version)
  450. max_tag <- gh_tags$tag[gh_tags$version == max_version]
  451. options(regexplain.no.check.version = FALSE)
  452. return(
  453. list(
  454. version = max_version,
  455. link = paste("https://github.com", gh_user, gh_repo, "releases/tag", max_tag, sep = "/")
  456. )
  457. )
  458. } else return(NULL)
  459. }