├── .gitignore ├── CHANGELOG.txt ├── Default.sublime-commands ├── LICENSE.txt ├── README.md ├── customfilters.py ├── filterpipes.py └── filters.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | Version: 1.1.0 [Apr 27, 2015] 2 | ----------------------------- 3 | Allow non-zero returns from external processes. 4 | FilterPipesProcessCommand now has an "expected_returns" parameter 5 | which allows you to specify a list of "success" return codes. It 6 | defaults to just [0], which is typical. Setting this to an empty 7 | set or false will disable checking the return code, and assume 8 | all returns are successful. 9 | 10 | Allow arbitrary options to be passed to Popen. 11 | Added parameter 'subprocess_args' to FilterPipesProcessCommand, 12 | which allows you to specify any additional parameters to pass to 13 | your subprocess call, such as a specific working directory or environment. 14 | 15 | 16 | Version: 1.0.1 [Mar 24, 2015] 17 | ----------------------------- 18 | Initial Release 19 | 20 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | /* Prompts for a command and executes it on the shell */ 3 | { 4 | "caption": "FilterPipes: Send Text to Command", 5 | "command": "filter_pipes_exec_prompt" 6 | }, 7 | /* Creates (or opens) your own customer FilterPipes plugin */ 8 | { 9 | "caption": "FilterPipes: My Custom Filters Plugin", 10 | "command": "filter_pipes_my_plugin" 11 | }, 12 | 13 | /* Various encode/decode functions provided in filters.py */ 14 | { 15 | "caption": "FilterPipes: Base64 Encode", 16 | "command": "filter_pipes_base64", 17 | "args": { 18 | "decode": false, 19 | "wrap": 64 20 | } 21 | }, 22 | { 23 | "caption": "FilterPipes: Base64 Decode", 24 | "command": "filter_pipes_base64", 25 | "args": { 26 | "decode": true 27 | } 28 | }, 29 | { 30 | "caption": "FilterPipes: URL Encode", 31 | "command": "filter_pipes_urlencode", 32 | "args": { 33 | "decode": false 34 | } 35 | }, 36 | { 37 | "caption": "FilterPipes: URL Decode", 38 | "command": "filter_pipes_urlencode", 39 | "args": { 40 | "decode": true 41 | } 42 | }, 43 | { 44 | "caption": "FilterPipes: String Escape", 45 | "command": "filter_pipes_escape" 46 | }, 47 | { 48 | "caption": "FilterPipes: String Unescape", 49 | "command": "filter_pipes_escape", 50 | "args": { 51 | "decode": true 52 | } 53 | }, 54 | /* Example of using the filter_pipes_regex functionality */ 55 | { 56 | "caption": "FilterPipes: Strip Trailing Space", 57 | "command": "filter_pipes_regex", 58 | "args": { 59 | "regex": "[\t ]+$", 60 | "replacement": "", 61 | "lines": true 62 | } 63 | }, 64 | /* Advanced regex with post_init use */ 65 | { 66 | "caption": "FilterPipes: Hex to Decimal", 67 | "command": "filter_pipes_int_to_int", 68 | "args": { 69 | "from_base": 16, 70 | "to_base": 10 71 | } 72 | }, 73 | { 74 | "caption": "FilterPipes: Decimal to Hex", 75 | "command": "filter_pipes_int_to_int", 76 | "args": { 77 | "from_base": 10, 78 | "to_base": 16 79 | } 80 | }, 81 | { 82 | "caption": "FilterPipes: Decimal to Octal", 83 | "command": "filter_pipes_int_to_int", 84 | "args": { 85 | "from_base": 10, 86 | "to_base": 8 87 | } 88 | }, 89 | { 90 | "caption": "FilterPipes: Octal to Decimal", 91 | "command": "filter_pipes_int_to_int", 92 | "args": { 93 | "from_base": 8, 94 | "to_base": 10 95 | } 96 | } 97 | ] 98 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FilterPipes 2 | 3 | FilterPipes is a SublimeText (v2 and v3) plugin for filtering selected text 4 | through pipes or filters, either written as Python functions or as external 5 | processes. 6 | 7 | This project lives at [github.com/tylerl/FilterPipes](https://github.com/tylerl/FilterPipes). 8 | 9 | ### Filter Through Process 10 | This plugin provides a prompt for you to supply the shell command to execute. 11 | 12 | ![Shell execute animation](https://raw.githubusercontent.com/tylerl/FilterPipes/media/fp_sort.gif) 13 | 14 | ### Filter Through Python 15 | Python code can also be used as the filter function. Several useful examples 16 | are provided as listed below, but the real power comes in when you create your 17 | own functions. 18 | 19 | ### Creating Custom Filters 20 | FilterPipes makes it crazy-simple for you to create your own custom filter 21 | plugins. There are some pre-defined classes for simple text replacement, 22 | regular expressions, character-for-character translations, and external 23 | processes. Creating your own custom filter is as simple as creating a 24 | Python class with a single method called `filter` as demonstrated below: 25 | 26 | ![Plugin creation animation](https://raw.githubusercontent.com/tylerl/FilterPipes/media/fp_reverse.gif) 27 | 28 | # Provided Commands 29 | 30 | The following commands are included. All commands are accessible from 31 | the command palette. No shortcut keys are assigned by default, 32 | though you are free to add your own if you like. 33 | 34 | * **My Custom Filters Plugin**: Create and access your own FilterPipes-derived 35 | custom plugin. The first time you run this command it will create the plugin 36 | for you and add some sample content. Every subsequent run will simply open 37 | your existing plugin directory. 38 | * **Send Text to Command**: Prompts you for a shell command to run, and 39 | then executes that command using your selection(s) as `stdin`, and replacing 40 | them with `stdout` if the program ends successfully. 41 | * **Base64 Encode** and **Base64 Decode**: Encodes and decodes text using 42 | [Base64](Base64 Decode) encoding rules. 43 | * **URL Encode** and **URL Decode**: Similarly, encodes and decodes text 44 | using [URL ("percent") encoding](http://en.wikipedia.org/wiki/Percent-encoding). 45 | * **String Escape** and **String Unescape**: Encodes and decodes strings using 46 | simple string-escaping rules (e.g. TAB character becomes `\t`, newline becomes 47 | `\n`, and so forth. 48 | * **Strip Trailing Space**: Does what it says on the tin: it strips any spaces 49 | at the end of lines. While mildly useful, this is here primarily because I 50 | wanted to include an example of a Regex-based filter. 51 | 52 | ### Example Filters 53 | 54 | These filters are included in the "My Custom Filters Plugin" example. Run the 55 | "My Custom Filters Plugin" command to install them. 56 | 57 | Most of these filters don't use any custom Python class, but instead use the 58 | customization options of the provided generic filters to do something more 59 | interesting. The only code will be the entry in `Default.sublime-commands` file 60 | for your created plugin. 61 | 62 | #### Translation Commands 63 | These provide simple character-for-character translations to apply. Think of 64 | them like the unix `tr` command, if you're nerdy enough to ever have used it. 65 | 66 | * **Swap Quotes**: Swaps single quotes for double quotes (and vice-versa). 67 | (surprisingly useful, but not context-aware). 68 | * **Convert to Straight Quotes**: Turns all the varieties of "smart quotes" into 69 | normal "straight" quotes, like any good programmer would prefer. 70 | 71 | #### Regex Filter 72 | Your regex filter configuration provides a simple search regex and replacement 73 | string. It's not unlike regex-based search-and-replace, except your setup gets 74 | baked into a single simple command. 75 | 76 | * **Collapse Spaces**: Turns runs of any sort of whitespace into a single 77 | space character. 78 | 79 | * **Hex to Decimal, Decimal to Hex, Octal to Decimal, Decimal to Octal**: 80 | Convert integers between popular numeric bases. This is provided as an 81 | example of a regex filter that uses a callback function for replacements 82 | instead of a simple string constant. It's also an example of the use of 83 | using the `post_init` callback to rewrite the runtime configuration 84 | (in this case the search regex) programatically. 85 | 86 | #### Process Filters 87 | Entire plugins have been written for performing this one simple action. Actually, 88 | this plugin started out as one of them. It's gotten much more useful since then, 89 | though. This filter type lets you specify a command to run. Kind of like the 90 | "Send Text to Command" filter, but without the input prompt, and (by default) 91 | without the shell interpretation (you can optionally turn that on, though). 92 | 93 | Note that the included examples require you to install the associated program. 94 | You may have to fiddle with the path (and perhaps command-line options) as well, 95 | depending on your local installation. 96 | 97 | * **Beautify JS (js-beautify)**: Many javascript developers use a command called 98 | "js-beautify" to clean up their code. This filter calls that command... assuming 99 | it's installed in the PATH. Otherwise, it does nothing but serve as an example 100 | of how to write this kind of filter. 101 | * **Minify JS (uglifyjs)**: Just like the above, but exactly the opposite. This 102 | command makes your javascript compact and illegible... assuming you have it 103 | installed -- the same caveat applies as above. 104 | 105 | #### Python Filters 106 | These are the *really* cool ones. You write your own python code to transform 107 | the selected text. Note that the naming of the command follows the same pattern 108 | as per normal for SublimeText. You convert the Python class name from CamelCase 109 | to underscore_case and remove `_command` from the end to ge the name that goes into 110 | your `.sublime-commands` file. This is done internally by SublimeText, so don't get 111 | mad at me about it. 112 | 113 | * **Reverse Words**: This is the example coded in the animation above. It reverses 114 | the order of words delimited by spaces. I have no idea why anyone would ever 115 | want to do that, but the code is tiny and the effect is obvious, so it makes a 116 | pretty compelling piece of example code. 117 | * **to CamelCase**: Converts words from `underscore_case` to `CamelCase`. 118 | * **to mixedCase**: Converts words from `underscore_case` to `mixedCase`, which is 119 | exactly the same as CamelCase, but without the first letter capitalized. In fact, 120 | both these filters use the same Python class. This filter demonstrates how to add 121 | your own custom command options. 122 | * **to underscore_case**: Converts from `CamelCase` back to `underscore_case`, included 123 | mostly out of an obsessive-compulsive need for parity. 124 | 125 | # Creating Your Own 126 | 127 | To write your own plugin, start out by bringing up the command pallete (typically 128 | ctrl+shift+P or 129 | cmd+shift+P) and running "My Custom Filters Plugin". 130 | Even if you don't end up using the plugin it creates, this will at least give 131 | you a working scaffold with all the right configuration and examples such to get started. 132 | 133 | All commands take the optional parameter `use_selections`, which when set to false, tells 134 | the system to ignore your text selections and pass the whole file in. Typically this is 135 | useful for tools that use the entire file for context, such as source code formatters. 136 | 137 | ## Using the built-in filter classes 138 | 139 | Generic filter classes are provided that allow you to do a lot of cool things without 140 | writing any Python code. For these, you don't need a custom plugin, you can just 141 | put the appropriate command invocation in your own `.sublime-settings` or `.sublime-keymap` 142 | file. 143 | 144 | #### Using `filter_pipes_process` 145 | 146 | This sends text to an external process. The general configuration looks like 147 | this. Note that unless you've specified `shell` as `true`, you'll want your 148 | command to be a list of parameters rather than a single string. 149 | 150 | ```json 151 | { 152 | "caption": "YOLO Text", 153 | "command": "filter_pipes_process", 154 | "args": { 155 | "command": ["banner", "-w", "80"], 156 | "shell": false 157 | } 158 | } 159 | ``` 160 | 161 | #### Using `filter_pipes_translate` 162 | 163 | Does character-for-character translations using Python's `string.translate` 164 | function. That remark earlier about the `tr` command? Yeah, time to get your *Nerd* on. 165 | 166 | ```json 167 | { 168 | "caption": "H4X0R", 169 | "command": "filter_pipes_translate", 170 | "args": { 171 | "before": "AaEeiIoOlsSTt", 172 | "after": "4433110015577" 173 | } 174 | } 175 | ``` 176 | 177 | #### Using `filter_pipes_regex` 178 | 179 | Supply a regex and replacement. Pretty straightforward, but don't forget that 180 | backslashes have to be escaped for JSON encoding. 181 | 182 | ```json 183 | { 184 | "caption": "Format Phone Numbers", 185 | "command": "filter_pipes_regex", 186 | "args": { 187 | "regex": "(\\d{3})(\\d{3})(\\d{4})", 188 | "replacement": "(\\1) \\2-\\3" 189 | } 190 | } 191 | ``` 192 | 193 | #### Writing your own custom Python Filters 194 | 195 | Here's where the real magic happens. You can very easily write 196 | custom code to transform your text in any way imaginable, and 197 | it only takes a minute or two to get going. 198 | 199 | Once you've got your custom filter plugin created, open 200 | up the `myfilters.py` file and at the bottom of the file 201 | create a class like this: 202 | 203 | ```python 204 | class SuperAwesomeFilterCommand(filterpipes.FilterPipesCommandBase): 205 | """Does approximately nothing, but does it with style.""" 206 | def filter(self, data): 207 | return data 208 | ``` 209 | 210 | The class name determines the command name using the SublimeText rules 211 | metioned earlier. So `SuperAwesomeFilterCommand` becomes `super_awesome_filter`. 212 | 213 | Whatever your `filter()` function returns is what your text will 214 | be replaced with. So go get creative. 215 | 216 | Note that if you want your plugin to take some configurable parameter 217 | from the `.sublime-commands` file, it's pretty easy. First you create 218 | a class variable and assign it your desired default value: 219 | 220 | ```python 221 | class SurroundSelectionCommand(filterpipes.FilterPipesCommandBase): 222 | """Prepends and appends some string to the selected text.""" 223 | prepend = '{' 224 | append = '}' 225 | def filter(self, data): 226 | return self.prepend + data + self.append 227 | ``` 228 | 229 | Then in your `.sublime-keymap` or `.sublime-commands` file, specify 230 | some alternate value for those variables in the "args" section like so: 231 | 232 | ```json 233 | { 234 | "caption": "Double Power Bracket", 235 | "command": "surround_selection", 236 | "args": { 237 | "prepend": ".:[[", 238 | "append": "]]:." 239 | } 240 | } 241 | ``` 242 | 243 | # Copyright and License 244 | 245 | ***This is not an official Google product.*** 246 | 247 | Copyright 2015 Google Inc. All Rights Reserved. 248 | 249 | Licensed under the Apache License, Version 2.0 (the "License"); 250 | you may not use this file except in compliance with the License. 251 | You may obtain a copy of the License at 252 | 253 | http://www.apache.org/licenses/LICENSE-2.0 254 | 255 | Unless required by applicable law or agreed to in writing, software 256 | distributed under the License is distributed on an "AS IS" BASIS, 257 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 258 | See the License for the specific language governing permissions and 259 | limitations under the License. 260 | 261 | -------------------------------------------------------------------------------- /customfilters.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # This is not an official Google product. 16 | 17 | """Custom FilterPipes plugin creation. 18 | 19 | The components in this file handle the creation and access 20 | of your custom FilterPipes plugin project. The full content 21 | of the custom plugin is contained here. 22 | 23 | Part of the FilterPipes SublimeText Plugin. 24 | github.com/tylerl/FilterPipes 25 | 26 | """ 27 | 28 | 29 | import os 30 | import sublime 31 | import sublime_plugin 32 | CUSTOM_PLUGIN_NAME = 'MyCustomFilterPipes' 33 | CUSTOM_PLUGIN_PROJECT = 'plugin.sublime-project' 34 | README_FILENAME = 'README.txt' 35 | 36 | 37 | class FilterPipesMyPluginCommand(sublime_plugin.WindowCommand): 38 | def _create_plugin_impl(self, plugin_dir): 39 | os.mkdir(plugin_dir, 493) # 0755 (python 2/3 safe) 40 | for name, content in CONTENT_TEMPLATE.items(): 41 | filepath = os.path.join(plugin_dir, name) 42 | with os.fdopen( 43 | os.open(filepath, os.O_WRONLY | os.O_CREAT, 420), 44 | 'w') as f: # 420 = 0644 45 | if name == README_FILENAME: 46 | content = content.format(directory=plugin_dir) 47 | f.write(content) 48 | 49 | def _maybe_create_plugin(self, plugin_dir): 50 | if not sublime.ok_cancel_dialog( 51 | 'Your custom filter plugin does not exist yet. ' 52 | 'Would you like to create it now?', 53 | 'Create it' 54 | ): 55 | return False 56 | self._create_plugin_impl(plugin_dir) 57 | return True 58 | 59 | def run(self): 60 | plugin_dir = os.path.join(sublime.packages_path(), CUSTOM_PLUGIN_NAME) 61 | if not os.path.exists(plugin_dir): 62 | try: 63 | if not self._maybe_create_plugin(plugin_dir): 64 | return 65 | except Exception as ex: 66 | sublime.message_dialog('Failed to create plugin:\n%s' % (ex)) 67 | raise ex 68 | readme_path = os.path.join(plugin_dir, README_FILENAME) 69 | self.window.run_command('open_file', {'file': readme_path}) 70 | self.window.run_command('open_dir', {'dir': plugin_dir}) 71 | 72 | 73 | CONTENT_TEMPLATE = { 74 | 'Default.sublime-commands': 75 | r"""[ 76 | /********************************************************* 77 | * The following example commands show you the basics of * 78 | * working with filter pipes. Delete, modify, rename, and * 79 | * use as you see fit. * 80 | **********************************************************/ 81 | 82 | // ########################################################### 83 | // Translate filters. Translates characters in the "before" 84 | // string to the corresponding (by position) character in 85 | // the "after" string. 86 | { 87 | "caption": "FilterPipes Example: Swap Quotes", 88 | "command": "filter_pipes_translate", 89 | "args": { 90 | "before": "'\"", 91 | "after": "\"'" 92 | } 93 | }, 94 | 95 | { 96 | "caption": "FilterPipes Example: Convert to Straight Quotes", 97 | "command": "filter_pipes_translate", 98 | "args": { 99 | "before": "\u201c\u201d\u201f\u301d\u301e\uff02\u201e\u301f\u2018\u2019\u201b\uff07\u201a", 100 | "after": "\"\"\"\"\"\"\"\"'''''" 101 | } 102 | }, 103 | // ########################################################### 104 | // Regex filters. Runs the selection through a regular 105 | // expression replacement. 106 | // 107 | // You can specify 108 | // "lines": true 109 | // to add the MULTILINE flag to replacement function -- 110 | // this makes ^ and $ match the beginning and end of each 111 | // individual line instead of the whole string. 112 | 113 | { 114 | "caption": "FilterPipes Example: collapse spaces", 115 | "command": "filter_pipes_regex", 116 | "args": { 117 | "regex": "\\s+", 118 | "replacement": " " 119 | } 120 | }, 121 | // ########################################################### 122 | // Process filters. Runs the selection through an external 123 | // program as a filter instead. Remember you can specify 124 | // "use_selections": false 125 | // if you want to always process the whole file, regardless of 126 | // selections. 127 | // 128 | // Also note that if command is a list, then the first element 129 | // is the executable, and the other elements are parameters. 130 | // If it's instead simply a string, then, then that string is 131 | // interpreted as a shell command. 132 | 133 | // "pip install jsbeautifier" to make this one work 134 | { 135 | "caption": "FilterPipes Example: Beautify JS (js-beautify)", 136 | "command": "filter_pipes_process", 137 | "args": { 138 | "command": ["js-beautify", "-i"] 139 | } 140 | }, 141 | { 142 | "caption": "FilterPipes Example: Minify JS (uglifyjs)", 143 | "command": "filter_pipes_process", 144 | "args": { 145 | "command": ["uglifyjs"] 146 | } 147 | }, 148 | 149 | // ########################################################### 150 | // Python Filters. These have corresponding entries in the 151 | // myfilters.py file, where the command name is translated to 152 | // camelcase with the word "Command" appended. So for example 153 | // "camel_case_filter" is the class "CamelCaseFilterCommand" 154 | // in myfilters.py. The filter() function determines what the 155 | // filter does. Finally, any args provided here get automatically 156 | // set as class object instance variables; usually overriding 157 | // a Default setting. 158 | 159 | { /* See ReverseWordsCommand */ 160 | "caption": "FilterPipes Example: Reverse Words", 161 | "command": "reverse_words" 162 | }, 163 | 164 | { /* See CamelCaseFilterCommand */ 165 | "caption": "FilterPipes Example: to CamelCase", 166 | "command": "camel_case_filter", 167 | "args": { 168 | "initial_caps": true 169 | } 170 | }, 171 | 172 | { /* See CamelCaseFilterCommand */ 173 | "caption": "FilterPipes Example: to mixedCase", 174 | "command": "camel_case_filter", 175 | "args": { 176 | "initial_caps": false 177 | } 178 | }, 179 | 180 | { /* See UnderscoreCaseFilterCommand */ 181 | "caption": "FilterPipes Example: to underscore_case", 182 | "command": "underscore_case_filter" 183 | } 184 | ] 185 | """, 186 | 'myfilters.py': 187 | """"\""Sample filters for doing mildly useful things using FilterPipes."\"" 188 | 189 | try: 190 | from FilterPipes import filterpipes # ST3-style import 191 | except ImportError: 192 | import filterpipes # ST2-style import 193 | 194 | 195 | class ReverseWordsCommand(filterpipes.FilterPipesCommandBase): 196 | "\""Reverse the order of selected words. Extremely simple example."\"" 197 | def filter(self, data): 198 | return " ".join(reversed(data.split(" "))) 199 | 200 | 201 | class CamelCaseFilterCommand(filterpipes.FilterPipesCommandBase): 202 | "\""Converts words_with_underlines to CamelCase."\"" 203 | initial_caps = True 204 | 205 | def filter(self, data): 206 | next_upper = self.initial_caps 207 | out = [] 208 | for c in data: 209 | if c == '_': 210 | next_upper = True 211 | elif c.islower(): 212 | if next_upper: 213 | out.append(c.upper()) 214 | else: 215 | out.append(c) 216 | next_upper = False 217 | else: 218 | next_upper = self.initial_caps and not c.isalnum() 219 | out.append(c) 220 | return ''.join(out) 221 | 222 | 223 | class UnderscoreCaseFilterCommand(filterpipes.FilterPipesCommandBase): 224 | "\""Converts CamelCase to words_with_underlines."\"" 225 | 226 | def filter(self, data): 227 | prev_lower = False 228 | out = [] 229 | for c in data: 230 | if c.isupper(): 231 | if prev_lower: 232 | out.append('_') 233 | out.append(c.lower()) 234 | prev_lower = False 235 | elif c.islower(): 236 | prev_lower = True 237 | out.append(c) 238 | else: 239 | prev_lower = False 240 | out.append(c) 241 | return ''.join(out) 242 | """, 243 | README_FILENAME: 244 | """CUSTOM PLUGIN FILTERS 245 | ===================== 246 | 247 | This directory contains the a set of filters to be customized 248 | by you, the user. It's pre-populated with a few that show you 249 | the basics of how to create your own. These examples are 250 | designed to be useful in their own right. But feel free to 251 | modify, rename, or remove them as you see fit. 252 | 253 | To restore it to this initial state, just remove or 254 | rename this directory and re-create it using the 255 | "My Custom Filters" command. 256 | 257 | This plugin was created at: 258 | {directory} 259 | 260 | FILES 261 | ===== 262 | 263 | You should pay attention to the following files. The others... meh. 264 | 265 | Default.sublime-commands 266 | ------------------------ 267 | 268 | A good first place to start. This is where you define what commands 269 | will appear in your command palette. You can use the same Python 270 | class to create multiple commands using different arguments. You'll 271 | see several examples of that in example content provided. 272 | 273 | You can create a Default.sublime-keymap file to do the same thing 274 | but for keyboard shortcuts instead of command palette entries. 275 | 276 | myfilters.py 277 | ------------ 278 | 279 | This contains examples of custom Python filters. Each filter command 280 | is its own classes with a filter() function. The naming convention 281 | on the class is enforced by Sublime, so follow the pattern you see 282 | in the file; namely: camelcase words ending in "Command". For example, 283 | to create a command named "convert_to_lowercase" your class will be 284 | named ConvertToLowercaseCommand. 285 | """ 286 | } 287 | -------------------------------------------------------------------------------- /filterpipes.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # This is not an official Google product. 16 | 17 | """FilterPipes Library. 18 | 19 | This module abstracts many of the details for dealing with the 20 | SublimeText API. Several generic commands are provided, which can 21 | be further customized for additional behavior. 22 | 23 | Part of the FilterPipes SublimeText Plugin. 24 | github.com/tylerl/FilterPipes 25 | 26 | """ 27 | 28 | __author__ = 'Tyler Larson [github.com/tylerl/]' 29 | __version__ = '1.1.0' 30 | __license__ = 'Apache 2' 31 | __copyright__ = 'Copyright 2015, Google Inc.' 32 | 33 | import subprocess 34 | import sublime 35 | import sublime_plugin 36 | import sys 37 | import re 38 | 39 | ############################################################### 40 | # Python/Sublime version compatibility 41 | if sys.version_info[0] == 2: # Python 2.x specific (ST2) 42 | PYTHON2=True 43 | def is_str(obj): 44 | return isinstance(obj, basestring) 45 | else: # Python 3.x 46 | PYTHON2=False 47 | def is_str(obj): # Python 3.x specific (ST3) 48 | return isinstance(obj, str) 49 | ############################################################### 50 | 51 | 52 | class FilterPipesCommandBase(sublime_plugin.TextCommand): 53 | """Generic base for function-based filter commands. 54 | 55 | This class is not used directly, but rather inherited to build 56 | a filter plugin that actually does something. Selection and 57 | text replacement logic are handled by this class. 58 | 59 | Override filter(self, data) to perform text filter operation. 60 | """ 61 | use_selections = True 62 | errors_on_statusbar = True 63 | report_success = True 64 | report_failure = True 65 | report_nochange = True 66 | 67 | def filter(self, data): 68 | """Perform transformation on document text. 69 | 70 | Args: 71 | data: string containing selected text. 72 | 73 | Returns: 74 | string containing the desired replacement, or None to 75 | indicate no operation. 76 | 77 | """ 78 | return None 79 | 80 | def success_message(self): 81 | """Message to display on status bar if success.""" 82 | return 'FilterPipes: success' 83 | 84 | def failure_message(self): 85 | """Message to display on status bar if unsuccessful.""" 86 | return 'FilterPipes: command failed' 87 | 88 | def nochange_message(self): 89 | """Message to display on status bar if replacement matches original.""" 90 | return 'FilterPipes: No change' 91 | 92 | def do_replacements(self, edit): 93 | self.success = False 94 | self.replaced = False 95 | replacements = [] 96 | for r in self._regions(): 97 | replacement = self._get_replacement(r) 98 | if replacement is None: 99 | continue 100 | if replacement: 101 | replacements.append(replacement) 102 | # replace in reverse order to avoid overlap complications 103 | for replacement in reversed(replacements): 104 | self._commit_replacement(edit, replacement) 105 | msg = None 106 | if not self.success: 107 | if self.report_failure: 108 | msg = self.failure_message() 109 | elif not self.replaced: 110 | if self.report_nochange: 111 | msg = self.nochange_message() 112 | else: 113 | if self.report_success: 114 | msg = self.success_message() 115 | if msg: 116 | sublime.status_message(msg) 117 | 118 | def post_init(self): 119 | """Hook for doing some post-init reconfiguration. 120 | 121 | See the IntToInt filter for an example of how this 122 | can be useful. 123 | """ 124 | pass 125 | 126 | def apply_settings(self, settings): 127 | for k, v in settings.items(): 128 | setattr(self, k, v) 129 | 130 | def run(self, edit, **settings): 131 | try: 132 | self.apply_settings(settings) 133 | self.post_init() 134 | self.do_replacements(edit) 135 | except Exception as ex: 136 | if self.errors_on_statusbar: 137 | sublime.status_message(str(ex)) 138 | raise 139 | 140 | def _get_replacement(self, region): 141 | existing = self.view.substr(region) 142 | filtered = self.filter(existing) 143 | if filtered is None: 144 | return None 145 | self.success = True 146 | if filtered == existing: 147 | return None 148 | self.replaced = True 149 | return (region, filtered) 150 | 151 | def _commit_replacement(self, edit, replacement): 152 | region, text = replacement 153 | self.view.replace(edit, region, text) 154 | 155 | def _regions(self): 156 | regions = None 157 | if self.use_selections: 158 | regions = [r for r in self.view.sel() if not r.empty()] 159 | if not regions: 160 | regions = [sublime.Region(0, self.view.size())] 161 | return regions 162 | 163 | 164 | class FilterPipesProcessCommand(FilterPipesCommandBase): 165 | """Generic base for Process-based filter commands. 166 | 167 | Override self.command or self.getcommand() to specify which command to run, 168 | or pass in at run time as a configuration parameter. Set "shell" to true to 169 | execute as a shell command instead of direct process invocation. 170 | """ 171 | command = [] 172 | use_selections = True 173 | shell = False 174 | report_failure = False # we do our own failure reporting 175 | expected_returns = [0] 176 | subprocess_args = {} 177 | 178 | def _execute_raw(self, command, text): 179 | """Executes a command and returns stdout, stderr, and return code.""" 180 | args = self.subprocess_args or {} 181 | args['shell'] = self.shell 182 | cmd = subprocess.Popen(command, 183 | stdin=subprocess.PIPE, 184 | stdout=subprocess.PIPE, 185 | stderr=subprocess.PIPE, **args) 186 | (stdout, stderr) = cmd.communicate(text.encode('UTF-8')) 187 | return (stdout, stderr, cmd.returncode) 188 | 189 | def _expect_success(self, command, text): 190 | try: 191 | stdout, stderr, status = self._execute_raw(command, text) 192 | if not self.expected_returns or status in self.expected_returns: 193 | return stdout.decode('UTF-8') 194 | except OSError as e: 195 | stdout, stderr, status = (None, str(e), e.errno) 196 | 197 | if self.errors_on_statusbar: 198 | sublime.status_message( 199 | 'Error %i executing command [%s]: %s' % 200 | (status, self.get_command_as_str(), stderr)) 201 | print( 202 | 'Error %i executing command [%s]:\n%s\n' % 203 | (status, self.get_command_as_str(False), stderr)) 204 | return None 205 | 206 | def filter(self, existing): 207 | return self._expect_success(self.get_command(), existing) 208 | 209 | def get_command(self): 210 | return self.command 211 | 212 | def get_command_as_str(self, short=True): 213 | c = self.get_command() 214 | if is_str(c): 215 | return c 216 | if short: 217 | return c[0] 218 | return ' '.join(c) 219 | 220 | def success_message(self): 221 | return 'Filtered through: %s' % (self.get_command_as_str()) 222 | 223 | 224 | class FilterPipesTranslateCommand(FilterPipesCommandBase): 225 | """Translates characters from one set to another. 226 | 227 | Like the tr shell command. 228 | 229 | """ 230 | before = None 231 | after = None 232 | 233 | def filter(self, data): 234 | if not self.before or not self.after: 235 | return None 236 | if PYTHON2: 237 | trans = dict(zip([ord(c) for c in self.before], self.after)) 238 | else: 239 | trans = str.maketrans(self.before,self.after) 240 | return data.translate(trans) 241 | 242 | 243 | class FilterPipesRegexCommand(FilterPipesCommandBase): 244 | """Performs a regular expression replacement. 245 | 246 | Because re.sub is magic, replacement can be either a string or a 247 | function that takes a match object. 248 | 249 | """ 250 | regex = None 251 | replacement = None 252 | flags = 0 253 | count = 0 254 | lines = False 255 | 256 | def filter(self, data): 257 | if self.regex is None or self.replacement is None: 258 | return None 259 | if self.lines: 260 | self.flags |= re.MULTILINE 261 | return re.sub(self.regex, self.replacement, data, 262 | count=self.count, flags=self.flags) 263 | 264 | 265 | class FilterPipesExecPromptCommand(sublime_plugin.TextCommand): 266 | """Prompt for a command to filter text through.""" 267 | 268 | def run(self, edit): 269 | self.view.window().show_input_panel( 270 | 'Filter Command:', '', self.on_done, None, None) 271 | 272 | def on_done(self, text): 273 | self.view.run_command( 274 | 'filter_pipes_process', {'command': text, 'shell': True}) 275 | -------------------------------------------------------------------------------- /filters.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # This is not an official Google product. 16 | 17 | """Included filters for FilterPipes. 18 | 19 | These filters build upon the classes from the filterpipes base 20 | library to provide a useful set of features for everyday use. 21 | 22 | Part of the FilterPipes SublimeText Plugin. 23 | github.com/tylerl/FilterPipes 24 | 25 | """ 26 | 27 | import sys 28 | if sys.version_info[0] == 3: # Python 3; ST 3 29 | from FilterPipes import filterpipes # ST3-style import 30 | from urllib.parse import quote, unquote 31 | else: 32 | import filterpipes # ST2-style import 33 | from urllib import quote, unquote 34 | 35 | import base64 36 | 37 | 38 | class FilterPipesBase64Command(filterpipes.FilterPipesCommandBase): 39 | decode = False 40 | wrap = 64 41 | urlsafe = False 42 | 43 | def filter(self, text): 44 | b64e = base64.base64encode 45 | b64d = base64.base64decode 46 | if self.urlsafe: 47 | b64e = base64.urlsafe_base64encode 48 | b64d = base64.urlsafe_base64decode 49 | if self.decode: 50 | return b64d(text.encode('UTF-8')).decode('UTF-8') 51 | encoded = b64e(text.encode('UTF-8')).decode('UTF-8') 52 | if self.wrap: 53 | return '\n'.join( 54 | (encoded[i:i + self.wrap] for i in 55 | range(0, len(encoded), self.wrap))) 56 | return encoded 57 | 58 | 59 | class FilterPipesUrlencodeCommand(filterpipes.FilterPipesCommandBase): 60 | decode = False 61 | 62 | def filter(self, text): 63 | if self.decode: 64 | return unquote(text) 65 | else: 66 | return quote(text) 67 | 68 | 69 | class FilterPipesEscapeCommand(filterpipes.FilterPipesCommandBase): 70 | decode = False 71 | 72 | def filter(self, data): 73 | if self.decode: 74 | return data.encode("UTF-8").decode('unicode-escape') 75 | else: 76 | return data.encode('unicode-escape').decode("UTF-8") 77 | 78 | 79 | class FilterPipesIntToIntCommand(filterpipes.FilterPipesRegexCommand): 80 | """Converts integer strings between common bases. 81 | 82 | Included as a demonstration of more complex uses of regex matching, 83 | including configuration using the post_init callback as well as 84 | replacement using a function instead of static text. 85 | """ 86 | BASE_REGEX = { 87 | 10: r"\b([0-9]+)\b", 88 | 16: r"\b((?:0[Xx])?[0-9a-fA-F]+)\b", 89 | 8: r"\b([0-7]+)\b" 90 | } 91 | BASE_FMT = {8: "%o", 10: "%i", 16: "%x"} 92 | PREFIX = {8: "0", 10: "", 16: "0x"} 93 | regex = None # set by post_init 94 | from_base = 10 95 | to_base = 16 96 | case = "lower" 97 | output_prefix = True 98 | 99 | def post_init(self): 100 | self.from_base = int(self.from_base) 101 | self.to_base = int(self.to_base) 102 | self.regex = self.BASE_REGEX[self.from_base] 103 | self.output_fmt = self.BASE_FMT[self.to_base] 104 | if self.output_prefix: 105 | self.output_fmt = self.PREFIX[self.to_base] + self.output_fmt 106 | if self.case == "upper": 107 | self.output_fmt = self.output_fmt.upper() 108 | 109 | def replacement(self, match): 110 | txt = match.group(1) 111 | try: 112 | val = int(txt, self.from_base) 113 | return self.output_fmt % (val) 114 | except ValueError: 115 | pass 116 | return match.group(0) 117 | --------------------------------------------------------------------------------