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

353 lines
10KB

  1. #' regexplain_addin
  2. #'
  3. #' @keywords internal
  4. regexplain_addin <- function() {
  5. # Get the document context.
  6. context <- rstudioapi::getActiveDocumentContext()
  7. # Get context text
  8. ctx_text <- context$selection[[1]]$text
  9. # If it is one line and evaluates to something, use that
  10. # Otherwise treat as text
  11. obj <- tryCatch({
  12. if (grepl("\n", ctx_text)) {
  13. ctx_text[1:min(length(ctx_text), 100)]
  14. } else {
  15. x <- eval(parse(text = ctx_text))
  16. x <- as.character(x)
  17. if (length(x) == 1 && grepl("\n", x))
  18. x <- strsplit(x, "\n")[[1]]
  19. if (length(x) > 10) {
  20. message(ctx_text, " gave ", length(x), " lines, limiting to first 10 unique lines.")
  21. x <- unique(x)
  22. x[1:min(length(x), 10)]
  23. } else x
  24. }
  25. },
  26. error = function(e) {as.character(ctx_text[1:min(length(ctx_text), 100)])})
  27. regex_gadget(if (length(obj) && obj != "") obj)
  28. }
  29. #' regexplain gadget
  30. #'
  31. #' @import miniUI
  32. #' @import shiny
  33. #' @export
  34. regex_gadget <- function(text = NULL) {
  35. stopifnot(requireNamespace("miniUI"), requireNamespace("shiny"))
  36. ui <- miniPage(
  37. shiny::includeCSS(system.file("style.css", package = "regexplain")),
  38. shiny::includeCSS(system.file("gadget.css", package = "regexplain")),
  39. gadgetTitleBar(
  40. "regexplain",
  41. right = miniTitleBarButton("done", "Send Regex To Console", TRUE)
  42. ),
  43. miniTabstripPanel(
  44. selected = if (is.null(text)) "Text" else "Regex",
  45. miniTabPanel(
  46. "Text", icon = icon('file-text-o'),
  47. miniContentPanel(
  48. fillCol(
  49. textAreaInputAlt('text',
  50. label = "Text to search or parse",
  51. value = paste(text, collapse = "\n"),
  52. resize = "both",
  53. width = "100%",
  54. height="90%",
  55. placeholder = "Paste, enter, or edit your sample text here.")
  56. )
  57. )
  58. ),
  59. miniTabPanel(
  60. "Regex", icon = icon('terminal'),
  61. miniContentPanel(
  62. fillCol(
  63. flex = c(1, 3),
  64. fillCol(
  65. flex = c(1, 1),
  66. textInputCode('pattern', 'Regex', width = "100%",
  67. placeholder = "Enter regex, single \\ okay"),
  68. checkboxGroupInput(
  69. 'regex_options',
  70. label = "",
  71. inline = TRUE,
  72. width = "90%",
  73. choices = c("Break Lines" = "text_break_lines",
  74. "Ignore Case" = "ignore.case",
  75. "Perl Style" = "perl",
  76. "Fixed" = "fixed",
  77. "Use Bytes" = "useBytes"
  78. # , "Invert" = "invert"
  79. ),
  80. selected = c('text_break_lines')
  81. )
  82. ),
  83. tags$div(
  84. class = "gadget-result",
  85. style = "overflow-y: scroll; height: 100%;",
  86. htmlOutput('result')
  87. )
  88. )
  89. )
  90. ),
  91. miniTabPanel(
  92. "Output", icon = icon("table"),
  93. miniContentPanel(
  94. fillCol(
  95. flex = c(1, 3),
  96. inputPanel(
  97. width = "100%;",
  98. selectInput('regexFn', label = 'Apply Function',
  99. choices = regexFn_choices)
  100. ),
  101. # verbatimTextOutput('output_result', placeholder = TRUE)
  102. tags$pre(
  103. id = "output_result",
  104. class = "shiny-text-output",
  105. style = "overflow-y: scroll; height: 100%;"
  106. )
  107. )
  108. )
  109. ),
  110. miniTabPanel(
  111. "Help", icon = icon("support"),
  112. miniContentPanel(
  113. fillRow(
  114. flex = c(1, 4),
  115. tagList(
  116. # selectInput("help_category", "Category", c("", unique(cheatsheet$category))),
  117. # uiOutput("help_group"),
  118. tags$ul(
  119. id = "help-sidebar",
  120. tags$li("Character Classes", class = "header"),
  121. tags$ul(
  122. class = "subgroup",
  123. tags$li(actionLink("help_cat_character_classes_regular", "Regular")),
  124. tags$li(actionLink("help_cat_character_classes_prebuilt", "Pre-Built"))
  125. ),
  126. tags$li(actionLink("help_cat_anchors", "Anchors")),
  127. tags$li("Escaped Characters", class = "header"),
  128. tags$ul(
  129. class = "subgroup",
  130. tags$li(actionLink("help_cat_escaped_general", "General")),
  131. tags$li(actionLink("help_cat_escaped_hex", "Hex")),
  132. tags$li(actionLink("help_cat_escaped_control", "Control Characters"))
  133. ),
  134. tags$li(actionLink("help_cat_groups", "Groups")),
  135. tags$li(actionLink("help_cat_quantifiers", "Quantifiers"))
  136. )
  137. ),
  138. tags$div(
  139. style = "width: 100%; padding-left: 10px;",
  140. uiOutput('help_text_selected')
  141. )
  142. )
  143. )
  144. )
  145. )
  146. )
  147. server <- function(input, output, session) {
  148. rtext <- reactive({
  149. x <- if ('text_break_lines' %in% input$regex_options) {
  150. strsplit(input$text, "\n")[[1]]
  151. } else input$text
  152. x
  153. })
  154. output$result <- renderUI({
  155. if (is.null(rtext())) return(NULL)
  156. if (input$pattern == "") {
  157. return(toHTML(paste('<p class="results">', escape_html(rtext()), "</p>", collapse = "")))
  158. }
  159. tryCatch({
  160. toHTML(
  161. paste(
  162. view_regex(
  163. rtext(),
  164. sanitize_text_input(input$pattern),
  165. ignore.case = 'ignore.case' %in% input$regex_options,
  166. perl = 'perl' %in% input$regex_options,
  167. fixed = 'fixed' %in% input$regex_options,
  168. useBytes = 'useBytes' %in% input$regex_options,
  169. # invert = 'invert' %in% input$regex_options,
  170. render = FALSE,
  171. escape = TRUE),
  172. collapse = ""
  173. )
  174. )
  175. },
  176. error = function(e) {
  177. toHTML(paste0("<pre class='alert alert-danger' style='padding: 4px; margin-top: 1px; margin-bottom: 4px;'>", paste(e$message, collapse = "<br>"), "</pre>"),
  178. paste('<p class="results">', escape_html(rtext()), "</p>", collapse = ""))
  179. })
  180. })
  181. output$output_result <- renderPrint({
  182. req(input$regexFn)
  183. regexPkg <- get_pkg_namespace(input$regexFn)
  184. regexFn <- getFromNamespace(input$regexFn, regexPkg)
  185. x <- if (regexPkg == "base") {
  186. regexFn(input$pattern, rtext())
  187. } else if (regexPkg == "stringr") {
  188. regexFn(rtext(), input$pattern)
  189. } else {
  190. "Um. Not sure how I got here."
  191. }
  192. # if (inherits(x, 'logical') || inherits(x, 'character')) {
  193. # if (length(x) < 25) print(x) else print(head(x, 25))
  194. # } else if (inherits(x, 'matrix') | inherits(x, "data.frame")) {
  195. # if (nrow(x) < 15) { print(x)
  196. # } else glimpse(x)
  197. # } else {
  198. # str(x, max.level = 3)
  199. # }
  200. print(x)
  201. })
  202. # output$help_group <- renderUI({
  203. # req(input$help_category)
  204. # groups <- unique(cheatsheet[cheatsheet$category == input$help_category, ]$group)
  205. # if (is.na(groups[1])) {
  206. # NULL
  207. # } else {
  208. # selectInput("help_group", "Group", groups)
  209. # }
  210. # })
  211. # ---- Help Section ---- #
  212. help_text <- reactiveVal("<p>Select a category from the left sidebar.</p>")
  213. output$help_text_selected <- renderUI({
  214. HTML(help_text())
  215. })
  216. make_html_table <- function(x) {
  217. select(x, .data$regexp, .data$text) %>%
  218. knitr::kable(
  219. col.names = c("Regexp", "Text"),
  220. escape = FALSE,
  221. format = "html")
  222. }
  223. observeEvent(input$help_cat_character_classes_regular, {
  224. cheatsheet %>%
  225. filter(category == "character classes", group == "regular") %>%
  226. make_html_table %>%
  227. help_text
  228. })
  229. observeEvent(input$help_cat_character_classes_prebuilt, {
  230. cheatsheet %>%
  231. filter(category == "character classes", group == "pre-built") %>%
  232. make_html_table %>%
  233. help_text
  234. })
  235. observeEvent(input$help_cat_anchors, {
  236. cheatsheet %>%
  237. filter(category == "anchors") %>%
  238. make_html_table %>%
  239. help_text
  240. })
  241. observeEvent(input$help_cat_escaped_general, {
  242. cheatsheet %>%
  243. filter(category == "escaped characters", group == "general") %>%
  244. make_html_table %>%
  245. help_text
  246. })
  247. observeEvent(input$help_cat_escaped_hex, {
  248. cheatsheet %>%
  249. filter(category == "escaped characters", group == "hex") %>%
  250. make_html_table %>%
  251. help_text
  252. })
  253. observeEvent(input$help_cat_escaped_control, {
  254. cheatsheet %>%
  255. filter(category == "escaped characters", group == "control characters") %>%
  256. make_html_table %>%
  257. help_text
  258. })
  259. observeEvent(input$help_cat_groups, {
  260. cheatsheet %>%
  261. filter(category == "groups") %>%
  262. make_html_table %>%
  263. help_text
  264. })
  265. observeEvent(input$help_cat_quantifiers, {
  266. cheatsheet %>%
  267. filter(category == "quantifiers") %>%
  268. make_html_table %>%
  269. help_text
  270. })
  271. observeEvent(input$done, {
  272. # browser()
  273. if (input$pattern != "") {
  274. pattern <- paste0('regex <- "', escape_backslash(sanitize_text_input(input$pattern)), '"')
  275. rstudioapi::sendToConsole(pattern, FALSE)
  276. }
  277. stopApp()
  278. })
  279. observeEvent(input$cancel, {
  280. stopApp()
  281. })
  282. }
  283. viewer <- shiny::paneViewer(700)
  284. runGadget(ui, server, viewer = viewer)
  285. }
  286. sanitize_text_input <- function(x) {
  287. x <- gsub("(“|”)", '"', x)
  288. x <- gsub("‘|’", "'", x)
  289. x
  290. }
  291. toHTML <- function(...) {
  292. x <- paste(..., collapse = "")
  293. x <- gsub("\n", "\\\\n", x)
  294. x <- gsub("\t", "\\\\t", x)
  295. x <- gsub("\r", "\\\\r", x)
  296. HTML(x)
  297. }
  298. regexFn_choices <- list(
  299. "Choose a function" = "",
  300. base = c(
  301. "grep",
  302. "grepl",
  303. "regexpr",
  304. "gregexpr",
  305. "regexec"
  306. ),
  307. stringr = c(
  308. "str_detect",
  309. "str_locate",
  310. "str_locate_all",
  311. "str_extract",
  312. "str_extract_all",
  313. "str_match",
  314. "str_match_all",
  315. "str_split"
  316. )
  317. )
  318. get_pkg_namespace <- function(fn) {
  319. x <- names(purrr::keep(regexFn_choices, ~ (fn %in% .)))
  320. if (length(x) > 1) warning(fn, " matches multiple functions in regexFn_choices, please review.")
  321. x
  322. }