🔍 An RStudio addin slash regex utility belt
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

490 líneas
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. }