├── .gitignore ├── Default.sublime-keymap ├── LICENSE ├── Main.sublime-menu ├── README.md ├── RegexExplainTip.py ├── RegexExplainTip.sublime-settings ├── css └── default.css ├── messages.json ├── messages └── 0.9.2.txt └── screenshots └── 1.PNG /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["shift+super+alt+r"], "command": "regexexplaintip" } 3 | ] 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Tomasz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [{ 2 | "caption": "Preferences", 3 | "mnemonic": "n", 4 | "id": "preferences", 5 | "children": [{ 6 | "caption": "Package Settings", 7 | "mnemonic": "P", 8 | "id": "package-settings", 9 | "children": [{ 10 | "caption": "RegexExplainTip", 11 | "children": [{ 12 | "command": "open_file", "args": { 13 | "file": "${packages}/RegexExplainTip/RegexExplainTip.sublime-settings" 14 | }, 15 | "caption": "Settings – Default" 16 | }, { 17 | "command": "open_file", "args": { 18 | "file": "${packages}/User/RegexExplainTip.sublime-settings" 19 | }, 20 | "caption": "Settings – User" 21 | }] 22 | }] 23 | }] 24 | }] 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SublimeRegexExplainTip 2 | SublimeText 3 plugin for displaying regular expression explanations 3 | 4 | ## What it looks like 5 | 6 | ![Screenshot](https://raw.githubusercontent.com/rubikonx9/SublimeRegexExplainTip/master/screenshots/1.PNG) 7 | 8 | ## Why? 9 | 10 | As some regular expressions are *write-only* code, it's sometimes useful to obtain a description of what a particular regex actually does. 11 | Having it available in a text editor can be an asset. 12 | 13 | ## Installation 14 | 15 | Install via [Package Control](https://packagecontrol.io/). 16 | Alternatively, clone this repository to `Packages` directory. 17 | 18 | ## How to use 19 | 20 | Press Shift+Super+Alt+R to display the explanation of selected text. 21 | Currently, a region must be explicitly selected. 22 | 23 | ## Customization 24 | 25 | It is possible to use custom CSS files. You can define the CSS file in settings file (navigate to `Preferences` -> `Package Settings` -> `RegexExplainTip` -> `Settings - User`) under `css_file` key: 26 | 27 | ``` 28 | { 29 | "css_file": "Packages/User/my-custom.css" 30 | } 31 | ``` 32 | 33 | You should locate the file somewhere under `Packages` (`Preferences` -> `Browse Packages...`) directory. 34 | 35 | ## Dependencies 36 | 37 | The plugin uses [YAPE::Regex::Explain](http://search.cpan.org/dist/YAPE-Regex-Explain/Explain.pm) to obtain the regex explanation. 38 | Therefore, Perl with `YAPE::Regex::Explain` module installed is required. 39 | 40 | ## Caveats 41 | 42 | As this plugin relies on external Perl installation and modules, one must have correct environment setup. 43 | This includes proper value if Perl's `@INC` variable, which allows Perl to find required modules. 44 | This directory depends on you environment settings, OS, CPAN configuration and so on. 45 | 46 | For example, you might need to define the paths in `PERL5LIB` variable: 47 | 48 | `export PERL5LIB=/some/perl/installation/directory/lib`. 49 | 50 | Alternatively, one may add the following line to Perl code declared in `get_explanation` method: 51 | 52 | `use lib 'c:/StrawberryPERL/perl/site/lib';` 53 | -------------------------------------------------------------------------------- /RegexExplainTip.py: -------------------------------------------------------------------------------- 1 | """ 2 | RegexExplainTip plugin for Sublime Text 3. 3 | """ 4 | 5 | import errno 6 | import os 7 | import re 8 | import subprocess 9 | 10 | import sublime 11 | import sublime_plugin 12 | 13 | 14 | class RegexexplaintipCommand(sublime_plugin.TextCommand): 15 | """ 16 | The only class for this plugin. 17 | """ 18 | REGEX_SUBSTITUTIONS = [ 19 | (r"&", "&"), 20 | (r"<", "<"), 21 | (r">", ">"), 22 | (r"\s", " "), 23 | (r"(\\[^&]|\\ )", """ 24 | \\g<1> 25 | 26 | """), 27 | (r"([\^\.\$\|])", """ 28 | \\g<1> 29 | 30 | """), 31 | (r"([\[\]\(\)])", """ 32 | \\g<1> 33 | 34 | """), 35 | (r"([\+\*\?]|\{\d+(,\d+)?\})", """ 36 | \\g<1> 37 | 38 | """) 39 | ] 40 | 41 | EXPLANATION_SUBSTITUTIONS = [ 42 | # Magic: http://stackoverflow.com/questions/23205606/regex-to-remove-comma-between-double-quotes-notepad 43 | (r" (?=[^']*'[^']*(?:'[^']*'[^']*)*$)", " "), 44 | (r"&", "&"), 45 | (r"<", "<"), 46 | (r">", ">"), 47 | (r"(? 48 | \\g<1> 49 | 50 | """), 51 | (r"\\\'", "'"), 52 | (r"(\\.)", """ 53 | \\g<1> 54 | 55 | """), 56 | (r"((^OR$)|\^|\$|\.|\\\d+)", """ 57 | \\g<1> 58 | 59 | """), 60 | (r"((between \d+ and )?\d+" + 61 | r" (or more )?times|optional)", """ 62 | \\g<1> 63 | 64 | """) 65 | ] 66 | 67 | def __init__(self, view): 68 | """ 69 | Reads CSS content from file. 70 | """ 71 | super(RegexexplaintipCommand, self).__init__(view) 72 | 73 | self.was_actual_rule = False 74 | 75 | self.load_css() 76 | 77 | def observe_settings(self): 78 | """ 79 | Adds an observer to settings file, which reloads the CSS file anytime the settings are changed. 80 | """ 81 | settings = sublime.load_settings("RegexExplainTip.sublime-settings") 82 | 83 | settings.clear_on_change("RegexExplainTip") 84 | settings.add_on_change("RegexExplainTip", self.load_css) 85 | 86 | def load_css(self): 87 | """ 88 | Loads CSS from file and stores it as object property. 89 | """ 90 | settings = sublime.load_settings("RegexExplainTip.sublime-settings") 91 | css_file = settings.get("css_file") 92 | 93 | self.observe_settings() 94 | 95 | try: 96 | self.css = sublime.load_resource(css_file).replace("\r", "") 97 | except IOError: 98 | self.css = "" 99 | print("RegexExplainTip:\nSpecified file: '%s' does not seem to exists." % css_file) 100 | 101 | def get_selected_text(self): 102 | """ 103 | Obtains the text under cursor (current selection). 104 | """ 105 | region = self.view.sel()[0] 106 | selected_text = self.view.substr(region) 107 | 108 | return selected_text 109 | 110 | def get_explanation(self, regex): 111 | """ 112 | Calls Perl to get the textual explanation of the regex. 113 | """ 114 | if regex == "": 115 | return "" 116 | 117 | regex = re.sub(r"\'", r"\\\'", regex) 118 | regex = re.sub(r"\\\[", r"\\\\\[", regex) 119 | regex = re.sub(r"\\\]", r"\\\\\]", regex) 120 | regex = re.sub(r"\\\\", r"\\\\\\\\", regex) 121 | regex = regex.encode('raw_unicode_escape').decode() 122 | 123 | command = """ 124 | use utf8; 125 | use YAPE::Regex::Explain; 126 | 127 | my $regex_string = '%s'; 128 | 129 | $regex_string =~ s|\\\\u([a-zA-Z0-9]{4})|UNICODE_ESCAPE_SEQUENCE$1|g; 130 | 131 | my $explanation = YAPE::Regex::Explain->new($regex_string)->explain('regex'); 132 | 133 | print $explanation; 134 | """ % re.sub("'", "\\'", re.sub("\\\\", "\\\\\\\\", regex)) 135 | 136 | startupinfo = None 137 | 138 | if os.name == "nt": 139 | startupinfo = subprocess.STARTUPINFO() 140 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 141 | 142 | try: 143 | perl_call = subprocess.Popen( 144 | [ "perl", "-e", command ], 145 | stdout = subprocess.PIPE, 146 | stderr = subprocess.PIPE, 147 | startupinfo = startupinfo 148 | ) 149 | except OSError as exception: 150 | if exception.errno == errno.ENOENT: 151 | sublime.error_message("RegexExplainTip:\nIt seems that you have no Perl installed.\nVerify the installation and $PATH.") 152 | 153 | print("RegexExplainTip: exception: '%s'." % exception.strerror) 154 | 155 | return "" 156 | 157 | out, err = perl_call.communicate() 158 | 159 | if err: 160 | message = err.decode().replace("\r", "").strip() 161 | 162 | if re.match(r"Can\'t locate YAPE\/Regex\/Explain\.pm", message): 163 | sublime.error_message("RegexExplainTip:\nIt seems that you have no `YAPE::Regex::Explain` module installed.\nVerify the installation.") 164 | 165 | print("RegexExplainTip: error calling Perl's `YAPE::Regex::Explain`.\nExit code: %d.\nSTDERR: '%s'.\nExpression being parsed: '%s'." % ( 166 | perl_call.returncode, 167 | message, 168 | regex 169 | )) 170 | 171 | return out.decode().replace("\r", "") 172 | 173 | def partition_by_empty_line(self, lines): 174 | """ 175 | Splits an array into array of arrays, treating an empty string (\n only) as a delimiter. 176 | """ 177 | partitioned = [] 178 | current_partition = [] 179 | 180 | for line in lines: 181 | if re.match("^$", line): 182 | partitioned.append(current_partition) 183 | current_partition = [] 184 | else: 185 | current_partition.append(line) 186 | 187 | return partitioned 188 | 189 | def unescape_unicode(self, text): 190 | text = re.sub("UNICODE_ESCAPE_SEQUENCE([a-zA-Z0-9]{4})", "\\\\u\g<1>", text) 191 | text = text.encode().decode("raw_unicode_escape") 192 | 193 | return text 194 | 195 | def extract_regex_and_explanation(self, rules): 196 | """ 197 | Creates a list of objects (regex - explanation pairs) from lists of lines. 198 | """ 199 | result = [] 200 | 201 | for rule in rules: 202 | regex_parts = [] 203 | explanation_parts = [] 204 | 205 | for line in rule: 206 | regex, explanation = self.split_by_middle_hash(line) 207 | 208 | regex_parts.append(regex) 209 | explanation_parts.append(explanation) 210 | 211 | result.append({ 212 | "regex" : self.unescape_unicode("".join(regex_parts)), 213 | "explanation" : self.unescape_unicode("".join(explanation_parts)) 214 | }) 215 | 216 | return result 217 | 218 | def convert_lines_to_html(self, lines): 219 | """ 220 | Converts the lines to a full explanation message. 221 | """ 222 | lines = lines[5:-3] # Remove heading & tail (not very useful info on regex flags) 223 | 224 | rules = self.extract_regex_and_explanation( 225 | self.partition_by_empty_line(lines) 226 | ) 227 | 228 | return "".join( 229 | map(self.convert_rule_to_html, rules) 230 | ) 231 | 232 | def convert_rule_to_html(self, rule): 233 | """ 234 | Converts a single rule to an HTML structure. 235 | """ 236 | regex = rule["regex"] 237 | explanation = rule["explanation"] 238 | 239 | if regex.endswith("\\"): 240 | # Restore required trailing space 241 | regex = regex + " " 242 | 243 | separator = """ 244 |
245 |
246 |
247 |
248 | """ if self.was_actual_rule else "" 249 | 250 | # Check for start of capture group 251 | group_start_match = re.match(r"(group and capture to \\(\d+)|" + 252 | r"group, but do not capture|" + 253 | r"look ahead to see if there is(?: not)?)(.*)", explanation) 254 | 255 | if group_start_match: 256 | self.was_actual_rule = False 257 | 258 | group_type = group_start_match.group(1) 259 | capture_group_number = group_start_match.group(2) if group_start_match.group(2) else "" 260 | additional_info = re.sub(r":$", "", group_start_match.group(3).strip()) 261 | 262 | if group_type.startswith("group and capture"): 263 | group_type = "Capture group" 264 | elif group_type.startswith("group, but"): 265 | group_type = "Non-capture group" 266 | elif group_type.find("not") != -1: 267 | group_type = "Negative look-ahead" 268 | else: 269 | group_type = "Look-ahead" 270 | 271 | return """ 272 |
273 |
274 |
275 | %s 276 | 277 | %s 278 | 279 | %s 280 |
281 | """ % ( 282 | capture_group_number, 283 | group_type, 284 | capture_group_number, 285 | self.replace_by_patterns(additional_info, self.EXPLANATION_SUBSTITUTIONS) 286 | ) 287 | 288 | # Check for end of capture group 289 | group_end_match = re.match(r"end of (?:\\\d+|grouping|look-ahead)\s?(.*)", explanation) 290 | 291 | if group_end_match: 292 | self.was_actual_rule = False 293 | 294 | additional_info = group_end_match.group(1) 295 | 296 | footer = """ 297 |
298 | %s 299 |
300 | """ % self.replace_by_patterns(additional_info, self.EXPLANATION_SUBSTITUTIONS) 301 | 302 | return """ 303 | %s 304 | %s 305 |
306 |
307 | """ % ( 308 | separator if additional_info else "", 309 | footer if additional_info else "" 310 | ) 311 | 312 | self.was_actual_rule = True 313 | 314 | # Regular case 315 | return """ 316 | %s 317 |
318 |
319 | 320 | %s 321 | 322 |
323 |
324 | %s 325 |
326 |
327 | """ % ( 328 | separator, 329 | self.replace_by_patterns(regex, self.REGEX_SUBSTITUTIONS), 330 | self.replace_by_patterns(explanation, self.EXPLANATION_SUBSTITUTIONS) 331 | ) 332 | 333 | def split_by_middle_hash(self, line): 334 | """ 335 | Splits the line by middle occurence of hash (#). 336 | Returns tuple of Nones if there are not hashes at all (empty line). 337 | There is always an odd number of hashes (N in the regex, N in explanation, and one in the middle --> 2N+1). 338 | """ 339 | hashes_count = line.count("#") 340 | 341 | if not hashes_count: 342 | return (None, None) 343 | 344 | regex = None 345 | explanation = None 346 | i = 0 347 | parts = re.finditer(r"#", line) 348 | 349 | for part in parts: 350 | i += 1 351 | 352 | if i >= hashes_count / 2: # Just reached the middle one 353 | regex = line[ : part.start() ] 354 | explanation = line[ part.end() : ] 355 | 356 | regex = re.sub("(? 381 | 384 |
385 |
386 | %s 387 | 388 | """ % ( 389 | self.css, 390 | self.convert_lines_to_html(explanation.split("\n")) 391 | ) 392 | 393 | def run(self, _): 394 | """ 395 | Executes the plugin. 396 | """ 397 | self.was_actual_rule = False 398 | 399 | message = self.build_html( 400 | self.get_explanation( 401 | self.get_selected_text() 402 | ) 403 | ) 404 | 405 | if message: 406 | self.view.show_popup(message, 407 | max_width = 1200, 408 | max_height = 600) 409 | -------------------------------------------------------------------------------- /RegexExplainTip.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "css_file": "Packages/RegexExplainTip/css/default.css" 3 | } 4 | -------------------------------------------------------------------------------- /css/default.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: #232628; 3 | color: white; 4 | } 5 | 6 | body { 7 | font-size: 15px; 8 | } 9 | 10 | div.expander { 11 | padding-left: 300px; 12 | } 13 | 14 | div.rule { 15 | margin: 10px; 16 | } 17 | 18 | div.regex { 19 | color: white; 20 | } 21 | 22 | div.explanation { 23 | margin-left: 120px; 24 | margin-top: 10px; 25 | color: #BBBB; 26 | } 27 | 28 | div.separator-outer { 29 | background-color: #333; 30 | } 31 | 32 | div.separator-inner { 33 | margin-top: 11px; 34 | background-color: #232628; 35 | } 36 | 37 | span.literal { 38 | color: #A21715; 39 | } 40 | 41 | span.bracket { 42 | color: #55C; 43 | } 44 | 45 | span.quantifier { 46 | color: #5C58; 47 | } 48 | 49 | span.meta { 50 | color: #CC5; 51 | } 52 | 53 | div.group-outer { 54 | background-color: #CCC; 55 | margin-top: 20px; 56 | margin-bottom: 10px; 57 | } 58 | 59 | div.group-inner { 60 | margin-top: 12px; 61 | margin-left: 2px; 62 | margin-bottom: 2px; 63 | margin-right: 2px; 64 | 65 | padding-top: 4px; 66 | padding-left: 6px; 67 | padding-bottom: 4px; 68 | padding-right: 4px; 69 | 70 | background-color: #232628; 71 | } 72 | 73 | div.group-info-header { 74 | color: #777; 75 | font-size: 13px; 76 | margin-bottom: 10px; 77 | } 78 | 79 | div.group-info-footer { 80 | color: #777; 81 | font-size: 13px; 82 | margin-top: 10px; 83 | margin-bottom: 10px; 84 | } 85 | 86 | div.level-1 { 87 | background-color: #CCC9; 88 | } 89 | 90 | div.level-2 { 91 | background-color: #C559; 92 | } 93 | 94 | div.level-3 { 95 | background-color: #5C59; 96 | } 97 | 98 | div.level-4 { 99 | background-color: #55CA; 100 | } 101 | 102 | div.level-5 { 103 | background-color: #5CC9; 104 | } 105 | 106 | div.level-6 { 107 | background-color: #C5C9; 108 | } 109 | 110 | div.level-7 { 111 | background-color: #CC59; 112 | } 113 | 114 | div.level-8 { 115 | background-color: #CCC9; 116 | } 117 | 118 | div.level-9 { 119 | background-color: #C559; 120 | } 121 | 122 | div.level-10 { 123 | background-color: #5C59; 124 | } 125 | 126 | div.level-11 { 127 | background-color: #55CA; 128 | } 129 | 130 | div.level-12 { 131 | background-color: #5CC9; 132 | } 133 | 134 | div.level-13 { 135 | background-color: #C5C9; 136 | } 137 | 138 | div.level-14 { 139 | background-color: #CC59; 140 | } 141 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.9.2": "messages/0.9.2.txt" 3 | } 4 | -------------------------------------------------------------------------------- /messages/0.9.2.txt: -------------------------------------------------------------------------------- 1 | RegexExplainTip: 0.9.2 update 2 | 3 | It is now possible to use custom CSS files. 4 | Please refer to Readme on https://github.com/rubikonx9/SublimeRegexExplainTip for details. 5 | -------------------------------------------------------------------------------- /screenshots/1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubikonx9/SublimeRegexExplainTip/c60a1eaa0a93b1894b062734f1daf77e24eb5966/screenshots/1.PNG --------------------------------------------------------------------------------