🔍 An RStudio addin slash regex utility belt
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

329 lines
10.0KB

  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. # ---- UI ----
  13. ui <- miniPage(
  14. shiny::includeCSS(system.file("styles", "style.css", package = "regexplain")),
  15. shiny::includeCSS(system.file("styles", "gadget.css", package = "regexplain")),
  16. gadgetTitleBar(
  17. "regexplain",
  18. right = miniTitleBarButton("done", "Send Regex To Console", TRUE)
  19. ),
  20. miniTabstripPanel(
  21. selected = match.arg(start_page, c("Text", "Regex", "Output", "Help")),
  22. # --- UI - Tab - Text ----
  23. miniTabPanel(
  24. "Text", icon = icon('file-text-o'),
  25. miniContentPanel(
  26. fillCol(
  27. textAreaInputAlt('text',
  28. label = "Text to search or parse",
  29. value = paste(text, collapse = "\n"),
  30. resize = "both",
  31. width = "100%",
  32. height="90%",
  33. placeholder = "Paste, enter, or edit your sample text here.")
  34. )
  35. )
  36. ),
  37. # ---- UI - Tab - Regex ----
  38. miniTabPanel(
  39. "Regex", icon = icon('terminal'),
  40. miniContentPanel(
  41. fillCol(
  42. flex = c(1, 3),
  43. fillCol(
  44. flex = c(1, 1),
  45. textInputCode('pattern', 'Regex', width = "100%",
  46. placeholder = "Enter regex, single \\ okay"),
  47. checkboxGroupInput(
  48. 'regex_options',
  49. label = HTML(
  50. '<div style="font-size: 1.25rem;">',
  51. 'Option Groups: ',
  52. '<span style="color: #337ab7;">regexplain</span>,',
  53. '<span style="color: #5cb85c;">all</span>, ',
  54. '<span style="color: #f0ad4e;">base only</span>',
  55. '</div>'
  56. ),
  57. inline = TRUE,
  58. width = "90%",
  59. choiceValues = list(
  60. "text_break_lines",
  61. "ignore.case",
  62. "fixed",
  63. "perl",
  64. "useBytes"),
  65. choiceNames = list(
  66. HTML('<span style="color: #337ab7;">Break Lines</span>'),
  67. HTML('<span style="color: #5cb85c;">Ignore Case</span>'),
  68. HTML('<span style="color: #5cb85c;">Fixed/Literal</span>'),
  69. HTML('<span style="color: #f0ad4e;">Perl Style</span>'),
  70. HTML('<span style="color: #f0ad4e;">Use Bytes</span>')),
  71. selected = c('text_break_lines')
  72. )
  73. ),
  74. tags$div(
  75. class = "gadget-result",
  76. style = "overflow-y: scroll; height: 100%;",
  77. htmlOutput('result')
  78. )
  79. )
  80. )
  81. ),
  82. # ---- UI - Tab - Output ----
  83. miniTabPanel(
  84. "Output", icon = icon("table"),
  85. miniContentPanel(
  86. fillCol(
  87. flex = c(1, 3),
  88. inputPanel(
  89. tags$div(
  90. width = "100%;",
  91. selectInput('regexFn', label = 'Apply Function',
  92. choices = regexFn_choices),
  93. tags$span(class = "help-block",
  94. style = "font-size:1.25rem; margin-top:-10px; margin-bottom:0px; margin-left:4px;",
  95. "Adjust options in Regex tab")
  96. ),
  97. uiOutput("output_sub")
  98. ),
  99. # verbatimTextOutput('output_result', placeholder = TRUE)
  100. tags$pre(
  101. id = "output_result",
  102. class = "shiny-text-output",
  103. style = "overflow-y: scroll; height: 100%;"
  104. )
  105. )
  106. )
  107. ),
  108. # ---- UI - Tab - Help ----
  109. miniTabPanel(
  110. "Help", icon = icon("support"),
  111. help_ui("help")
  112. )
  113. )
  114. )
  115. # ---- Server ----
  116. server <- function(input, output, session) {
  117. # ---- Server - Global ----
  118. rtext <- reactive({
  119. x <- if ('text_break_lines' %in% input$regex_options) {
  120. strsplit(input$text, "\n")[[1]]
  121. } else input$text
  122. x
  123. })
  124. pattern <- reactive({
  125. sanitize_text_input(input$pattern)
  126. })
  127. alert_result <- function(msg, type = "danger") {
  128. msg <- gsub("\n", "<br>", msg)
  129. msg <- gsub("\t", "&nbsp;&nbsp;", msg)
  130. paste0("<pre class='alert alert-", type, "' ",
  131. "style='padding: 4px; margin-top: 1px; margin-bottom: 4px;'>",
  132. paste(msg, collapse = "<br>"),
  133. "</pre>")
  134. }
  135. # ---- Server - Tab - Regex ----
  136. output$result <- renderUI({
  137. if (is.null(rtext())) return(NULL)
  138. if (pattern() == "") {
  139. return(toHTML(paste('<p class="results">', escape_html(rtext()), "</p>", collapse = "")))
  140. }
  141. res <- NULL
  142. error_message <- NULL
  143. warning_message <- NULL
  144. tryCatch({
  145. res <- paste(
  146. view_regex(
  147. rtext(),
  148. pattern(),
  149. ignore.case = 'ignore.case' %in% input$regex_options,
  150. perl = 'perl' %in% input$regex_options,
  151. fixed = 'fixed' %in% input$regex_options,
  152. useBytes = 'useBytes' %in% input$regex_options,
  153. # invert = 'invert' %in% input$regex_options,
  154. render = FALSE,
  155. escape = TRUE,
  156. exact = FALSE),
  157. collapse = ""
  158. )
  159. },
  160. error = function(e) {
  161. error_message <<- alert_result(e$message, "danger")
  162. },
  163. warning = function(w) {
  164. warning_message <<- alert_result(w$message, "warning")
  165. })
  166. if (is.null(res)) res <- toHTML(
  167. paste('<p class="results">', escape_html(rtext()), "</p>", collapse = "")
  168. )
  169. toHTML(paste(error_message, warning_message, res))
  170. })
  171. # ---- Server - Tab - Output ----
  172. regexFn_replacement_val <- NULL
  173. output$output_sub <- renderUI({
  174. req(input$regexFn)
  175. if (!input$regexFn %in% regexFn_substitute) return(NULL)
  176. textInputCode('regexFn_replacement', 'Subsitution',
  177. value = regexFn_replacement_val,
  178. placeholder = "Replacement Text")
  179. })
  180. replacement <- reactive({
  181. req(input$regexFn)
  182. if (!input$regexFn %in% regexFn_substitute) {
  183. NULL
  184. } else {
  185. regexFn_replacement_val <<- input$regexFn_replacement
  186. sanitize_text_input(input$regexFn_replacement)
  187. }
  188. })
  189. output$output_result <- renderPrint({
  190. req(input$regexFn)
  191. regexPkg <- get_pkg_namespace(input$regexFn)
  192. regexFn <- getFromNamespace(input$regexFn, regexPkg)
  193. req_sub_arg <- input$regexFn %in% regexFn_substitute
  194. x <- if (regexPkg == "base") {
  195. if (req_sub_arg) {
  196. req(replacement())
  197. regexFn(pattern(), replacement(), rtext(),
  198. ignore.case = 'ignore.case' %in% input$regex_options,
  199. perl = 'perl' %in% input$regex_options,
  200. fixed = 'fixed' %in% input$regex_options,
  201. useBytes = 'useBytes' %in% input$regex_options)
  202. } else {
  203. regexFn(pattern(), rtext(),
  204. ignore.case = 'ignore.case' %in% input$regex_options,
  205. perl = 'perl' %in% input$regex_options,
  206. fixed = 'fixed' %in% input$regex_options,
  207. useBytes = 'useBytes' %in% input$regex_options)
  208. }
  209. } else if (regexPkg == "stringr") {
  210. if (req_sub_arg) {
  211. req(replacement())
  212. regexFn(
  213. rtext(),
  214. stringr::regex(
  215. pattern(),
  216. ignore_case = 'ignore.case' %in% input$regex_options,
  217. literal = 'fixed' %in% input$regex_options
  218. ),
  219. replacement()
  220. )
  221. } else {
  222. regexFn(
  223. rtext(),
  224. stringr::regex(
  225. pattern(),
  226. ignore_case = 'ignore.case' %in% input$regex_options,
  227. literal = 'fixed' %in% input$regex_options
  228. )
  229. )
  230. }
  231. } else {
  232. "Um. Not sure how I got here."
  233. }
  234. print(x)
  235. })
  236. # ---- Server - Tab - Help ----
  237. help_text <- callModule(help_server, "help")
  238. # ---- Server - Tab - Exit ----
  239. observeEvent(input$done, {
  240. # browser()
  241. if (pattern() != "") {
  242. pattern <- paste0('pattern <- "', escape_backslash(pattern()), '"')
  243. rstudioapi::sendToConsole(pattern, FALSE)
  244. }
  245. stopApp()
  246. })
  247. observeEvent(input$cancel, {
  248. stopApp()
  249. })
  250. }
  251. viewer <- shiny::paneViewer(minHeight = 1000)
  252. runGadget(ui, server, viewer = viewer)
  253. }
  254. # ---- Gadget Helper Functions and Variables ----
  255. sanitize_text_input <- function(x) {
  256. if (is.null(x) || !nchar(x)) return(x)
  257. if (grepl("\\u|\\x|\\N|\\a|\\o", x)) {
  258. try({
  259. y <- stringi::stri_unescape_unicode(x)
  260. }, silent = TRUE)
  261. if (!is.na(y)) x <- y
  262. }
  263. # x <- gsub("\u201C|\u201D", '"', x)
  264. # x <- gsub("\u2018|\u2019", "'", x)
  265. x
  266. }
  267. toHTML <- function(...) {
  268. x <- paste(..., collapse = "")
  269. x <- gsub("\n", "\\\\n", x)
  270. x <- gsub("\t", "\\\\t", x)
  271. x <- gsub("\r", "\\\\r", x)
  272. HTML(x)
  273. }
  274. regexFn_choices <- list(
  275. "Choose a function" = "",
  276. base = c(
  277. "grep",
  278. "grepl",
  279. "sub", #<<
  280. "gsub", #<<
  281. "regexpr",
  282. "gregexpr",
  283. "regexec"
  284. ),
  285. stringr = c(
  286. "str_detect",
  287. "str_locate",
  288. "str_locate_all",
  289. "str_extract",
  290. "str_extract_all",
  291. "str_match",
  292. "str_match_all",
  293. "str_replace", #<<
  294. "str_replace_all", #<<
  295. "str_split"
  296. )
  297. )
  298. regexFn_substitute <- c(
  299. paste0(c("", "g"), "sub"),
  300. paste0("str_replace", c("", "_all"))
  301. )
  302. get_pkg_namespace <- function(fn) {
  303. x <- names(purrr::keep(regexFn_choices, ~ (fn %in% .)))
  304. if (length(x) > 1) warning(fn, " matches multiple functions in regexFn_choices, please review.")
  305. x
  306. }