├── .no-sublime-package ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-commands ├── Main.sublime-menu ├── README.md ├── README.src.md ├── diff_match_patch ├── __init__.py ├── python2 │ ├── __init__.py │ └── diff_match_patch.py └── python3 │ ├── __init__.py │ └── diff_match_patch.py ├── fmt.phar ├── generateReadme.php ├── message ├── messages.json ├── messages ├── 1.14.0.txt ├── 1.14.1.txt ├── 1.14.2.txt ├── 1.14.3.txt ├── 1.15.0.txt ├── 1.16.3.txt ├── 1.17.0.txt ├── 1.18.2.txt ├── 1.19.0.txt ├── 1.22.0.txt ├── 1.25.0.txt ├── 1.26.0.txt ├── 1.59.0.txt ├── 11.0.0.txt ├── 12.0.0.txt ├── 12.1.0.txt ├── 2.13.0.txt ├── 3.14.0.txt ├── 3.21.0.txt ├── 3.27.0.txt ├── 3.6.0.txt ├── 3.9.0.txt ├── 4.0.0.txt ├── 4.0.1.txt ├── 4.23.0.txt ├── 4.4.0.txt ├── 5.0.3.txt ├── 5.0.7.txt ├── 5.1.0.txt ├── 5.2.0.txt ├── 6.2.0.txt ├── 9.13.0.txt ├── 9.3.0.txt └── install.txt ├── oracle.php ├── php.tools.ini ├── phpfmt.py ├── phpfmt.sublime-settings ├── refactor.php └── version.txt /.no-sublime-package: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanch/phpfmt_stable/98de6272d5dd98667a49d7d2f831b13dfac7838c/.no-sublime-package -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["f10"], "command": "analyse_this" }, 3 | { "keys": ["f11"], "command": "fmt_now" } 4 | ] 5 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["f10"], "command": "analyse_this" }, 3 | { "keys": ["f11"], "command": "fmt_now" } 4 | ] 5 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+f10"], "command": "analyse_this" }, 3 | { "keys": ["ctrl+f11"], "command": "fmt_now" } 4 | ] 5 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | {"caption": "phpfmt: format now", "command": "fmt_now"}, 3 | {"caption": "phpfmt: indentation with spaces", "command": "indent_with_spaces"}, 4 | 5 | {"caption": "phpfmt: toggle additional transformations", "command": "toggle_pass_menu" }, 6 | {"caption": "phpfmt: toggle excluded transformations", "command": "toggle_exclude_menu" }, 7 | 8 | {"caption": "phpfmt: toggle skip execution when .php.tools.ini is missing", "command": "toggle", "args": {"option":"skip_if_ini_missing"}}, 9 | 10 | {"caption": "phpfmt: toggle autocomplete", "command": "toggle", "args": {"option":"autocomplete"}}, 11 | {"caption": "phpfmt: toggle dependency autoimport", "command": "toggle", "args": {"option":"autoimport"}}, 12 | {"caption": "phpfmt: toggle format on save", "command": "toggle", "args": {"option":"format_on_save"}}, 13 | 14 | {"caption": "phpfmt: toggle PSR1 - Class and Methods names", "command": "toggle", "args": {"option":"psr1_naming"}}, 15 | {"caption": "phpfmt: toggle PSR1", "command": "toggle", "args": {"option":"psr1"}}, 16 | {"caption": "phpfmt: toggle PSR2", "command": "toggle", "args": {"option":"psr2"}}, 17 | {"caption": "phpfmt: analyse this", "command": "analyse_this"}, 18 | {"caption": "phpfmt: build autocomplete database", "command": "build_oracle"}, 19 | {"caption": "phpfmt: getter and setter (camelCase)", "command": "sgter_camel"}, 20 | {"caption": "phpfmt: getter and setter (Go)", "command": "sgter_go"}, 21 | {"caption": "phpfmt: getter and setter (snake_case)", "command": "sgter_snake"}, 22 | {"caption": "phpfmt: generate PHPDoc block", "command": "generate_phpdoc"}, 23 | {"caption": "phpfmt: look for .php.tools.ini", "command": "toggle", "args": {"option":"readini"}}, 24 | {"caption": "phpfmt: reorganize content of class", "command": "order_method"}, 25 | 26 | {"caption": "phpfmt: enable/disable additional transformations", "command": "toggle_pass_menu" }, 27 | {"caption": "phpfmt: troubleshoot information", "command": "debug_env"}, 28 | 29 | {"caption": "phpfmt: update PHP binary path", "command": "update_php_bin"} 30 | ] -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mnemonic": "n", 4 | "caption": "Preferences", 5 | "id": "preferences", 6 | "children": [ 7 | { 8 | "mnemonic": "P", 9 | "caption": "Package Settings", 10 | "id": "package-settings", 11 | "children": [ 12 | { 13 | "caption": "phpfmt", 14 | "children": [ 15 | { 16 | "caption": "Settings – Default", 17 | "args": { 18 | "file": "${packages}/phpfmt/phpfmt.sublime-settings" 19 | }, 20 | "command": "open_file" 21 | }, 22 | { 23 | "caption": "Settings – User", 24 | "args": { 25 | "file": "${packages}/User/phpfmt.sublime-settings" 26 | }, 27 | "command": "open_file" 28 | }, 29 | { 30 | "caption": "Key Bindings – Default", 31 | "args": { 32 | "file": "${packages}/phpfmt/Default.sublime-keymap" 33 | }, 34 | "command": "open_file" 35 | }, 36 | { 37 | "caption": "Key Bindings – User", 38 | "args": { 39 | "file": "${packages}/User/Default.sublime-keymap" 40 | }, 41 | "command": "open_file" 42 | }, 43 | { 44 | "caption": "-" 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [phpfmt](https://github.com/nanch/phpfmt_stable) support for Sublime Text 3 2 | 3 | ## Installation 4 | 5 | #### Requirements 6 | - **You must have a running copy of PHP on the machine you are running Sublime Text** 7 | 8 | Plugin runs with PHP 7.0 or newer installed in the machine running the plugin. 9 | 10 | #### Install this plugin through Package Manager. 11 | 12 | - In Sublime Text press `ctrl+shift+P` 13 | - Choose `Package Control: Install Package` 14 | - Choose `phpfmt` 15 | 16 | #### Configuration (Windows) 17 | 18 | - Edit configuration file (`%AppData%\Sublime Text\Packages\phpfmt\phpfmt.sublime-settings`) 19 | - For field `"php_bin"` enter the path to the php.exe 20 | Example: `"php_bin":"c:/PHP/php.exe"` 21 | 22 | #### Configuration (OS X and Linux) 23 | 24 | - Edit configuration file (`phpfmt.sublime-settings`) 25 | - For field `"php_bin"` enter the path to the php 26 | Example: `"php_bin":"/usr/local/bin/php"` 27 | 28 | ### Settings 29 | 30 | Prefer using the toggle options at command palette. However you might find yourself in need to setup where PHP is running, use this option below for the configuration file. 31 | ``` 32 | { 33 | "php_bin":"/usr/local/bin/php", 34 | } 35 | ``` 36 | 37 | **The following features are available through command palette (`ctrl+shift+P` or `cmd+shift+P`) :** 38 | 39 | * phpfmt: format now 40 | * phpfmt: indentation with spaces 41 | * phpfmt: toggle additional transformations 42 | * phpfmt: toggle excluded transformations 43 | * phpfmt: toggle skip execution when .php.tools.ini is missing 44 | * phpfmt: toggle autocomplete 45 | * phpfmt: toggle dependency autoimport 46 | * phpfmt: toggle format on save 47 | * phpfmt: toggle PSR1 - Class and Methods names 48 | * phpfmt: toggle PSR1 49 | * phpfmt: toggle PSR2 50 | * phpfmt: analyse this 51 | * phpfmt: build autocomplete database 52 | * phpfmt: getter and setter (camelCase) 53 | * phpfmt: getter and setter (Go) 54 | * phpfmt: getter and setter (snake_case) 55 | * phpfmt: generate PHPDoc block 56 | * phpfmt: look for .php.tools.ini 57 | * phpfmt: reorganize content of class 58 | * phpfmt: enable/disable additional transformations 59 | * phpfmt: troubleshoot information 60 | * phpfmt: update PHP binary path 61 | 62 | 63 | ### Currently Supported Transformations: 64 | 65 | * AddMissingParentheses Add extra parentheses in new instantiations. 66 | * AliasToMaster Replace function aliases to their masters - only basic syntax alias. 67 | * AlignConstVisibilityEquals Vertically align "=" of visibility and const blocks. 68 | * AlignDoubleArrow Vertically align T_DOUBLE_ARROW (=>). 69 | * AlignDoubleSlashComments Vertically align "//" comments. 70 | * AlignEquals Vertically align "=". 71 | * AlignGroupDoubleArrow Vertically align T_DOUBLE_ARROW (=>) by line groups. 72 | * AlignPHPCode Align PHP code within HTML block. 73 | * AlignTypehint Vertically align function type hints. 74 | * AllmanStyleBraces Transform all curly braces into Allman-style. 75 | * AutoPreincrement Automatically convert postincrement to preincrement. 76 | * AutoSemicolon Add semicolons in statements ends. 77 | * CakePHPStyle Applies CakePHP Coding Style 78 | * ClassToSelf "self" is preferred within class, trait or interface. 79 | * ClassToStatic "static" is preferred within class, trait or interface. 80 | * ConvertOpenTagWithEcho Convert from " implode()). 88 | * LeftWordWrap Word wrap at 80 columns - left justify. 89 | * LongArray Convert short to long arrays. 90 | * MergeElseIf Merge if with else. 91 | * SplitElseIf Merge if with else. 92 | * MergeNamespaceWithOpenTag Ensure there is no more than one linebreak before namespace 93 | * MildAutoPreincrement Automatically convert postincrement to preincrement. (Deprecated pass. Use AutoPreincrement instead). 94 | * NewLineBeforeReturn Add an empty line before T_RETURN. 95 | * OrganizeClass Organize class, interface and trait structure. 96 | * OrderAndRemoveUseClauses Order use block and remove unused imports. 97 | * OnlyOrderUseClauses Order use block - do not remove unused imports. 98 | * OrderMethod Organize class, interface and trait structure. 99 | * OrderMethodAndVisibility Organize class, interface and trait structure. 100 | * PHPDocTypesToFunctionTypehint Read variable types from PHPDoc blocks and add them in function signatures. 101 | * PrettyPrintDocBlocks Prettify Doc Blocks 102 | * PSR2EmptyFunction Merges in the same line of function header the body of empty functions. 103 | * PSR2MultilineFunctionParams Break function parameters into multiple lines. 104 | * ReindentAndAlignObjOps Align object operators. 105 | * ReindentSwitchBlocks Reindent one level deeper the content of switch blocks. 106 | * RemoveIncludeParentheses Remove parentheses from include declarations. 107 | * RemoveSemicolonAfterCurly Remove semicolon after closing curly brace. 108 | * RemoveUseLeadingSlash Remove leading slash in T_USE imports. 109 | * ReplaceBooleanAndOr Convert from "and"/"or" to "&&"/"||". Danger! This pass leads to behavior change. 110 | * ReplaceIsNull Replace is_null($a) with null === $a. 111 | * RestoreComments Revert any formatting of comments content. 112 | * ReturnNull Simplify empty returns. 113 | * ShortArray Convert old array into new array. (array() -> []) 114 | * SmartLnAfterCurlyOpen Add line break when implicit curly block is added. 115 | * SortUseNameSpace Organize use clauses by length and alphabetic order. 116 | * SpaceAroundControlStructures Add space around control structures. 117 | * SpaceAroundExclamationMark Add spaces around exclamation mark. 118 | * SpaceBetweenMethods Put space between methods. 119 | * StrictBehavior Activate strict option in array_search, base64_decode, in_array, array_keys, mb_detect_encoding. Danger! This pass leads to behavior change. 120 | * StrictComparison All comparisons are converted to strict. Danger! This pass leads to behavior change. 121 | * StripExtraCommaInArray Remove trailing commas within array blocks 122 | * StripNewlineAfterClassOpen Strip empty lines after class opening curly brace. 123 | * StripNewlineAfterCurlyOpen Strip empty lines after opening curly brace. 124 | * StripNewlineWithinClassBody Strip empty lines after class opening curly brace. 125 | * StripSpaces Remove all empty spaces 126 | * StripSpaceWithinControlStructures Strip empty lines within control structures. 127 | * TightConcat Ensure string concatenation does not have spaces, except when close to numbers. 128 | * TrimSpaceBeforeSemicolon Remove empty lines before semi-colon. 129 | * UpgradeToPreg Upgrade ereg_* calls to preg_* 130 | * WordWrap Word wrap at 80 columns. 131 | * WrongConstructorName Update old constructor names into new ones. http://php.net/manual/en/language.oop5.decon.php 132 | * YodaComparisons Execute Yoda Comparisons. 133 | 134 | ### What does it do? 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 151 | 160 | 161 | 162 | 169 | 177 | 178 | 179 | 192 | 206 | 207 |
BeforeAfter
143 |
<?php
144 | for($i = 0; $i < 10; $i++)
145 | {
146 | if($i%2==0)
147 | echo "Flipflop";
148 | }
149 | 
150 |
152 |
<?php
153 | for ($i = 0; $i < 10; $i++) {
154 |   if ($i%2 == 0) {
155 |     echo "Flipflop";
156 |   }
157 | }
158 | 
159 |
163 |
<?php
164 | $a = 10;
165 | $otherVar = 20;
166 | $third = 30;
167 | 
168 |
170 |
<?php
171 | $a        = 10;
172 | $otherVar = 20;
173 | $third    = 30;
174 | 
175 | This can be enabled with the option "enable_auto_align" 176 |
180 |
<?php
181 | namespace NS\Something;
182 | use \OtherNS\C;
183 | use \OtherNS\B;
184 | use \OtherNS\A;
185 | use \OtherNS\D;
186 | 
187 | $a = new A();
188 | $b = new C();
189 | $d = new D();
190 | 
191 |
193 |
<?php
194 | namespace NS\Something;
195 | 
196 | use \OtherNS\A;
197 | use \OtherNS\C;
198 | use \OtherNS\D;
199 | 
200 | $a = new A();
201 | $b = new C();
202 | $d = new D();
203 | 
204 | note how it sorts the use clauses, and removes unused ones 205 |
208 | 209 | ### What does it do? - PSR version 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 226 | 236 | 237 | 238 | 247 | 259 | 260 | 261 | 274 | 288 | 289 |
BeforeAfter
218 |
<?php
219 | for($i = 0; $i < 10; $i++)
220 | {
221 | if($i%2==0)
222 | echo "Flipflop";
223 | }
224 | 
225 |
227 |
<?php
228 | for ($i = 0; $i < 10; $i++) {
229 |     if ($i%2 == 0) {
230 |         echo "Flipflop";
231 |     }
232 | }
233 | 
234 | Note the identation of 4 spaces. 235 |
239 |
<?php
240 | class A {
241 | function a(){
242 | return 10;
243 | }
244 | }
245 | 
246 |
248 |
<?php
249 | class A
250 | {
251 |     public function a()
252 |     {
253 |         return 10;
254 |     }
255 | }
256 | 
257 | Note the braces position, and the visibility adjustment in the method a(). 258 |
262 |
<?php
263 | namespace NS\Something;
264 | use \OtherNS\C;
265 | use \OtherNS\B;
266 | use \OtherNS\A;
267 | use \OtherNS\D;
268 | 
269 | $a = new A();
270 | $b = new C();
271 | $d = new D();
272 | 
273 |
275 |
<?php
276 | namespace NS\Something;
277 | 
278 | use \OtherNS\A;
279 | use \OtherNS\C;
280 | use \OtherNS\D;
281 | 
282 | $a = new A();
283 | $b = new C();
284 | $d = new D();
285 | 
286 | note how it sorts the use clauses, and removes unused ones 287 |
290 | 291 | ### Troubleshooting 292 | - Be sure you can run PHP from the command line. 293 | - If you need support, please open an issue at [fmt issues](https://github.com/nanch/phpfmt_stable/issues) 294 | 295 | ### The Most FAQ 296 | 297 | ***I want to use sublime-phpfmt, but it needs PHP 5.6 or newer and on my production 298 | server I have PHP 5.5 or older. What should I do?*** 299 | 300 | Consider installing a standalone PHP 5.6 in a separate directory and have it *not* 301 | configured in the environment. Within the plugin, ensure `php_bin` parameter is pointed to this standalone installation. 302 | 303 | ### Acknowledgements 304 | - GoSublime - for the method to update the formatted buffer 305 | - Google's diff match patch - http://code.google.com/p/google-diff-match-patch/ 306 | -------------------------------------------------------------------------------- /README.src.md: -------------------------------------------------------------------------------- 1 | # [phpfmt](https://github.com/phpfmt/fmt) support for Sublime Text 2/3 2 | 3 | ***[This project follows a Code of Conduct.](https://github.com/phpfmt/code-of-conduct)*** 4 | 5 | ### Installation 6 | 7 | #### Requirements 8 | - **You must have a running copy of PHP on the machine you are running Sublime Text** 9 | 10 | Plugin runs with PHP 7.0 or newer installed in the machine running the plugin. 11 | 12 | #### Install this plugin through Package Manager. 13 | 14 | - In Sublime Text press `ctrl+shift+P` 15 | - Choose `Package Control: Install Package` 16 | - Choose `phpfmt` 17 | 18 | #### Configuration (Windows) 19 | 20 | - Edit configuration file (`%AppData%\Sublime Text\Packages\phpfmt\phpfmt.sublime-settings`) 21 | - For field `"php_bin"` enter the path to the php.exe 22 | Example: `"php_bin":"c:/PHP/php.exe"` 23 | 24 | #### Configuration (OS X and Linux) 25 | 26 | - Edit configuration file (`phpfmt.sublime-settings`) 27 | - For field `"php_bin"` enter the path to the php 28 | Example: `"php_bin":"/usr/local/bin/php"` 29 | 30 | ### Settings 31 | 32 | Prefer using the toggle options at command palette. However you might find yourself in need to setup where PHP is running, use this option below for the configuration file. 33 | ``` 34 | { 35 | "php_bin":"/usr/local/bin/php", 36 | } 37 | ``` 38 | 39 | **The following features are available through command palette (`ctrl+shift+P` or `cmd+shift+P`) :** 40 | 41 | %CMD% 42 | 43 | 44 | ### Currently Supported Transformations: 45 | 46 | %PASSES% 47 | 48 | ### What does it do? 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 65 | 74 | 75 | 76 | 83 | 91 | 92 | 93 | 106 | 120 | 121 |
BeforeAfter
57 |
<?php
 58 | for($i = 0; $i < 10; $i++)
 59 | {
 60 | if($i%2==0)
 61 | echo "Flipflop";
 62 | }
 63 | 
64 |
66 |
<?php
 67 | for ($i = 0; $i < 10; $i++) {
 68 | 	if ($i%2 == 0) {
 69 | 		echo "Flipflop";
 70 | 	}
 71 | }
 72 | 
73 |
77 |
<?php
 78 | $a = 10;
 79 | $otherVar = 20;
 80 | $third = 30;
 81 | 
82 |
84 |
<?php
 85 | $a        = 10;
 86 | $otherVar = 20;
 87 | $third    = 30;
 88 | 
89 | This can be enabled with the option "enable_auto_align" 90 |
94 |
<?php
 95 | namespace NS\Something;
 96 | use \OtherNS\C;
 97 | use \OtherNS\B;
 98 | use \OtherNS\A;
 99 | use \OtherNS\D;
100 | 
101 | $a = new A();
102 | $b = new C();
103 | $d = new D();
104 | 
105 |
107 |
<?php
108 | namespace NS\Something;
109 | 
110 | use \OtherNS\A;
111 | use \OtherNS\C;
112 | use \OtherNS\D;
113 | 
114 | $a = new A();
115 | $b = new C();
116 | $d = new D();
117 | 
118 | note how it sorts the use clauses, and removes unused ones 119 |
122 | 123 | ### What does it do? - PSR version 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 140 | 150 | 151 | 152 | 161 | 173 | 174 | 175 | 188 | 202 | 203 |
BeforeAfter
132 |
<?php
133 | for($i = 0; $i < 10; $i++)
134 | {
135 | if($i%2==0)
136 | echo "Flipflop";
137 | }
138 | 
139 |
141 |
<?php
142 | for ($i = 0; $i < 10; $i++) {
143 |     if ($i%2 == 0) {
144 |         echo "Flipflop";
145 |     }
146 | }
147 | 
148 | Note the identation of 4 spaces. 149 |
153 |
<?php
154 | class A {
155 | function a(){
156 | return 10;
157 | }
158 | }
159 | 
160 |
162 |
<?php
163 | class A
164 | {
165 |     public function a()
166 |     {
167 |         return 10;
168 |     }
169 | }
170 | 
171 | Note the braces position, and the visibility adjustment in the method a(). 172 |
176 |
<?php
177 | namespace NS\Something;
178 | use \OtherNS\C;
179 | use \OtherNS\B;
180 | use \OtherNS\A;
181 | use \OtherNS\D;
182 | 
183 | $a = new A();
184 | $b = new C();
185 | $d = new D();
186 | 
187 |
189 |
<?php
190 | namespace NS\Something;
191 | 
192 | use \OtherNS\A;
193 | use \OtherNS\C;
194 | use \OtherNS\D;
195 | 
196 | $a = new A();
197 | $b = new C();
198 | $d = new D();
199 | 
200 | note how it sorts the use clauses, and removes unused ones 201 |
204 | 205 | ### Troubleshooting 206 | - Be sure you can run PHP from the command line. 207 | - If you need support, please open an issue at [fmt issues](https://github.com/phpfmt/fmt/issues) 208 | 209 | ### The Most FAQ 210 | 211 | ***I want to use sublime-phpfmt, but it needs PHP 5.6 or newer and on my production 212 | server I have PHP 5.5 or older. What should I do?*** 213 | 214 | Consider installing a standalone PHP 5.6 in a separate directory and have it *not* 215 | configured in the environment. Within the plugin, ensure `php_bin` parameter is pointed to this standalone installation. 216 | 217 | ### Acknowledgements 218 | - GoSublime - for the method to update the formatted buffer 219 | - Google's diff match patch - http://code.google.com/p/google-diff-match-patch/ 220 | -------------------------------------------------------------------------------- /diff_match_patch/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /diff_match_patch/python2/__init__.py: -------------------------------------------------------------------------------- 1 | from .diff_match_patch import diff_match_patch, patch_obj 2 | 3 | -------------------------------------------------------------------------------- /diff_match_patch/python3/__init__.py: -------------------------------------------------------------------------------- 1 | from .diff_match_patch import diff_match_patch, patch_obj 2 | 3 | -------------------------------------------------------------------------------- /diff_match_patch/python3/diff_match_patch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """Diff Match and Patch 4 | 5 | Copyright 2006 Google Inc. 6 | http://code.google.com/p/google-diff-match-patch/ 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | """ 20 | 21 | """Functions for diff, match and patch. 22 | 23 | Computes the difference between two texts to create a patch. 24 | Applies the patch onto another text, allowing for errors. 25 | """ 26 | 27 | __author__ = 'fraser@google.com (Neil Fraser)' 28 | 29 | import math 30 | import re 31 | import sys 32 | import time 33 | import urllib.parse 34 | 35 | class diff_match_patch: 36 | """Class containing the diff, match and patch methods. 37 | 38 | Also contains the behaviour settings. 39 | """ 40 | 41 | def __init__(self): 42 | """Inits a diff_match_patch object with default settings. 43 | Redefine these in your program to override the defaults. 44 | """ 45 | 46 | # Number of seconds to map a diff before giving up (0 for infinity). 47 | self.Diff_Timeout = 1.0 48 | # Cost of an empty edit operation in terms of edit characters. 49 | self.Diff_EditCost = 4 50 | # At what point is no match declared (0.0 = perfection, 1.0 = very loose). 51 | self.Match_Threshold = 0.5 52 | # How far to search for a match (0 = exact location, 1000+ = broad match). 53 | # A match this many characters away from the expected location will add 54 | # 1.0 to the score (0.0 is a perfect match). 55 | self.Match_Distance = 1000 56 | # When deleting a large block of text (over ~64 characters), how close do 57 | # the contents have to be to match the expected contents. (0.0 = perfection, 58 | # 1.0 = very loose). Note that Match_Threshold controls how closely the 59 | # end points of a delete need to match. 60 | self.Patch_DeleteThreshold = 0.5 61 | # Chunk size for context length. 62 | self.Patch_Margin = 4 63 | 64 | # The number of bits in an int. 65 | # Python has no maximum, thus to disable patch splitting set to 0. 66 | # However to avoid long patches in certain pathological cases, use 32. 67 | # Multiple short patches (using native ints) are much faster than long ones. 68 | self.Match_MaxBits = 32 69 | 70 | # DIFF FUNCTIONS 71 | 72 | # The data structure representing a diff is an array of tuples: 73 | # [(DIFF_DELETE, "Hello"), (DIFF_INSERT, "Goodbye"), (DIFF_EQUAL, " world.")] 74 | # which means: delete "Hello", add "Goodbye" and keep " world." 75 | DIFF_DELETE = -1 76 | DIFF_INSERT = 1 77 | DIFF_EQUAL = 0 78 | 79 | def diff_main(self, text1, text2, checklines=True, deadline=None): 80 | """Find the differences between two texts. Simplifies the problem by 81 | stripping any common prefix or suffix off the texts before diffing. 82 | 83 | Args: 84 | text1: Old string to be diffed. 85 | text2: New string to be diffed. 86 | checklines: Optional speedup flag. If present and false, then don't run 87 | a line-level diff first to identify the changed areas. 88 | Defaults to true, which does a faster, slightly less optimal diff. 89 | deadline: Optional time when the diff should be complete by. Used 90 | internally for recursive calls. Users should set DiffTimeout instead. 91 | 92 | Returns: 93 | Array of changes. 94 | """ 95 | # Set a deadline by which time the diff must be complete. 96 | if deadline == None: 97 | # Unlike in most languages, Python counts time in seconds. 98 | if self.Diff_Timeout <= 0: 99 | deadline = sys.maxsize 100 | else: 101 | deadline = time.time() + self.Diff_Timeout 102 | 103 | # Check for null inputs. 104 | if text1 == None or text2 == None: 105 | raise ValueError("Null inputs. (diff_main)") 106 | 107 | # Check for equality (speedup). 108 | if text1 == text2: 109 | if text1: 110 | return [(self.DIFF_EQUAL, text1)] 111 | return [] 112 | 113 | # Trim off common prefix (speedup). 114 | commonlength = self.diff_commonPrefix(text1, text2) 115 | commonprefix = text1[:commonlength] 116 | text1 = text1[commonlength:] 117 | text2 = text2[commonlength:] 118 | 119 | # Trim off common suffix (speedup). 120 | commonlength = self.diff_commonSuffix(text1, text2) 121 | if commonlength == 0: 122 | commonsuffix = '' 123 | else: 124 | commonsuffix = text1[-commonlength:] 125 | text1 = text1[:-commonlength] 126 | text2 = text2[:-commonlength] 127 | 128 | # Compute the diff on the middle block. 129 | diffs = self.diff_compute(text1, text2, checklines, deadline) 130 | 131 | # Restore the prefix and suffix. 132 | if commonprefix: 133 | diffs[:0] = [(self.DIFF_EQUAL, commonprefix)] 134 | if commonsuffix: 135 | diffs.append((self.DIFF_EQUAL, commonsuffix)) 136 | self.diff_cleanupMerge(diffs) 137 | return diffs 138 | 139 | def diff_compute(self, text1, text2, checklines, deadline): 140 | """Find the differences between two texts. Assumes that the texts do not 141 | have any common prefix or suffix. 142 | 143 | Args: 144 | text1: Old string to be diffed. 145 | text2: New string to be diffed. 146 | checklines: Speedup flag. If false, then don't run a line-level diff 147 | first to identify the changed areas. 148 | If true, then run a faster, slightly less optimal diff. 149 | deadline: Time when the diff should be complete by. 150 | 151 | Returns: 152 | Array of changes. 153 | """ 154 | if not text1: 155 | # Just add some text (speedup). 156 | return [(self.DIFF_INSERT, text2)] 157 | 158 | if not text2: 159 | # Just delete some text (speedup). 160 | return [(self.DIFF_DELETE, text1)] 161 | 162 | if len(text1) > len(text2): 163 | (longtext, shorttext) = (text1, text2) 164 | else: 165 | (shorttext, longtext) = (text1, text2) 166 | i = longtext.find(shorttext) 167 | if i != -1: 168 | # Shorter text is inside the longer text (speedup). 169 | diffs = [(self.DIFF_INSERT, longtext[:i]), (self.DIFF_EQUAL, shorttext), 170 | (self.DIFF_INSERT, longtext[i + len(shorttext):])] 171 | # Swap insertions for deletions if diff is reversed. 172 | if len(text1) > len(text2): 173 | diffs[0] = (self.DIFF_DELETE, diffs[0][1]) 174 | diffs[2] = (self.DIFF_DELETE, diffs[2][1]) 175 | return diffs 176 | 177 | if len(shorttext) == 1: 178 | # Single character string. 179 | # After the previous speedup, the character can't be an equality. 180 | return [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)] 181 | 182 | # Check to see if the problem can be split in two. 183 | hm = self.diff_halfMatch(text1, text2) 184 | if hm: 185 | # A half-match was found, sort out the return data. 186 | (text1_a, text1_b, text2_a, text2_b, mid_common) = hm 187 | # Send both pairs off for separate processing. 188 | diffs_a = self.diff_main(text1_a, text2_a, checklines, deadline) 189 | diffs_b = self.diff_main(text1_b, text2_b, checklines, deadline) 190 | # Merge the results. 191 | return diffs_a + [(self.DIFF_EQUAL, mid_common)] + diffs_b 192 | 193 | if checklines and len(text1) > 100 and len(text2) > 100: 194 | return self.diff_lineMode(text1, text2, deadline) 195 | 196 | return self.diff_bisect(text1, text2, deadline) 197 | 198 | def diff_lineMode(self, text1, text2, deadline): 199 | """Do a quick line-level diff on both strings, then rediff the parts for 200 | greater accuracy. 201 | This speedup can produce non-minimal diffs. 202 | 203 | Args: 204 | text1: Old string to be diffed. 205 | text2: New string to be diffed. 206 | deadline: Time when the diff should be complete by. 207 | 208 | Returns: 209 | Array of changes. 210 | """ 211 | 212 | # Scan the text on a line-by-line basis first. 213 | (text1, text2, linearray) = self.diff_linesToChars(text1, text2) 214 | 215 | diffs = self.diff_main(text1, text2, False, deadline) 216 | 217 | # Convert the diff back to original text. 218 | self.diff_charsToLines(diffs, linearray) 219 | # Eliminate freak matches (e.g. blank lines) 220 | self.diff_cleanupSemantic(diffs) 221 | 222 | # Rediff any replacement blocks, this time character-by-character. 223 | # Add a dummy entry at the end. 224 | diffs.append((self.DIFF_EQUAL, '')) 225 | pointer = 0 226 | count_delete = 0 227 | count_insert = 0 228 | text_delete = '' 229 | text_insert = '' 230 | while pointer < len(diffs): 231 | if diffs[pointer][0] == self.DIFF_INSERT: 232 | count_insert += 1 233 | text_insert += diffs[pointer][1] 234 | elif diffs[pointer][0] == self.DIFF_DELETE: 235 | count_delete += 1 236 | text_delete += diffs[pointer][1] 237 | elif diffs[pointer][0] == self.DIFF_EQUAL: 238 | # Upon reaching an equality, check for prior redundancies. 239 | if count_delete >= 1 and count_insert >= 1: 240 | # Delete the offending records and add the merged ones. 241 | a = self.diff_main(text_delete, text_insert, False, deadline) 242 | diffs[pointer - count_delete - count_insert : pointer] = a 243 | pointer = pointer - count_delete - count_insert + len(a) 244 | count_insert = 0 245 | count_delete = 0 246 | text_delete = '' 247 | text_insert = '' 248 | 249 | pointer += 1 250 | 251 | diffs.pop() # Remove the dummy entry at the end. 252 | 253 | return diffs 254 | 255 | def diff_bisect(self, text1, text2, deadline): 256 | """Find the 'middle snake' of a diff, split the problem in two 257 | and return the recursively constructed diff. 258 | See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. 259 | 260 | Args: 261 | text1: Old string to be diffed. 262 | text2: New string to be diffed. 263 | deadline: Time at which to bail if not yet complete. 264 | 265 | Returns: 266 | Array of diff tuples. 267 | """ 268 | 269 | # Cache the text lengths to prevent multiple calls. 270 | text1_length = len(text1) 271 | text2_length = len(text2) 272 | max_d = (text1_length + text2_length + 1) // 2 273 | v_offset = max_d 274 | v_length = 2 * max_d 275 | v1 = [-1] * v_length 276 | v1[v_offset + 1] = 0 277 | v2 = v1[:] 278 | delta = text1_length - text2_length 279 | # If the total number of characters is odd, then the front path will 280 | # collide with the reverse path. 281 | front = (delta % 2 != 0) 282 | # Offsets for start and end of k loop. 283 | # Prevents mapping of space beyond the grid. 284 | k1start = 0 285 | k1end = 0 286 | k2start = 0 287 | k2end = 0 288 | for d in range(max_d): 289 | # Bail out if deadline is reached. 290 | if time.time() > deadline: 291 | break 292 | 293 | # Walk the front path one step. 294 | for k1 in range(-d + k1start, d + 1 - k1end, 2): 295 | k1_offset = v_offset + k1 296 | if k1 == -d or (k1 != d and 297 | v1[k1_offset - 1] < v1[k1_offset + 1]): 298 | x1 = v1[k1_offset + 1] 299 | else: 300 | x1 = v1[k1_offset - 1] + 1 301 | y1 = x1 - k1 302 | while (x1 < text1_length and y1 < text2_length and 303 | text1[x1] == text2[y1]): 304 | x1 += 1 305 | y1 += 1 306 | v1[k1_offset] = x1 307 | if x1 > text1_length: 308 | # Ran off the right of the graph. 309 | k1end += 2 310 | elif y1 > text2_length: 311 | # Ran off the bottom of the graph. 312 | k1start += 2 313 | elif front: 314 | k2_offset = v_offset + delta - k1 315 | if k2_offset >= 0 and k2_offset < v_length and v2[k2_offset] != -1: 316 | # Mirror x2 onto top-left coordinate system. 317 | x2 = text1_length - v2[k2_offset] 318 | if x1 >= x2: 319 | # Overlap detected. 320 | return self.diff_bisectSplit(text1, text2, x1, y1, deadline) 321 | 322 | # Walk the reverse path one step. 323 | for k2 in range(-d + k2start, d + 1 - k2end, 2): 324 | k2_offset = v_offset + k2 325 | if k2 == -d or (k2 != d and 326 | v2[k2_offset - 1] < v2[k2_offset + 1]): 327 | x2 = v2[k2_offset + 1] 328 | else: 329 | x2 = v2[k2_offset - 1] + 1 330 | y2 = x2 - k2 331 | while (x2 < text1_length and y2 < text2_length and 332 | text1[-x2 - 1] == text2[-y2 - 1]): 333 | x2 += 1 334 | y2 += 1 335 | v2[k2_offset] = x2 336 | if x2 > text1_length: 337 | # Ran off the left of the graph. 338 | k2end += 2 339 | elif y2 > text2_length: 340 | # Ran off the top of the graph. 341 | k2start += 2 342 | elif not front: 343 | k1_offset = v_offset + delta - k2 344 | if k1_offset >= 0 and k1_offset < v_length and v1[k1_offset] != -1: 345 | x1 = v1[k1_offset] 346 | y1 = v_offset + x1 - k1_offset 347 | # Mirror x2 onto top-left coordinate system. 348 | x2 = text1_length - x2 349 | if x1 >= x2: 350 | # Overlap detected. 351 | return self.diff_bisectSplit(text1, text2, x1, y1, deadline) 352 | 353 | # Diff took too long and hit the deadline or 354 | # number of diffs equals number of characters, no commonality at all. 355 | return [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)] 356 | 357 | def diff_bisectSplit(self, text1, text2, x, y, deadline): 358 | """Given the location of the 'middle snake', split the diff in two parts 359 | and recurse. 360 | 361 | Args: 362 | text1: Old string to be diffed. 363 | text2: New string to be diffed. 364 | x: Index of split point in text1. 365 | y: Index of split point in text2. 366 | deadline: Time at which to bail if not yet complete. 367 | 368 | Returns: 369 | Array of diff tuples. 370 | """ 371 | text1a = text1[:x] 372 | text2a = text2[:y] 373 | text1b = text1[x:] 374 | text2b = text2[y:] 375 | 376 | # Compute both diffs serially. 377 | diffs = self.diff_main(text1a, text2a, False, deadline) 378 | diffsb = self.diff_main(text1b, text2b, False, deadline) 379 | 380 | return diffs + diffsb 381 | 382 | def diff_linesToChars(self, text1, text2): 383 | """Split two texts into an array of strings. Reduce the texts to a string 384 | of hashes where each Unicode character represents one line. 385 | 386 | Args: 387 | text1: First string. 388 | text2: Second string. 389 | 390 | Returns: 391 | Three element tuple, containing the encoded text1, the encoded text2 and 392 | the array of unique strings. The zeroth element of the array of unique 393 | strings is intentionally blank. 394 | """ 395 | lineArray = [] # e.g. lineArray[4] == "Hello\n" 396 | lineHash = {} # e.g. lineHash["Hello\n"] == 4 397 | 398 | # "\x00" is a valid character, but various debuggers don't like it. 399 | # So we'll insert a junk entry to avoid generating a null character. 400 | lineArray.append('') 401 | 402 | def diff_linesToCharsMunge(text): 403 | """Split a text into an array of strings. Reduce the texts to a string 404 | of hashes where each Unicode character represents one line. 405 | Modifies linearray and linehash through being a closure. 406 | 407 | Args: 408 | text: String to encode. 409 | 410 | Returns: 411 | Encoded string. 412 | """ 413 | chars = [] 414 | # Walk the text, pulling out a substring for each line. 415 | # text.split('\n') would would temporarily double our memory footprint. 416 | # Modifying text would create many large strings to garbage collect. 417 | lineStart = 0 418 | lineEnd = -1 419 | while lineEnd < len(text) - 1: 420 | lineEnd = text.find('\n', lineStart) 421 | if lineEnd == -1: 422 | lineEnd = len(text) - 1 423 | line = text[lineStart:lineEnd + 1] 424 | lineStart = lineEnd + 1 425 | 426 | if line in lineHash: 427 | chars.append(chr(lineHash[line])) 428 | else: 429 | lineArray.append(line) 430 | lineHash[line] = len(lineArray) - 1 431 | chars.append(chr(len(lineArray) - 1)) 432 | return "".join(chars) 433 | 434 | chars1 = diff_linesToCharsMunge(text1) 435 | chars2 = diff_linesToCharsMunge(text2) 436 | return (chars1, chars2, lineArray) 437 | 438 | def diff_charsToLines(self, diffs, lineArray): 439 | """Rehydrate the text in a diff from a string of line hashes to real lines 440 | of text. 441 | 442 | Args: 443 | diffs: Array of diff tuples. 444 | lineArray: Array of unique strings. 445 | """ 446 | for x in range(len(diffs)): 447 | text = [] 448 | for char in diffs[x][1]: 449 | text.append(lineArray[ord(char)]) 450 | diffs[x] = (diffs[x][0], "".join(text)) 451 | 452 | def diff_commonPrefix(self, text1, text2): 453 | """Determine the common prefix of two strings. 454 | 455 | Args: 456 | text1: First string. 457 | text2: Second string. 458 | 459 | Returns: 460 | The number of characters common to the start of each string. 461 | """ 462 | # Quick check for common null cases. 463 | if not text1 or not text2 or text1[0] != text2[0]: 464 | return 0 465 | # Binary search. 466 | # Performance analysis: http://neil.fraser.name/news/2007/10/09/ 467 | pointermin = 0 468 | pointermax = min(len(text1), len(text2)) 469 | pointermid = pointermax 470 | pointerstart = 0 471 | while pointermin < pointermid: 472 | if text1[pointerstart:pointermid] == text2[pointerstart:pointermid]: 473 | pointermin = pointermid 474 | pointerstart = pointermin 475 | else: 476 | pointermax = pointermid 477 | pointermid = (pointermax - pointermin) // 2 + pointermin 478 | return pointermid 479 | 480 | def diff_commonSuffix(self, text1, text2): 481 | """Determine the common suffix of two strings. 482 | 483 | Args: 484 | text1: First string. 485 | text2: Second string. 486 | 487 | Returns: 488 | The number of characters common to the end of each string. 489 | """ 490 | # Quick check for common null cases. 491 | if not text1 or not text2 or text1[-1] != text2[-1]: 492 | return 0 493 | # Binary search. 494 | # Performance analysis: http://neil.fraser.name/news/2007/10/09/ 495 | pointermin = 0 496 | pointermax = min(len(text1), len(text2)) 497 | pointermid = pointermax 498 | pointerend = 0 499 | while pointermin < pointermid: 500 | if (text1[-pointermid:len(text1) - pointerend] == 501 | text2[-pointermid:len(text2) - pointerend]): 502 | pointermin = pointermid 503 | pointerend = pointermin 504 | else: 505 | pointermax = pointermid 506 | pointermid = (pointermax - pointermin) // 2 + pointermin 507 | return pointermid 508 | 509 | def diff_commonOverlap(self, text1, text2): 510 | """Determine if the suffix of one string is the prefix of another. 511 | 512 | Args: 513 | text1 First string. 514 | text2 Second string. 515 | 516 | Returns: 517 | The number of characters common to the end of the first 518 | string and the start of the second string. 519 | """ 520 | # Cache the text lengths to prevent multiple calls. 521 | text1_length = len(text1) 522 | text2_length = len(text2) 523 | # Eliminate the null case. 524 | if text1_length == 0 or text2_length == 0: 525 | return 0 526 | # Truncate the longer string. 527 | if text1_length > text2_length: 528 | text1 = text1[-text2_length:] 529 | elif text1_length < text2_length: 530 | text2 = text2[:text1_length] 531 | text_length = min(text1_length, text2_length) 532 | # Quick check for the worst case. 533 | if text1 == text2: 534 | return text_length 535 | 536 | # Start by looking for a single character match 537 | # and increase length until no match is found. 538 | # Performance analysis: http://neil.fraser.name/news/2010/11/04/ 539 | best = 0 540 | length = 1 541 | while True: 542 | pattern = text1[-length:] 543 | found = text2.find(pattern) 544 | if found == -1: 545 | return best 546 | length += found 547 | if found == 0 or text1[-length:] == text2[:length]: 548 | best = length 549 | length += 1 550 | 551 | def diff_halfMatch(self, text1, text2): 552 | """Do the two texts share a substring which is at least half the length of 553 | the longer text? 554 | This speedup can produce non-minimal diffs. 555 | 556 | Args: 557 | text1: First string. 558 | text2: Second string. 559 | 560 | Returns: 561 | Five element Array, containing the prefix of text1, the suffix of text1, 562 | the prefix of text2, the suffix of text2 and the common middle. Or None 563 | if there was no match. 564 | """ 565 | if self.Diff_Timeout <= 0: 566 | # Don't risk returning a non-optimal diff if we have unlimited time. 567 | return None 568 | if len(text1) > len(text2): 569 | (longtext, shorttext) = (text1, text2) 570 | else: 571 | (shorttext, longtext) = (text1, text2) 572 | if len(longtext) < 4 or len(shorttext) * 2 < len(longtext): 573 | return None # Pointless. 574 | 575 | def diff_halfMatchI(longtext, shorttext, i): 576 | """Does a substring of shorttext exist within longtext such that the 577 | substring is at least half the length of longtext? 578 | Closure, but does not reference any external variables. 579 | 580 | Args: 581 | longtext: Longer string. 582 | shorttext: Shorter string. 583 | i: Start index of quarter length substring within longtext. 584 | 585 | Returns: 586 | Five element Array, containing the prefix of longtext, the suffix of 587 | longtext, the prefix of shorttext, the suffix of shorttext and the 588 | common middle. Or None if there was no match. 589 | """ 590 | seed = longtext[i:i + len(longtext) // 4] 591 | best_common = '' 592 | j = shorttext.find(seed) 593 | while j != -1: 594 | prefixLength = self.diff_commonPrefix(longtext[i:], shorttext[j:]) 595 | suffixLength = self.diff_commonSuffix(longtext[:i], shorttext[:j]) 596 | if len(best_common) < suffixLength + prefixLength: 597 | best_common = (shorttext[j - suffixLength:j] + 598 | shorttext[j:j + prefixLength]) 599 | best_longtext_a = longtext[:i - suffixLength] 600 | best_longtext_b = longtext[i + prefixLength:] 601 | best_shorttext_a = shorttext[:j - suffixLength] 602 | best_shorttext_b = shorttext[j + prefixLength:] 603 | j = shorttext.find(seed, j + 1) 604 | 605 | if len(best_common) * 2 >= len(longtext): 606 | return (best_longtext_a, best_longtext_b, 607 | best_shorttext_a, best_shorttext_b, best_common) 608 | else: 609 | return None 610 | 611 | # First check if the second quarter is the seed for a half-match. 612 | hm1 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 3) // 4) 613 | # Check again based on the third quarter. 614 | hm2 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 1) // 2) 615 | if not hm1 and not hm2: 616 | return None 617 | elif not hm2: 618 | hm = hm1 619 | elif not hm1: 620 | hm = hm2 621 | else: 622 | # Both matched. Select the longest. 623 | if len(hm1[4]) > len(hm2[4]): 624 | hm = hm1 625 | else: 626 | hm = hm2 627 | 628 | # A half-match was found, sort out the return data. 629 | if len(text1) > len(text2): 630 | (text1_a, text1_b, text2_a, text2_b, mid_common) = hm 631 | else: 632 | (text2_a, text2_b, text1_a, text1_b, mid_common) = hm 633 | return (text1_a, text1_b, text2_a, text2_b, mid_common) 634 | 635 | def diff_cleanupSemantic(self, diffs): 636 | """Reduce the number of edits by eliminating semantically trivial 637 | equalities. 638 | 639 | Args: 640 | diffs: Array of diff tuples. 641 | """ 642 | changes = False 643 | equalities = [] # Stack of indices where equalities are found. 644 | lastequality = None # Always equal to diffs[equalities[-1]][1] 645 | pointer = 0 # Index of current position. 646 | # Number of chars that changed prior to the equality. 647 | length_insertions1, length_deletions1 = 0, 0 648 | # Number of chars that changed after the equality. 649 | length_insertions2, length_deletions2 = 0, 0 650 | while pointer < len(diffs): 651 | if diffs[pointer][0] == self.DIFF_EQUAL: # Equality found. 652 | equalities.append(pointer) 653 | length_insertions1, length_insertions2 = length_insertions2, 0 654 | length_deletions1, length_deletions2 = length_deletions2, 0 655 | lastequality = diffs[pointer][1] 656 | else: # An insertion or deletion. 657 | if diffs[pointer][0] == self.DIFF_INSERT: 658 | length_insertions2 += len(diffs[pointer][1]) 659 | else: 660 | length_deletions2 += len(diffs[pointer][1]) 661 | # Eliminate an equality that is smaller or equal to the edits on both 662 | # sides of it. 663 | if (lastequality and (len(lastequality) <= 664 | max(length_insertions1, length_deletions1)) and 665 | (len(lastequality) <= max(length_insertions2, length_deletions2))): 666 | # Duplicate record. 667 | diffs.insert(equalities[-1], (self.DIFF_DELETE, lastequality)) 668 | # Change second copy to insert. 669 | diffs[equalities[-1] + 1] = (self.DIFF_INSERT, 670 | diffs[equalities[-1] + 1][1]) 671 | # Throw away the equality we just deleted. 672 | equalities.pop() 673 | # Throw away the previous equality (it needs to be reevaluated). 674 | if len(equalities): 675 | equalities.pop() 676 | if len(equalities): 677 | pointer = equalities[-1] 678 | else: 679 | pointer = -1 680 | # Reset the counters. 681 | length_insertions1, length_deletions1 = 0, 0 682 | length_insertions2, length_deletions2 = 0, 0 683 | lastequality = None 684 | changes = True 685 | pointer += 1 686 | 687 | # Normalize the diff. 688 | if changes: 689 | self.diff_cleanupMerge(diffs) 690 | self.diff_cleanupSemanticLossless(diffs) 691 | 692 | # Find any overlaps between deletions and insertions. 693 | # e.g: abcxxxxxxdef 694 | # -> abcxxxdef 695 | # e.g: xxxabcdefxxx 696 | # -> defxxxabc 697 | # Only extract an overlap if it is as big as the edit ahead or behind it. 698 | pointer = 1 699 | while pointer < len(diffs): 700 | if (diffs[pointer - 1][0] == self.DIFF_DELETE and 701 | diffs[pointer][0] == self.DIFF_INSERT): 702 | deletion = diffs[pointer - 1][1] 703 | insertion = diffs[pointer][1] 704 | overlap_length1 = self.diff_commonOverlap(deletion, insertion) 705 | overlap_length2 = self.diff_commonOverlap(insertion, deletion) 706 | if overlap_length1 >= overlap_length2: 707 | if (overlap_length1 >= len(deletion) / 2.0 or 708 | overlap_length1 >= len(insertion) / 2.0): 709 | # Overlap found. Insert an equality and trim the surrounding edits. 710 | diffs.insert(pointer, (self.DIFF_EQUAL, 711 | insertion[:overlap_length1])) 712 | diffs[pointer - 1] = (self.DIFF_DELETE, 713 | deletion[:len(deletion) - overlap_length1]) 714 | diffs[pointer + 1] = (self.DIFF_INSERT, 715 | insertion[overlap_length1:]) 716 | pointer += 1 717 | else: 718 | if (overlap_length2 >= len(deletion) / 2.0 or 719 | overlap_length2 >= len(insertion) / 2.0): 720 | # Reverse overlap found. 721 | # Insert an equality and swap and trim the surrounding edits. 722 | diffs.insert(pointer, (self.DIFF_EQUAL, deletion[:overlap_length2])) 723 | diffs[pointer - 1] = (self.DIFF_INSERT, 724 | insertion[:len(insertion) - overlap_length2]) 725 | diffs[pointer + 1] = (self.DIFF_DELETE, deletion[overlap_length2:]) 726 | pointer += 1 727 | pointer += 1 728 | pointer += 1 729 | 730 | def diff_cleanupSemanticLossless(self, diffs): 731 | """Look for single edits surrounded on both sides by equalities 732 | which can be shifted sideways to align the edit to a word boundary. 733 | e.g: The cat came. -> The cat came. 734 | 735 | Args: 736 | diffs: Array of diff tuples. 737 | """ 738 | 739 | def diff_cleanupSemanticScore(one, two): 740 | """Given two strings, compute a score representing whether the 741 | internal boundary falls on logical boundaries. 742 | Scores range from 6 (best) to 0 (worst). 743 | Closure, but does not reference any external variables. 744 | 745 | Args: 746 | one: First string. 747 | two: Second string. 748 | 749 | Returns: 750 | The score. 751 | """ 752 | if not one or not two: 753 | # Edges are the best. 754 | return 6 755 | 756 | # Each port of this function behaves slightly differently due to 757 | # subtle differences in each language's definition of things like 758 | # 'whitespace'. Since this function's purpose is largely cosmetic, 759 | # the choice has been made to use each language's native features 760 | # rather than force total conformity. 761 | char1 = one[-1] 762 | char2 = two[0] 763 | nonAlphaNumeric1 = not char1.isalnum() 764 | nonAlphaNumeric2 = not char2.isalnum() 765 | whitespace1 = nonAlphaNumeric1 and char1.isspace() 766 | whitespace2 = nonAlphaNumeric2 and char2.isspace() 767 | lineBreak1 = whitespace1 and (char1 == "\r" or char1 == "\n") 768 | lineBreak2 = whitespace2 and (char2 == "\r" or char2 == "\n") 769 | blankLine1 = lineBreak1 and self.BLANKLINEEND.search(one) 770 | blankLine2 = lineBreak2 and self.BLANKLINESTART.match(two) 771 | 772 | if blankLine1 or blankLine2: 773 | # Five points for blank lines. 774 | return 5 775 | elif lineBreak1 or lineBreak2: 776 | # Four points for line breaks. 777 | return 4 778 | elif nonAlphaNumeric1 and not whitespace1 and whitespace2: 779 | # Three points for end of sentences. 780 | return 3 781 | elif whitespace1 or whitespace2: 782 | # Two points for whitespace. 783 | return 2 784 | elif nonAlphaNumeric1 or nonAlphaNumeric2: 785 | # One point for non-alphanumeric. 786 | return 1 787 | return 0 788 | 789 | pointer = 1 790 | # Intentionally ignore the first and last element (don't need checking). 791 | while pointer < len(diffs) - 1: 792 | if (diffs[pointer - 1][0] == self.DIFF_EQUAL and 793 | diffs[pointer + 1][0] == self.DIFF_EQUAL): 794 | # This is a single edit surrounded by equalities. 795 | equality1 = diffs[pointer - 1][1] 796 | edit = diffs[pointer][1] 797 | equality2 = diffs[pointer + 1][1] 798 | 799 | # First, shift the edit as far left as possible. 800 | commonOffset = self.diff_commonSuffix(equality1, edit) 801 | if commonOffset: 802 | commonString = edit[-commonOffset:] 803 | equality1 = equality1[:-commonOffset] 804 | edit = commonString + edit[:-commonOffset] 805 | equality2 = commonString + equality2 806 | 807 | # Second, step character by character right, looking for the best fit. 808 | bestEquality1 = equality1 809 | bestEdit = edit 810 | bestEquality2 = equality2 811 | bestScore = (diff_cleanupSemanticScore(equality1, edit) + 812 | diff_cleanupSemanticScore(edit, equality2)) 813 | while edit and equality2 and edit[0] == equality2[0]: 814 | equality1 += edit[0] 815 | edit = edit[1:] + equality2[0] 816 | equality2 = equality2[1:] 817 | score = (diff_cleanupSemanticScore(equality1, edit) + 818 | diff_cleanupSemanticScore(edit, equality2)) 819 | # The >= encourages trailing rather than leading whitespace on edits. 820 | if score >= bestScore: 821 | bestScore = score 822 | bestEquality1 = equality1 823 | bestEdit = edit 824 | bestEquality2 = equality2 825 | 826 | if diffs[pointer - 1][1] != bestEquality1: 827 | # We have an improvement, save it back to the diff. 828 | if bestEquality1: 829 | diffs[pointer - 1] = (diffs[pointer - 1][0], bestEquality1) 830 | else: 831 | del diffs[pointer - 1] 832 | pointer -= 1 833 | diffs[pointer] = (diffs[pointer][0], bestEdit) 834 | if bestEquality2: 835 | diffs[pointer + 1] = (diffs[pointer + 1][0], bestEquality2) 836 | else: 837 | del diffs[pointer + 1] 838 | pointer -= 1 839 | pointer += 1 840 | 841 | # Define some regex patterns for matching boundaries. 842 | BLANKLINEEND = re.compile(r"\n\r?\n$"); 843 | BLANKLINESTART = re.compile(r"^\r?\n\r?\n"); 844 | 845 | def diff_cleanupEfficiency(self, diffs): 846 | """Reduce the number of edits by eliminating operationally trivial 847 | equalities. 848 | 849 | Args: 850 | diffs: Array of diff tuples. 851 | """ 852 | changes = False 853 | equalities = [] # Stack of indices where equalities are found. 854 | lastequality = None # Always equal to diffs[equalities[-1]][1] 855 | pointer = 0 # Index of current position. 856 | pre_ins = False # Is there an insertion operation before the last equality. 857 | pre_del = False # Is there a deletion operation before the last equality. 858 | post_ins = False # Is there an insertion operation after the last equality. 859 | post_del = False # Is there a deletion operation after the last equality. 860 | while pointer < len(diffs): 861 | if diffs[pointer][0] == self.DIFF_EQUAL: # Equality found. 862 | if (len(diffs[pointer][1]) < self.Diff_EditCost and 863 | (post_ins or post_del)): 864 | # Candidate found. 865 | equalities.append(pointer) 866 | pre_ins = post_ins 867 | pre_del = post_del 868 | lastequality = diffs[pointer][1] 869 | else: 870 | # Not a candidate, and can never become one. 871 | equalities = [] 872 | lastequality = None 873 | 874 | post_ins = post_del = False 875 | else: # An insertion or deletion. 876 | if diffs[pointer][0] == self.DIFF_DELETE: 877 | post_del = True 878 | else: 879 | post_ins = True 880 | 881 | # Five types to be split: 882 | # ABXYCD 883 | # AXCD 884 | # ABXC 885 | # AXCD 886 | # ABXC 887 | 888 | if lastequality and ((pre_ins and pre_del and post_ins and post_del) or 889 | ((len(lastequality) < self.Diff_EditCost / 2) and 890 | (pre_ins + pre_del + post_ins + post_del) == 3)): 891 | # Duplicate record. 892 | diffs.insert(equalities[-1], (self.DIFF_DELETE, lastequality)) 893 | # Change second copy to insert. 894 | diffs[equalities[-1] + 1] = (self.DIFF_INSERT, 895 | diffs[equalities[-1] + 1][1]) 896 | equalities.pop() # Throw away the equality we just deleted. 897 | lastequality = None 898 | if pre_ins and pre_del: 899 | # No changes made which could affect previous entry, keep going. 900 | post_ins = post_del = True 901 | equalities = [] 902 | else: 903 | if len(equalities): 904 | equalities.pop() # Throw away the previous equality. 905 | if len(equalities): 906 | pointer = equalities[-1] 907 | else: 908 | pointer = -1 909 | post_ins = post_del = False 910 | changes = True 911 | pointer += 1 912 | 913 | if changes: 914 | self.diff_cleanupMerge(diffs) 915 | 916 | def diff_cleanupMerge(self, diffs): 917 | """Reorder and merge like edit sections. Merge equalities. 918 | Any edit section can move as long as it doesn't cross an equality. 919 | 920 | Args: 921 | diffs: Array of diff tuples. 922 | """ 923 | diffs.append((self.DIFF_EQUAL, '')) # Add a dummy entry at the end. 924 | pointer = 0 925 | count_delete = 0 926 | count_insert = 0 927 | text_delete = '' 928 | text_insert = '' 929 | while pointer < len(diffs): 930 | if diffs[pointer][0] == self.DIFF_INSERT: 931 | count_insert += 1 932 | text_insert += diffs[pointer][1] 933 | pointer += 1 934 | elif diffs[pointer][0] == self.DIFF_DELETE: 935 | count_delete += 1 936 | text_delete += diffs[pointer][1] 937 | pointer += 1 938 | elif diffs[pointer][0] == self.DIFF_EQUAL: 939 | # Upon reaching an equality, check for prior redundancies. 940 | if count_delete + count_insert > 1: 941 | if count_delete != 0 and count_insert != 0: 942 | # Factor out any common prefixies. 943 | commonlength = self.diff_commonPrefix(text_insert, text_delete) 944 | if commonlength != 0: 945 | x = pointer - count_delete - count_insert - 1 946 | if x >= 0 and diffs[x][0] == self.DIFF_EQUAL: 947 | diffs[x] = (diffs[x][0], diffs[x][1] + 948 | text_insert[:commonlength]) 949 | else: 950 | diffs.insert(0, (self.DIFF_EQUAL, text_insert[:commonlength])) 951 | pointer += 1 952 | text_insert = text_insert[commonlength:] 953 | text_delete = text_delete[commonlength:] 954 | # Factor out any common suffixies. 955 | commonlength = self.diff_commonSuffix(text_insert, text_delete) 956 | if commonlength != 0: 957 | diffs[pointer] = (diffs[pointer][0], text_insert[-commonlength:] + 958 | diffs[pointer][1]) 959 | text_insert = text_insert[:-commonlength] 960 | text_delete = text_delete[:-commonlength] 961 | # Delete the offending records and add the merged ones. 962 | if count_delete == 0: 963 | diffs[pointer - count_insert : pointer] = [ 964 | (self.DIFF_INSERT, text_insert)] 965 | elif count_insert == 0: 966 | diffs[pointer - count_delete : pointer] = [ 967 | (self.DIFF_DELETE, text_delete)] 968 | else: 969 | diffs[pointer - count_delete - count_insert : pointer] = [ 970 | (self.DIFF_DELETE, text_delete), 971 | (self.DIFF_INSERT, text_insert)] 972 | pointer = pointer - count_delete - count_insert + 1 973 | if count_delete != 0: 974 | pointer += 1 975 | if count_insert != 0: 976 | pointer += 1 977 | elif pointer != 0 and diffs[pointer - 1][0] == self.DIFF_EQUAL: 978 | # Merge this equality with the previous one. 979 | diffs[pointer - 1] = (diffs[pointer - 1][0], 980 | diffs[pointer - 1][1] + diffs[pointer][1]) 981 | del diffs[pointer] 982 | else: 983 | pointer += 1 984 | 985 | count_insert = 0 986 | count_delete = 0 987 | text_delete = '' 988 | text_insert = '' 989 | 990 | if diffs[-1][1] == '': 991 | diffs.pop() # Remove the dummy entry at the end. 992 | 993 | # Second pass: look for single edits surrounded on both sides by equalities 994 | # which can be shifted sideways to eliminate an equality. 995 | # e.g: ABAC -> ABAC 996 | changes = False 997 | pointer = 1 998 | # Intentionally ignore the first and last element (don't need checking). 999 | while pointer < len(diffs) - 1: 1000 | if (diffs[pointer - 1][0] == self.DIFF_EQUAL and 1001 | diffs[pointer + 1][0] == self.DIFF_EQUAL): 1002 | # This is a single edit surrounded by equalities. 1003 | if diffs[pointer][1].endswith(diffs[pointer - 1][1]): 1004 | # Shift the edit over the previous equality. 1005 | diffs[pointer] = (diffs[pointer][0], 1006 | diffs[pointer - 1][1] + 1007 | diffs[pointer][1][:-len(diffs[pointer - 1][1])]) 1008 | diffs[pointer + 1] = (diffs[pointer + 1][0], 1009 | diffs[pointer - 1][1] + diffs[pointer + 1][1]) 1010 | del diffs[pointer - 1] 1011 | changes = True 1012 | elif diffs[pointer][1].startswith(diffs[pointer + 1][1]): 1013 | # Shift the edit over the next equality. 1014 | diffs[pointer - 1] = (diffs[pointer - 1][0], 1015 | diffs[pointer - 1][1] + diffs[pointer + 1][1]) 1016 | diffs[pointer] = (diffs[pointer][0], 1017 | diffs[pointer][1][len(diffs[pointer + 1][1]):] + 1018 | diffs[pointer + 1][1]) 1019 | del diffs[pointer + 1] 1020 | changes = True 1021 | pointer += 1 1022 | 1023 | # If shifts were made, the diff needs reordering and another shift sweep. 1024 | if changes: 1025 | self.diff_cleanupMerge(diffs) 1026 | 1027 | def diff_xIndex(self, diffs, loc): 1028 | """loc is a location in text1, compute and return the equivalent location 1029 | in text2. e.g. "The cat" vs "The big cat", 1->1, 5->8 1030 | 1031 | Args: 1032 | diffs: Array of diff tuples. 1033 | loc: Location within text1. 1034 | 1035 | Returns: 1036 | Location within text2. 1037 | """ 1038 | chars1 = 0 1039 | chars2 = 0 1040 | last_chars1 = 0 1041 | last_chars2 = 0 1042 | for x in range(len(diffs)): 1043 | (op, text) = diffs[x] 1044 | if op != self.DIFF_INSERT: # Equality or deletion. 1045 | chars1 += len(text) 1046 | if op != self.DIFF_DELETE: # Equality or insertion. 1047 | chars2 += len(text) 1048 | if chars1 > loc: # Overshot the location. 1049 | break 1050 | last_chars1 = chars1 1051 | last_chars2 = chars2 1052 | 1053 | if len(diffs) != x and diffs[x][0] == self.DIFF_DELETE: 1054 | # The location was deleted. 1055 | return last_chars2 1056 | # Add the remaining len(character). 1057 | return last_chars2 + (loc - last_chars1) 1058 | 1059 | def diff_prettyHtml(self, diffs): 1060 | """Convert a diff array into a pretty HTML report. 1061 | 1062 | Args: 1063 | diffs: Array of diff tuples. 1064 | 1065 | Returns: 1066 | HTML representation. 1067 | """ 1068 | html = [] 1069 | for (op, data) in diffs: 1070 | text = (data.replace("&", "&").replace("<", "<") 1071 | .replace(">", ">").replace("\n", "¶
")) 1072 | if op == self.DIFF_INSERT: 1073 | html.append("%s" % text) 1074 | elif op == self.DIFF_DELETE: 1075 | html.append("%s" % text) 1076 | elif op == self.DIFF_EQUAL: 1077 | html.append("%s" % text) 1078 | return "".join(html) 1079 | 1080 | def diff_text1(self, diffs): 1081 | """Compute and return the source text (all equalities and deletions). 1082 | 1083 | Args: 1084 | diffs: Array of diff tuples. 1085 | 1086 | Returns: 1087 | Source text. 1088 | """ 1089 | text = [] 1090 | for (op, data) in diffs: 1091 | if op != self.DIFF_INSERT: 1092 | text.append(data) 1093 | return "".join(text) 1094 | 1095 | def diff_text2(self, diffs): 1096 | """Compute and return the destination text (all equalities and insertions). 1097 | 1098 | Args: 1099 | diffs: Array of diff tuples. 1100 | 1101 | Returns: 1102 | Destination text. 1103 | """ 1104 | text = [] 1105 | for (op, data) in diffs: 1106 | if op != self.DIFF_DELETE: 1107 | text.append(data) 1108 | return "".join(text) 1109 | 1110 | def diff_levenshtein(self, diffs): 1111 | """Compute the Levenshtein distance; the number of inserted, deleted or 1112 | substituted characters. 1113 | 1114 | Args: 1115 | diffs: Array of diff tuples. 1116 | 1117 | Returns: 1118 | Number of changes. 1119 | """ 1120 | levenshtein = 0 1121 | insertions = 0 1122 | deletions = 0 1123 | for (op, data) in diffs: 1124 | if op == self.DIFF_INSERT: 1125 | insertions += len(data) 1126 | elif op == self.DIFF_DELETE: 1127 | deletions += len(data) 1128 | elif op == self.DIFF_EQUAL: 1129 | # A deletion and an insertion is one substitution. 1130 | levenshtein += max(insertions, deletions) 1131 | insertions = 0 1132 | deletions = 0 1133 | levenshtein += max(insertions, deletions) 1134 | return levenshtein 1135 | 1136 | def diff_toDelta(self, diffs): 1137 | """Crush the diff into an encoded string which describes the operations 1138 | required to transform text1 into text2. 1139 | E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. 1140 | Operations are tab-separated. Inserted text is escaped using %xx notation. 1141 | 1142 | Args: 1143 | diffs: Array of diff tuples. 1144 | 1145 | Returns: 1146 | Delta text. 1147 | """ 1148 | text = [] 1149 | for (op, data) in diffs: 1150 | if op == self.DIFF_INSERT: 1151 | # High ascii will raise UnicodeDecodeError. Use Unicode instead. 1152 | data = data.encode("utf-8") 1153 | text.append("+" + urllib.parse.quote(data, "!~*'();/?:@&=+$,# ")) 1154 | elif op == self.DIFF_DELETE: 1155 | text.append("-%d" % len(data)) 1156 | elif op == self.DIFF_EQUAL: 1157 | text.append("=%d" % len(data)) 1158 | return "\t".join(text) 1159 | 1160 | def diff_fromDelta(self, text1, delta): 1161 | """Given the original text1, and an encoded string which describes the 1162 | operations required to transform text1 into text2, compute the full diff. 1163 | 1164 | Args: 1165 | text1: Source string for the diff. 1166 | delta: Delta text. 1167 | 1168 | Returns: 1169 | Array of diff tuples. 1170 | 1171 | Raises: 1172 | ValueError: If invalid input. 1173 | """ 1174 | diffs = [] 1175 | pointer = 0 # Cursor in text1 1176 | tokens = delta.split("\t") 1177 | for token in tokens: 1178 | if token == "": 1179 | # Blank tokens are ok (from a trailing \t). 1180 | continue 1181 | # Each token begins with a one character parameter which specifies the 1182 | # operation of this token (delete, insert, equality). 1183 | param = token[1:] 1184 | if token[0] == "+": 1185 | param = urllib.parse.unquote(param) 1186 | diffs.append((self.DIFF_INSERT, param)) 1187 | elif token[0] == "-" or token[0] == "=": 1188 | try: 1189 | n = int(param) 1190 | except ValueError: 1191 | raise ValueError("Invalid number in diff_fromDelta: " + param) 1192 | if n < 0: 1193 | raise ValueError("Negative number in diff_fromDelta: " + param) 1194 | text = text1[pointer : pointer + n] 1195 | pointer += n 1196 | if token[0] == "=": 1197 | diffs.append((self.DIFF_EQUAL, text)) 1198 | else: 1199 | diffs.append((self.DIFF_DELETE, text)) 1200 | else: 1201 | # Anything else is an error. 1202 | raise ValueError("Invalid diff operation in diff_fromDelta: " + 1203 | token[0]) 1204 | if pointer != len(text1): 1205 | raise ValueError( 1206 | "Delta length (%d) does not equal source text length (%d)." % 1207 | (pointer, len(text1))) 1208 | return diffs 1209 | 1210 | # MATCH FUNCTIONS 1211 | 1212 | def match_main(self, text, pattern, loc): 1213 | """Locate the best instance of 'pattern' in 'text' near 'loc'. 1214 | 1215 | Args: 1216 | text: The text to search. 1217 | pattern: The pattern to search for. 1218 | loc: The location to search around. 1219 | 1220 | Returns: 1221 | Best match index or -1. 1222 | """ 1223 | # Check for null inputs. 1224 | if text == None or pattern == None: 1225 | raise ValueError("Null inputs. (match_main)") 1226 | 1227 | loc = max(0, min(loc, len(text))) 1228 | if text == pattern: 1229 | # Shortcut (potentially not guaranteed by the algorithm) 1230 | return 0 1231 | elif not text: 1232 | # Nothing to match. 1233 | return -1 1234 | elif text[loc:loc + len(pattern)] == pattern: 1235 | # Perfect match at the perfect spot! (Includes case of null pattern) 1236 | return loc 1237 | else: 1238 | # Do a fuzzy compare. 1239 | match = self.match_bitap(text, pattern, loc) 1240 | return match 1241 | 1242 | def match_bitap(self, text, pattern, loc): 1243 | """Locate the best instance of 'pattern' in 'text' near 'loc' using the 1244 | Bitap algorithm. 1245 | 1246 | Args: 1247 | text: The text to search. 1248 | pattern: The pattern to search for. 1249 | loc: The location to search around. 1250 | 1251 | Returns: 1252 | Best match index or -1. 1253 | """ 1254 | # Python doesn't have a maxint limit, so ignore this check. 1255 | #if self.Match_MaxBits != 0 and len(pattern) > self.Match_MaxBits: 1256 | # raise ValueError("Pattern too long for this application.") 1257 | 1258 | # Initialise the alphabet. 1259 | s = self.match_alphabet(pattern) 1260 | 1261 | def match_bitapScore(e, x): 1262 | """Compute and return the score for a match with e errors and x location. 1263 | Accesses loc and pattern through being a closure. 1264 | 1265 | Args: 1266 | e: Number of errors in match. 1267 | x: Location of match. 1268 | 1269 | Returns: 1270 | Overall score for match (0.0 = good, 1.0 = bad). 1271 | """ 1272 | accuracy = float(e) / len(pattern) 1273 | proximity = abs(loc - x) 1274 | if not self.Match_Distance: 1275 | # Dodge divide by zero error. 1276 | return proximity and 1.0 or accuracy 1277 | return accuracy + (proximity / float(self.Match_Distance)) 1278 | 1279 | # Highest score beyond which we give up. 1280 | score_threshold = self.Match_Threshold 1281 | # Is there a nearby exact match? (speedup) 1282 | best_loc = text.find(pattern, loc) 1283 | if best_loc != -1: 1284 | score_threshold = min(match_bitapScore(0, best_loc), score_threshold) 1285 | # What about in the other direction? (speedup) 1286 | best_loc = text.rfind(pattern, loc + len(pattern)) 1287 | if best_loc != -1: 1288 | score_threshold = min(match_bitapScore(0, best_loc), score_threshold) 1289 | 1290 | # Initialise the bit arrays. 1291 | matchmask = 1 << (len(pattern) - 1) 1292 | best_loc = -1 1293 | 1294 | bin_max = len(pattern) + len(text) 1295 | # Empty initialization added to appease pychecker. 1296 | last_rd = None 1297 | for d in range(len(pattern)): 1298 | # Scan for the best match each iteration allows for one more error. 1299 | # Run a binary search to determine how far from 'loc' we can stray at 1300 | # this error level. 1301 | bin_min = 0 1302 | bin_mid = bin_max 1303 | while bin_min < bin_mid: 1304 | if match_bitapScore(d, loc + bin_mid) <= score_threshold: 1305 | bin_min = bin_mid 1306 | else: 1307 | bin_max = bin_mid 1308 | bin_mid = (bin_max - bin_min) // 2 + bin_min 1309 | 1310 | # Use the result from this iteration as the maximum for the next. 1311 | bin_max = bin_mid 1312 | start = max(1, loc - bin_mid + 1) 1313 | finish = min(loc + bin_mid, len(text)) + len(pattern) 1314 | 1315 | rd = [0] * (finish + 2) 1316 | rd[finish + 1] = (1 << d) - 1 1317 | for j in range(finish, start - 1, -1): 1318 | if len(text) <= j - 1: 1319 | # Out of range. 1320 | charMatch = 0 1321 | else: 1322 | charMatch = s.get(text[j - 1], 0) 1323 | if d == 0: # First pass: exact match. 1324 | rd[j] = ((rd[j + 1] << 1) | 1) & charMatch 1325 | else: # Subsequent passes: fuzzy match. 1326 | rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) | ( 1327 | ((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1] 1328 | if rd[j] & matchmask: 1329 | score = match_bitapScore(d, j - 1) 1330 | # This match will almost certainly be better than any existing match. 1331 | # But check anyway. 1332 | if score <= score_threshold: 1333 | # Told you so. 1334 | score_threshold = score 1335 | best_loc = j - 1 1336 | if best_loc > loc: 1337 | # When passing loc, don't exceed our current distance from loc. 1338 | start = max(1, 2 * loc - best_loc) 1339 | else: 1340 | # Already passed loc, downhill from here on in. 1341 | break 1342 | # No hope for a (better) match at greater error levels. 1343 | if match_bitapScore(d + 1, loc) > score_threshold: 1344 | break 1345 | last_rd = rd 1346 | return best_loc 1347 | 1348 | def match_alphabet(self, pattern): 1349 | """Initialise the alphabet for the Bitap algorithm. 1350 | 1351 | Args: 1352 | pattern: The text to encode. 1353 | 1354 | Returns: 1355 | Hash of character locations. 1356 | """ 1357 | s = {} 1358 | for char in pattern: 1359 | s[char] = 0 1360 | for i in range(len(pattern)): 1361 | s[pattern[i]] |= 1 << (len(pattern) - i - 1) 1362 | return s 1363 | 1364 | # PATCH FUNCTIONS 1365 | 1366 | def patch_addContext(self, patch, text): 1367 | """Increase the context until it is unique, 1368 | but don't let the pattern expand beyond Match_MaxBits. 1369 | 1370 | Args: 1371 | patch: The patch to grow. 1372 | text: Source text. 1373 | """ 1374 | if len(text) == 0: 1375 | return 1376 | pattern = text[patch.start2 : patch.start2 + patch.length1] 1377 | padding = 0 1378 | 1379 | # Look for the first and last matches of pattern in text. If two different 1380 | # matches are found, increase the pattern length. 1381 | while (text.find(pattern) != text.rfind(pattern) and (self.Match_MaxBits == 1382 | 0 or len(pattern) < self.Match_MaxBits - self.Patch_Margin - 1383 | self.Patch_Margin)): 1384 | padding += self.Patch_Margin 1385 | pattern = text[max(0, patch.start2 - padding) : 1386 | patch.start2 + patch.length1 + padding] 1387 | # Add one chunk for good luck. 1388 | padding += self.Patch_Margin 1389 | 1390 | # Add the prefix. 1391 | prefix = text[max(0, patch.start2 - padding) : patch.start2] 1392 | if prefix: 1393 | patch.diffs[:0] = [(self.DIFF_EQUAL, prefix)] 1394 | # Add the suffix. 1395 | suffix = text[patch.start2 + patch.length1 : 1396 | patch.start2 + patch.length1 + padding] 1397 | if suffix: 1398 | patch.diffs.append((self.DIFF_EQUAL, suffix)) 1399 | 1400 | # Roll back the start points. 1401 | patch.start1 -= len(prefix) 1402 | patch.start2 -= len(prefix) 1403 | # Extend lengths. 1404 | patch.length1 += len(prefix) + len(suffix) 1405 | patch.length2 += len(prefix) + len(suffix) 1406 | 1407 | def patch_make(self, a, b=None, c=None): 1408 | """Compute a list of patches to turn text1 into text2. 1409 | Use diffs if provided, otherwise compute it ourselves. 1410 | There are four ways to call this function, depending on what data is 1411 | available to the caller: 1412 | Method 1: 1413 | a = text1, b = text2 1414 | Method 2: 1415 | a = diffs 1416 | Method 3 (optimal): 1417 | a = text1, b = diffs 1418 | Method 4 (deprecated, use method 3): 1419 | a = text1, b = text2, c = diffs 1420 | 1421 | Args: 1422 | a: text1 (methods 1,3,4) or Array of diff tuples for text1 to 1423 | text2 (method 2). 1424 | b: text2 (methods 1,4) or Array of diff tuples for text1 to 1425 | text2 (method 3) or undefined (method 2). 1426 | c: Array of diff tuples for text1 to text2 (method 4) or 1427 | undefined (methods 1,2,3). 1428 | 1429 | Returns: 1430 | Array of Patch objects. 1431 | """ 1432 | text1 = None 1433 | diffs = None 1434 | if isinstance(a, str) and isinstance(b, str) and c is None: 1435 | # Method 1: text1, text2 1436 | # Compute diffs from text1 and text2. 1437 | text1 = a 1438 | diffs = self.diff_main(text1, b, True) 1439 | if len(diffs) > 2: 1440 | self.diff_cleanupSemantic(diffs) 1441 | self.diff_cleanupEfficiency(diffs) 1442 | elif isinstance(a, list) and b is None and c is None: 1443 | # Method 2: diffs 1444 | # Compute text1 from diffs. 1445 | diffs = a 1446 | text1 = self.diff_text1(diffs) 1447 | elif isinstance(a, str) and isinstance(b, list) and c is None: 1448 | # Method 3: text1, diffs 1449 | text1 = a 1450 | diffs = b 1451 | elif (isinstance(a, str) and isinstance(b, str) and 1452 | isinstance(c, list)): 1453 | # Method 4: text1, text2, diffs 1454 | # text2 is not used. 1455 | text1 = a 1456 | diffs = c 1457 | else: 1458 | raise ValueError("Unknown call format to patch_make.") 1459 | 1460 | if not diffs: 1461 | return [] # Get rid of the None case. 1462 | patches = [] 1463 | patch = patch_obj() 1464 | char_count1 = 0 # Number of characters into the text1 string. 1465 | char_count2 = 0 # Number of characters into the text2 string. 1466 | prepatch_text = text1 # Recreate the patches to determine context info. 1467 | postpatch_text = text1 1468 | for x in range(len(diffs)): 1469 | (diff_type, diff_text) = diffs[x] 1470 | if len(patch.diffs) == 0 and diff_type != self.DIFF_EQUAL: 1471 | # A new patch starts here. 1472 | patch.start1 = char_count1 1473 | patch.start2 = char_count2 1474 | if diff_type == self.DIFF_INSERT: 1475 | # Insertion 1476 | patch.diffs.append(diffs[x]) 1477 | patch.length2 += len(diff_text) 1478 | postpatch_text = (postpatch_text[:char_count2] + diff_text + 1479 | postpatch_text[char_count2:]) 1480 | elif diff_type == self.DIFF_DELETE: 1481 | # Deletion. 1482 | patch.length1 += len(diff_text) 1483 | patch.diffs.append(diffs[x]) 1484 | postpatch_text = (postpatch_text[:char_count2] + 1485 | postpatch_text[char_count2 + len(diff_text):]) 1486 | elif (diff_type == self.DIFF_EQUAL and 1487 | len(diff_text) <= 2 * self.Patch_Margin and 1488 | len(patch.diffs) != 0 and len(diffs) != x + 1): 1489 | # Small equality inside a patch. 1490 | patch.diffs.append(diffs[x]) 1491 | patch.length1 += len(diff_text) 1492 | patch.length2 += len(diff_text) 1493 | 1494 | if (diff_type == self.DIFF_EQUAL and 1495 | len(diff_text) >= 2 * self.Patch_Margin): 1496 | # Time for a new patch. 1497 | if len(patch.diffs) != 0: 1498 | self.patch_addContext(patch, prepatch_text) 1499 | patches.append(patch) 1500 | patch = patch_obj() 1501 | # Unlike Unidiff, our patch lists have a rolling context. 1502 | # http://code.google.com/p/google-diff-match-patch/wiki/Unidiff 1503 | # Update prepatch text & pos to reflect the application of the 1504 | # just completed patch. 1505 | prepatch_text = postpatch_text 1506 | char_count1 = char_count2 1507 | 1508 | # Update the current character count. 1509 | if diff_type != self.DIFF_INSERT: 1510 | char_count1 += len(diff_text) 1511 | if diff_type != self.DIFF_DELETE: 1512 | char_count2 += len(diff_text) 1513 | 1514 | # Pick up the leftover patch if not empty. 1515 | if len(patch.diffs) != 0: 1516 | self.patch_addContext(patch, prepatch_text) 1517 | patches.append(patch) 1518 | return patches 1519 | 1520 | def patch_deepCopy(self, patches): 1521 | """Given an array of patches, return another array that is identical. 1522 | 1523 | Args: 1524 | patches: Array of Patch objects. 1525 | 1526 | Returns: 1527 | Array of Patch objects. 1528 | """ 1529 | patchesCopy = [] 1530 | for patch in patches: 1531 | patchCopy = patch_obj() 1532 | # No need to deep copy the tuples since they are immutable. 1533 | patchCopy.diffs = patch.diffs[:] 1534 | patchCopy.start1 = patch.start1 1535 | patchCopy.start2 = patch.start2 1536 | patchCopy.length1 = patch.length1 1537 | patchCopy.length2 = patch.length2 1538 | patchesCopy.append(patchCopy) 1539 | return patchesCopy 1540 | 1541 | def patch_apply(self, patches, text): 1542 | """Merge a set of patches onto the text. Return a patched text, as well 1543 | as a list of true/false values indicating which patches were applied. 1544 | 1545 | Args: 1546 | patches: Array of Patch objects. 1547 | text: Old text. 1548 | 1549 | Returns: 1550 | Two element Array, containing the new text and an array of boolean values. 1551 | """ 1552 | if not patches: 1553 | return (text, []) 1554 | 1555 | # Deep copy the patches so that no changes are made to originals. 1556 | patches = self.patch_deepCopy(patches) 1557 | 1558 | nullPadding = self.patch_addPadding(patches) 1559 | text = nullPadding + text + nullPadding 1560 | self.patch_splitMax(patches) 1561 | 1562 | # delta keeps track of the offset between the expected and actual location 1563 | # of the previous patch. If there are patches expected at positions 10 and 1564 | # 20, but the first patch was found at 12, delta is 2 and the second patch 1565 | # has an effective expected position of 22. 1566 | delta = 0 1567 | results = [] 1568 | for patch in patches: 1569 | expected_loc = patch.start2 + delta 1570 | text1 = self.diff_text1(patch.diffs) 1571 | end_loc = -1 1572 | if len(text1) > self.Match_MaxBits: 1573 | # patch_splitMax will only provide an oversized pattern in the case of 1574 | # a monster delete. 1575 | start_loc = self.match_main(text, text1[:self.Match_MaxBits], 1576 | expected_loc) 1577 | if start_loc != -1: 1578 | end_loc = self.match_main(text, text1[-self.Match_MaxBits:], 1579 | expected_loc + len(text1) - self.Match_MaxBits) 1580 | if end_loc == -1 or start_loc >= end_loc: 1581 | # Can't find valid trailing context. Drop this patch. 1582 | start_loc = -1 1583 | else: 1584 | start_loc = self.match_main(text, text1, expected_loc) 1585 | if start_loc == -1: 1586 | # No match found. :( 1587 | results.append(False) 1588 | # Subtract the delta for this failed patch from subsequent patches. 1589 | delta -= patch.length2 - patch.length1 1590 | else: 1591 | # Found a match. :) 1592 | results.append(True) 1593 | delta = start_loc - expected_loc 1594 | if end_loc == -1: 1595 | text2 = text[start_loc : start_loc + len(text1)] 1596 | else: 1597 | text2 = text[start_loc : end_loc + self.Match_MaxBits] 1598 | if text1 == text2: 1599 | # Perfect match, just shove the replacement text in. 1600 | text = (text[:start_loc] + self.diff_text2(patch.diffs) + 1601 | text[start_loc + len(text1):]) 1602 | else: 1603 | # Imperfect match. 1604 | # Run a diff to get a framework of equivalent indices. 1605 | diffs = self.diff_main(text1, text2, False) 1606 | if (len(text1) > self.Match_MaxBits and 1607 | self.diff_levenshtein(diffs) / float(len(text1)) > 1608 | self.Patch_DeleteThreshold): 1609 | # The end points match, but the content is unacceptably bad. 1610 | results[-1] = False 1611 | else: 1612 | self.diff_cleanupSemanticLossless(diffs) 1613 | index1 = 0 1614 | for (op, data) in patch.diffs: 1615 | if op != self.DIFF_EQUAL: 1616 | index2 = self.diff_xIndex(diffs, index1) 1617 | if op == self.DIFF_INSERT: # Insertion 1618 | text = text[:start_loc + index2] + data + text[start_loc + 1619 | index2:] 1620 | elif op == self.DIFF_DELETE: # Deletion 1621 | text = text[:start_loc + index2] + text[start_loc + 1622 | self.diff_xIndex(diffs, index1 + len(data)):] 1623 | if op != self.DIFF_DELETE: 1624 | index1 += len(data) 1625 | # Strip the padding off. 1626 | text = text[len(nullPadding):-len(nullPadding)] 1627 | return (text, results) 1628 | 1629 | def patch_addPadding(self, patches): 1630 | """Add some padding on text start and end so that edges can match 1631 | something. Intended to be called only from within patch_apply. 1632 | 1633 | Args: 1634 | patches: Array of Patch objects. 1635 | 1636 | Returns: 1637 | The padding string added to each side. 1638 | """ 1639 | paddingLength = self.Patch_Margin 1640 | nullPadding = "" 1641 | for x in range(1, paddingLength + 1): 1642 | nullPadding += chr(x) 1643 | 1644 | # Bump all the patches forward. 1645 | for patch in patches: 1646 | patch.start1 += paddingLength 1647 | patch.start2 += paddingLength 1648 | 1649 | # Add some padding on start of first diff. 1650 | patch = patches[0] 1651 | diffs = patch.diffs 1652 | if not diffs or diffs[0][0] != self.DIFF_EQUAL: 1653 | # Add nullPadding equality. 1654 | diffs.insert(0, (self.DIFF_EQUAL, nullPadding)) 1655 | patch.start1 -= paddingLength # Should be 0. 1656 | patch.start2 -= paddingLength # Should be 0. 1657 | patch.length1 += paddingLength 1658 | patch.length2 += paddingLength 1659 | elif paddingLength > len(diffs[0][1]): 1660 | # Grow first equality. 1661 | extraLength = paddingLength - len(diffs[0][1]) 1662 | newText = nullPadding[len(diffs[0][1]):] + diffs[0][1] 1663 | diffs[0] = (diffs[0][0], newText) 1664 | patch.start1 -= extraLength 1665 | patch.start2 -= extraLength 1666 | patch.length1 += extraLength 1667 | patch.length2 += extraLength 1668 | 1669 | # Add some padding on end of last diff. 1670 | patch = patches[-1] 1671 | diffs = patch.diffs 1672 | if not diffs or diffs[-1][0] != self.DIFF_EQUAL: 1673 | # Add nullPadding equality. 1674 | diffs.append((self.DIFF_EQUAL, nullPadding)) 1675 | patch.length1 += paddingLength 1676 | patch.length2 += paddingLength 1677 | elif paddingLength > len(diffs[-1][1]): 1678 | # Grow last equality. 1679 | extraLength = paddingLength - len(diffs[-1][1]) 1680 | newText = diffs[-1][1] + nullPadding[:extraLength] 1681 | diffs[-1] = (diffs[-1][0], newText) 1682 | patch.length1 += extraLength 1683 | patch.length2 += extraLength 1684 | 1685 | return nullPadding 1686 | 1687 | def patch_splitMax(self, patches): 1688 | """Look through the patches and break up any which are longer than the 1689 | maximum limit of the match algorithm. 1690 | Intended to be called only from within patch_apply. 1691 | 1692 | Args: 1693 | patches: Array of Patch objects. 1694 | """ 1695 | patch_size = self.Match_MaxBits 1696 | if patch_size == 0: 1697 | # Python has the option of not splitting strings due to its ability 1698 | # to handle integers of arbitrary precision. 1699 | return 1700 | for x in range(len(patches)): 1701 | if patches[x].length1 <= patch_size: 1702 | continue 1703 | bigpatch = patches[x] 1704 | # Remove the big old patch. 1705 | del patches[x] 1706 | x -= 1 1707 | start1 = bigpatch.start1 1708 | start2 = bigpatch.start2 1709 | precontext = '' 1710 | while len(bigpatch.diffs) != 0: 1711 | # Create one of several smaller patches. 1712 | patch = patch_obj() 1713 | empty = True 1714 | patch.start1 = start1 - len(precontext) 1715 | patch.start2 = start2 - len(precontext) 1716 | if precontext: 1717 | patch.length1 = patch.length2 = len(precontext) 1718 | patch.diffs.append((self.DIFF_EQUAL, precontext)) 1719 | 1720 | while (len(bigpatch.diffs) != 0 and 1721 | patch.length1 < patch_size - self.Patch_Margin): 1722 | (diff_type, diff_text) = bigpatch.diffs[0] 1723 | if diff_type == self.DIFF_INSERT: 1724 | # Insertions are harmless. 1725 | patch.length2 += len(diff_text) 1726 | start2 += len(diff_text) 1727 | patch.diffs.append(bigpatch.diffs.pop(0)) 1728 | empty = False 1729 | elif (diff_type == self.DIFF_DELETE and len(patch.diffs) == 1 and 1730 | patch.diffs[0][0] == self.DIFF_EQUAL and 1731 | len(diff_text) > 2 * patch_size): 1732 | # This is a large deletion. Let it pass in one chunk. 1733 | patch.length1 += len(diff_text) 1734 | start1 += len(diff_text) 1735 | empty = False 1736 | patch.diffs.append((diff_type, diff_text)) 1737 | del bigpatch.diffs[0] 1738 | else: 1739 | # Deletion or equality. Only take as much as we can stomach. 1740 | diff_text = diff_text[:patch_size - patch.length1 - 1741 | self.Patch_Margin] 1742 | patch.length1 += len(diff_text) 1743 | start1 += len(diff_text) 1744 | if diff_type == self.DIFF_EQUAL: 1745 | patch.length2 += len(diff_text) 1746 | start2 += len(diff_text) 1747 | else: 1748 | empty = False 1749 | 1750 | patch.diffs.append((diff_type, diff_text)) 1751 | if diff_text == bigpatch.diffs[0][1]: 1752 | del bigpatch.diffs[0] 1753 | else: 1754 | bigpatch.diffs[0] = (bigpatch.diffs[0][0], 1755 | bigpatch.diffs[0][1][len(diff_text):]) 1756 | 1757 | # Compute the head context for the next patch. 1758 | precontext = self.diff_text2(patch.diffs) 1759 | precontext = precontext[-self.Patch_Margin:] 1760 | # Append the end context for this patch. 1761 | postcontext = self.diff_text1(bigpatch.diffs)[:self.Patch_Margin] 1762 | if postcontext: 1763 | patch.length1 += len(postcontext) 1764 | patch.length2 += len(postcontext) 1765 | if len(patch.diffs) != 0 and patch.diffs[-1][0] == self.DIFF_EQUAL: 1766 | patch.diffs[-1] = (self.DIFF_EQUAL, patch.diffs[-1][1] + 1767 | postcontext) 1768 | else: 1769 | patch.diffs.append((self.DIFF_EQUAL, postcontext)) 1770 | 1771 | if not empty: 1772 | x += 1 1773 | patches.insert(x, patch) 1774 | 1775 | def patch_toText(self, patches): 1776 | """Take a list of patches and return a textual representation. 1777 | 1778 | Args: 1779 | patches: Array of Patch objects. 1780 | 1781 | Returns: 1782 | Text representation of patches. 1783 | """ 1784 | text = [] 1785 | for patch in patches: 1786 | text.append(str(patch)) 1787 | return "".join(text) 1788 | 1789 | def patch_fromText(self, textline): 1790 | """Parse a textual representation of patches and return a list of patch 1791 | objects. 1792 | 1793 | Args: 1794 | textline: Text representation of patches. 1795 | 1796 | Returns: 1797 | Array of Patch objects. 1798 | 1799 | Raises: 1800 | ValueError: If invalid input. 1801 | """ 1802 | patches = [] 1803 | if not textline: 1804 | return patches 1805 | text = textline.split('\n') 1806 | while len(text) != 0: 1807 | m = re.match("^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$", text[0]) 1808 | if not m: 1809 | raise ValueError("Invalid patch string: " + text[0]) 1810 | patch = patch_obj() 1811 | patches.append(patch) 1812 | patch.start1 = int(m.group(1)) 1813 | if m.group(2) == '': 1814 | patch.start1 -= 1 1815 | patch.length1 = 1 1816 | elif m.group(2) == '0': 1817 | patch.length1 = 0 1818 | else: 1819 | patch.start1 -= 1 1820 | patch.length1 = int(m.group(2)) 1821 | 1822 | patch.start2 = int(m.group(3)) 1823 | if m.group(4) == '': 1824 | patch.start2 -= 1 1825 | patch.length2 = 1 1826 | elif m.group(4) == '0': 1827 | patch.length2 = 0 1828 | else: 1829 | patch.start2 -= 1 1830 | patch.length2 = int(m.group(4)) 1831 | 1832 | del text[0] 1833 | 1834 | while len(text) != 0: 1835 | if text[0]: 1836 | sign = text[0][0] 1837 | else: 1838 | sign = '' 1839 | line = urllib.parse.unquote(text[0][1:]) 1840 | if sign == '+': 1841 | # Insertion. 1842 | patch.diffs.append((self.DIFF_INSERT, line)) 1843 | elif sign == '-': 1844 | # Deletion. 1845 | patch.diffs.append((self.DIFF_DELETE, line)) 1846 | elif sign == ' ': 1847 | # Minor equality. 1848 | patch.diffs.append((self.DIFF_EQUAL, line)) 1849 | elif sign == '@': 1850 | # Start of next patch. 1851 | break 1852 | elif sign == '': 1853 | # Blank line? Whatever. 1854 | pass 1855 | else: 1856 | # WTF? 1857 | raise ValueError("Invalid patch mode: '%s'\n%s" % (sign, line)) 1858 | del text[0] 1859 | return patches 1860 | 1861 | 1862 | class patch_obj: 1863 | """Class representing one patch operation. 1864 | """ 1865 | 1866 | def __init__(self): 1867 | """Initializes with an empty list of diffs. 1868 | """ 1869 | self.diffs = [] 1870 | self.start1 = None 1871 | self.start2 = None 1872 | self.length1 = 0 1873 | self.length2 = 0 1874 | 1875 | def __str__(self): 1876 | """Emmulate GNU diff's format. 1877 | Header: @@ -382,8 +481,9 @@ 1878 | Indicies are printed as 1-based, not 0-based. 1879 | 1880 | Returns: 1881 | The GNU diff string. 1882 | """ 1883 | if self.length1 == 0: 1884 | coords1 = str(self.start1) + ",0" 1885 | elif self.length1 == 1: 1886 | coords1 = str(self.start1 + 1) 1887 | else: 1888 | coords1 = str(self.start1 + 1) + "," + str(self.length1) 1889 | if self.length2 == 0: 1890 | coords2 = str(self.start2) + ",0" 1891 | elif self.length2 == 1: 1892 | coords2 = str(self.start2 + 1) 1893 | else: 1894 | coords2 = str(self.start2 + 1) + "," + str(self.length2) 1895 | text = ["@@ -", coords1, " +", coords2, " @@\n"] 1896 | # Escape the body of the patch with %xx notation. 1897 | for (op, data) in self.diffs: 1898 | if op == diff_match_patch.DIFF_INSERT: 1899 | text.append("+") 1900 | elif op == diff_match_patch.DIFF_DELETE: 1901 | text.append("-") 1902 | elif op == diff_match_patch.DIFF_EQUAL: 1903 | text.append(" ") 1904 | # High ascii will raise UnicodeDecodeError. Use Unicode instead. 1905 | data = data.encode("utf-8") 1906 | text.append(urllib.parse.quote(data, "!~*'();/?:@&=+$,# ") + "\n") 1907 | return "".join(text) 1908 | -------------------------------------------------------------------------------- /fmt.phar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanch/phpfmt_stable/98de6272d5dd98667a49d7d2f831b13dfac7838c/fmt.phar -------------------------------------------------------------------------------- /generateReadme.php: -------------------------------------------------------------------------------- 1 | $strCommands, 24 | '%PASSES%' => $strPasses, 25 | ]) 26 | ); -------------------------------------------------------------------------------- /message: -------------------------------------------------------------------------------- 1 | Autocomplete and autoimport need building a database of terms. 2 | Please, fill in below with the proper location for this database. 3 | Keep in mind that the best location is always the root of the project, therefore limiting the size and ensuring speed. -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt", 3 | "1.14.0": "messages/1.14.0.txt", 4 | "1.14.1": "messages/1.14.1.txt", 5 | "1.14.2": "messages/1.14.2.txt", 6 | "1.14.3": "messages/1.14.3.txt", 7 | "1.15.0": "messages/1.15.0.txt", 8 | "1.16.3": "messages/1.16.3.txt", 9 | "1.17.0": "messages/1.17.0.txt", 10 | "1.18.2": "messages/1.18.2.txt", 11 | "1.19.0": "messages/1.19.0.txt", 12 | "1.22.0": "messages/1.22.0.txt", 13 | "1.25.0": "messages/1.25.0.txt", 14 | "1.26.0": "messages/1.26.0.txt", 15 | "1.59.0": "messages/1.59.0.txt", 16 | "2.13.0": "messages/2.13.0.txt", 17 | "3.6.0": "messages/3.6.0.txt", 18 | "3.9.0": "messages/3.9.0.txt", 19 | "3.14.0": "messages/3.14.0.txt", 20 | "3.21.0": "messages/3.21.0.txt", 21 | "3.27.0": "messages/3.27.0.txt", 22 | "4.0.1": "messages/4.0.1.txt", 23 | "4.4.0": "messages/4.4.0.txt", 24 | "4.23.0": "messages/4.23.0.txt", 25 | "5.0.3": "messages/5.0.3.txt", 26 | "5.0.7": "messages/5.0.7.txt", 27 | "5.1.0": "messages/5.1.0.txt", 28 | "5.2.0": "messages/5.2.0.txt", 29 | "6.2.0": "messages/6.2.0.txt", 30 | "9.3.0": "messages/9.3.0.txt", 31 | "9.13.0": "messages/9.3.0.txt", 32 | "11.0.0": "messages/11.0.0.txt", 33 | "12.0.0": "messages/12.0.0.txt", 34 | "12.1.0": "messages/12.1.0.txt" 35 | } -------------------------------------------------------------------------------- /messages/1.14.0.txt: -------------------------------------------------------------------------------- 1 | php.fmt 2 | ======= 3 | 4 | Thank you for upgrading. 5 | 6 | New feature added in command palette: 7 | "phpfmt: toggle linebreak between methods" 8 | 9 | From: 10 | class A { 11 | function a(){ 12 | } 13 | function b(){ 14 | } 15 | } 16 | 17 | To: 18 | class A { 19 | function a(){ 20 | } 21 | 22 | function b(){ 23 | } 24 | } 25 | 26 | 27 | - If you find anything wrong with this update, please report an issue at https://github.com/dericofilho/sublime-phpfmt/issues 28 | 29 | - If you like what this plugin does for you, please consider starring at https://github.com/dericofilho/sublime-phpfmt or https://github.com/dericofilho/php.tools 30 | 31 | -------------------------------------------------------------------------------- /messages/1.14.1.txt: -------------------------------------------------------------------------------- 1 | php.fmt 2 | ======= 3 | 4 | Thank you for upgrading. 5 | 6 | New feature added in command palette: 7 | "phpfmt: toggle linebreak between methods" 8 | 9 | From: 10 | class A { 11 | function a(){ 12 | } 13 | function b(){ 14 | } 15 | } 16 | 17 | To: 18 | class A { 19 | function a(){ 20 | } 21 | 22 | function b(){ 23 | } 24 | } 25 | 26 | Bugfix: 27 | Duplicated semi-colons are now removed. 28 | From: 29 | 14 | 19 | 20 | 21 | To: 22 |
23 | 28 |
29 | --- 30 | 31 | - If you find anything wrong with this update, please report an issue at https://github.com/dericofilho/sublime-phpfmt/issues 32 | 33 | - If you like what this plugin does for you, please consider starring at https://github.com/dericofilho/sublime-phpfmt or https://github.com/dericofilho/php.tools 34 | 35 | -------------------------------------------------------------------------------- /messages/3.21.0.txt: -------------------------------------------------------------------------------- 1 | php.fmt 2 | ======= 3 | 4 | Thank you for upgrading. 5 | 6 | New feature added in configuration file: 7 | { 8 | "passes": ["PSR2MultilineFunctionParams"], 9 | } 10 | 11 | 12 | //From: 13 | function a($a) 14 | { 15 | return false; 16 | } 17 | function b($a, $b, $c) 18 | { 19 | return true; 20 | } 21 | 22 | // To 23 | function a($a) 24 | { 25 | return false; 26 | } 27 | function b( 28 | $a, 29 | $b, 30 | $c 31 | ) { 32 | return true; 33 | } 34 | 35 | --- 36 | 37 | New feature added in configuration file: 38 | { 39 | "passes": ["SpaceAroundControlStructures"], 40 | } 41 | 42 | 43 | //From: 44 | if($a){ 45 | 46 | } 47 | do { 48 | 49 | }while($a); 50 | 51 | //To: 52 | if($a){ 53 | 54 | } 55 | 56 | do { 57 | 58 | }while($a); 59 | 60 | --- 61 | 62 | - If you find anything wrong with this update, please report an issue at https://github.com/dericofilho/sublime-phpfmt/issues 63 | 64 | - If you like what this plugin does for you, please consider starring at https://github.com/dericofilho/sublime-phpfmt or https://github.com/dericofilho/php.tools 65 | 66 | -------------------------------------------------------------------------------- /messages/3.27.0.txt: -------------------------------------------------------------------------------- 1 | php.fmt 2 | ======= 3 | 4 | Thank you for upgrading. 5 | 6 | New feature added in configuration file: 7 | { 8 | "passes": ["AutoSemicolon"], 9 | } 10 | 11 | 12 | //From: 13 | echo $a 14 | 15 | echo $a + $b 16 | 17 | // To 18 | echo $a; 19 | 20 | echo $a + $b; 21 | 22 | Note: this feature is still in beta. Use it carefully. 23 | 24 | --- 25 | 26 | - If you find anything wrong with this update, please report an issue at https://github.com/dericofilho/sublime-phpfmt/issues 27 | 28 | - If you like what this plugin does for you, please consider starring at https://github.com/dericofilho/sublime-phpfmt or https://github.com/dericofilho/php.tools 29 | 30 | -------------------------------------------------------------------------------- /messages/3.6.0.txt: -------------------------------------------------------------------------------- 1 | php.fmt 2 | ======= 3 | 4 | - If you find anything wrong with this update, or if you have any difficulty in making it work, please report an issue at https://github.com/dericofilho/sublime-phpfmt/issues 5 | 6 | - If you like what this plugin does for you, please consider starring at https://github.com/dericofilho/sublime-phpfmt or https://github.com/dericofilho/php.tools 7 | 8 | -------------------------------------------------------------------------------- /messages/3.9.0.txt: -------------------------------------------------------------------------------- 1 | php.fmt 2 | ======= 3 | 4 | 5 | Hi, 6 | 7 | 8 | Thanks for using sublime-phpfmt. I use it almost everyday and I take care of it as one of the most important tools I have. I am not alone in using it, I know you are using it too. 9 | 10 | I want to issue a major release removing unnecessary features, refactoring the plugin code and improving performance wherever is possible. 11 | 12 | In order to do that, I need your help. Please, fill this survey in which I will ask you what is that you use or do not use. 13 | 14 | http://goo.gl/forms/kNzIeqnG4L 15 | 16 | 17 | Thanks, 18 | @dericofilho 19 | 20 | --- 21 | 22 | - If you find anything wrong with this update, please report an issue at https://github.com/dericofilho/sublime-phpfmt/issues 23 | 24 | - If you like what this plugin does for you, please consider starring at https://github.com/dericofilho/sublime-phpfmt or https://github.com/dericofilho/php.tools 25 | 26 | -------------------------------------------------------------------------------- /messages/4.0.0.txt: -------------------------------------------------------------------------------- 1 | php.fmt 2 | ======= 3 | 4 | Thank you for upgrading. 5 | 6 | PHP 5.5 reached end-of-life and is no longer supported. 7 | 8 | Please, upgrade your local PHP installation to PHP 5.6 or newer. 9 | 10 | --- 11 | 12 | - If you find anything wrong with this update, please report an issue at https://github.com/dericofilho/sublime-phpfmt/issues 13 | 14 | - If you like what this plugin does for you, please consider starring at https://github.com/dericofilho/sublime-phpfmt or https://github.com/dericofilho/php.tools 15 | 16 | -------------------------------------------------------------------------------- /messages/4.0.1.txt: -------------------------------------------------------------------------------- 1 | php.fmt 2 | ======= 3 | 4 | Thank you for upgrading. 5 | 6 | PHP 5.5 reached end-of-life and is no longer supported. 7 | 8 | Please, upgrade your local PHP installation to PHP 5.6 or newer. 9 | 10 | If you do not want to upgrade your local PHP installation, 11 | please consider installing PHPCS with PHP-CS-Fixer. 12 | 13 | --- 14 | 15 | - If you find anything wrong with this update, please report an issue at https://github.com/dericofilho/sublime-phpfmt/issues 16 | 17 | - If you like what this plugin does for you, please consider starring at https://github.com/dericofilho/sublime-phpfmt or https://github.com/dericofilho/php.tools 18 | 19 | -------------------------------------------------------------------------------- /messages/4.23.0.txt: -------------------------------------------------------------------------------- 1 | php.fmt 2 | ======= 3 | 4 | Call to arms: 5 | 6 | (Un)fortunatelly, I have been coding less and less in PHP. I understand 7 | there is a small community around both sublime-phpfmt and php.tools. 8 | 9 | Thus, I am calling all people interested in these tools to help me to 10 | keep them alive. 11 | 12 | Right now, I do not have any documentation to help you to provide support 13 | and to propose PRs. But as soon as I see people taking action, I shall 14 | help them to write such documentation and implement improvements on these 15 | tools. 16 | 17 | Eventually, I plan to move both projects to an independent account and 18 | to have it run by a maintenance team. 19 | 20 | Please! Take action and support these projects. 21 | 22 | --- 23 | 24 | - If you find anything wrong with this update, please report an issue at https://github.com/dericofilho/sublime-phpfmt/issues 25 | 26 | - If you like what this plugin does for you, please consider starring at https://github.com/dericofilho/sublime-phpfmt or https://github.com/dericofilho/php.tools 27 | 28 | -------------------------------------------------------------------------------- /messages/4.4.0.txt: -------------------------------------------------------------------------------- 1 | php.fmt 2 | ======= 3 | 4 | Thank you for upgrading. 5 | 6 | Introducing PHP 5.5 compatibility mode. 7 | 8 | Activate it through command palette: 9 | `phpfmt: toggle PHP 5.5 compatibility mode`. 10 | 11 | 12 | This is a backwards compatible mode with PHP 5.5 - however no further 13 | improvements will be available in it. 14 | 15 | --- 16 | 17 | - If you find anything wrong with this update, please report an issue at https://github.com/dericofilho/sublime-phpfmt/issues 18 | 19 | - If you like what this plugin does for you, please consider starring at https://github.com/dericofilho/sublime-phpfmt or https://github.com/dericofilho/php.tools 20 | 21 | -------------------------------------------------------------------------------- /messages/5.0.3.txt: -------------------------------------------------------------------------------- 1 | php.fmt 2 | ======= 3 | 4 | Thank you for upgrading. 5 | 6 | New feature added in configuration file: 7 | { 8 | "passes": ["OrderMethodAndVisibility"], 9 | } 10 | 11 | 12 | //From: 13 | class A { 14 | public function d(){} 15 | protected function b(){} 16 | private function c(){} 17 | public function a(){} 18 | } 19 | 20 | // To 21 | class A { 22 | public function a() {} 23 | public function d() {} 24 | protected function b() {} 25 | private function c() {} 26 | } 27 | 28 | --- 29 | 30 | - If you find anything wrong with this update, please report an issue at https://github.com/dericofilho/sublime-phpfmt/issues 31 | 32 | - If you like what this plugin does for you, please consider starring at https://github.com/dericofilho/sublime-phpfmt or https://github.com/dericofilho/php.tools 33 | 34 | -------------------------------------------------------------------------------- /messages/5.0.7.txt: -------------------------------------------------------------------------------- 1 | php.fmt 2 | ======= 3 | 4 | Thank you for upgrading. 5 | 6 | 7 | php.fmt now is under the management of https://github.com/phpfmt/ 8 | 9 | This is the first step to handover this plugin to the community. 10 | 11 | Eventually, I shall stop altogether providing updates and fixes. So, if 12 | you use and care about this plugin, please get involved in the 13 | development of https://github.com/phpfmt/php.tools and 14 | https://github.com/phpfmt/sublime-phpfmt. 15 | 16 | 17 | --- 18 | 19 | - If you find anything wrong with this update, please report an issue at 20 | https://github.com/phpfmt/php.tools/issues 21 | 22 | -------------------------------------------------------------------------------- /messages/5.1.0.txt: -------------------------------------------------------------------------------- 1 | php.fmt 2 | ======= 3 | 4 | Thank you for upgrading. This is an important fix for OrderMethod and 5 | OrderMethodVisibility. 6 | 7 | In previous releases, the ordering of methods did not drag along 8 | T_DOC_COMMENTS of methods. Thus: 9 | 10 | // From: 11 | class A { 12 | /** 13 | * comment for D 14 | */ 15 | public function d(){} 16 | /** 17 | * comment for B 18 | */ 19 | protected function b(){} 20 | /** 21 | * comment for C 22 | */ 23 | private function c(){} 24 | /** 25 | * comment for A 26 | */ 27 | public function a(){} 28 | } 29 | 30 | // Wrongly corrected to 31 | class A { 32 | /** 33 | * comment for D 34 | */ 35 | public function a() {} 36 | /** 37 | * comment for B 38 | */ 39 | public function d() {} 40 | /** 41 | * comment for C 42 | */ 43 | protected function b() {} 44 | /** 45 | * comment for A 46 | */ 47 | private function c() {} 48 | } 49 | 50 | 51 | With this fix, it will correctly reorder to: 52 | 53 | class A { 54 | /** 55 | * comment for A 56 | */ 57 | public function a() {} 58 | /** 59 | * comment for D 60 | */ 61 | public function d() {} 62 | /** 63 | * comment for B 64 | */ 65 | protected function b() {} 66 | /** 67 | * comment for C 68 | */ 69 | private function c() {} 70 | } 71 | 72 | 73 | --- 74 | 75 | - If you find anything wrong with this update, please report an issue at 76 | https://github.com/phpfmt/php.tools/issues 77 | 78 | -------------------------------------------------------------------------------- /messages/5.2.0.txt: -------------------------------------------------------------------------------- 1 | php.fmt 2 | ======= 3 | 4 | Thank you for upgrading. 5 | 6 | New feature added in configuration file: 7 | { 8 | "passes": ["OrganizeClass"], 9 | } 10 | 11 | // From 12 | class A { 13 | public function d(){} 14 | protected function b(){} 15 | private $a = ""; 16 | private function c(){} 17 | public function a(){} 18 | public $b = ""; 19 | const B = 0; 20 | const A = 0; 21 | } 22 | 23 | // To 24 | class A { 25 | const A = 0; 26 | 27 | const B = 0; 28 | 29 | public $b = ""; 30 | 31 | private $a = ""; 32 | 33 | public function a(){} 34 | 35 | public function d(){} 36 | 37 | protected function b(){} 38 | 39 | private function c(){} 40 | } 41 | 42 | --- 43 | 44 | - If you find anything wrong with this update, please report an issue at 45 | https://github.com/phpfmt/php.tools/issues 46 | 47 | -------------------------------------------------------------------------------- /messages/6.2.0.txt: -------------------------------------------------------------------------------- 1 | Thanks for upgrading this plugin. 2 | 3 | As announced in v5.0.7, as of this release, I shall keep occasional updates both 4 | for this plugin and its engine. Therefore, I call the community of their users to 5 | support both with Pull Requests and answering at Issue Tracker. 6 | 7 | I am ready to support anyone willing to step up to become contributors of these 8 | projects. Just ping @ccirello in Issue Tracker. 9 | -------------------------------------------------------------------------------- /messages/9.13.0.txt: -------------------------------------------------------------------------------- 1 | New feature added in configuration file: 2 | { 3 | "passes": ["PHPDocTypesToFunctionTypehint"], 4 | } 5 | 6 | // From: 7 | /** 8 | * @param int $a 9 | * @param int $b 10 | * @return int 11 | */ 12 | function abc($a = 10, $b = 20, $c) { 13 | 14 | } 15 | 16 | // To: 17 | /** 18 | * @param int $a 19 | * @param int $b 20 | * @return int 21 | */ 22 | function abc(int $a = 10, int $b = 20, $c): int { 23 | 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /messages/9.3.0.txt: -------------------------------------------------------------------------------- 1 | Thanks for upgrading this plugin. 2 | 3 | OrderMethod and OrderMethodAndVisibility are deprecated in favor of OrganizeClass. 4 | In the next major release, they will be automatically replaced with OrganizeClass. -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | # [phpfmt](https://github.com/phpfmt/fmt) support for Sublime Text 3 2 | 3 | 4 | Thanks for installing phpfmt for Sublime Text. 5 | 6 | Please, before posting an issue that this plugin is not formatting your code, answer the following questions: 7 | 8 | - Have you installed PHP in the computer which is running Sublime Text? 9 | 10 | - Is PHP configured in the default $PATH/%PATH% variable? 11 | 12 | - If PHP is not configured in $PATH/%PATH%, have you added the option "php_bin" in the configuration file with full path of PHP binary? 13 | 14 | - Are you running at least PHP 5.6? 15 | 16 | If you have answered more than one "no", then double check your environment. 17 | 18 | XDebug makes this plugin to work much slower than normal. Consider disabling it before actually using phpfmt. 19 | -------------------------------------------------------------------------------- /oracle.php: -------------------------------------------------------------------------------- 1 | "); 27 | define("ST_IS_SMALLER", "<"); 28 | define("ST_MINUS", "-"); 29 | define("ST_MODULUS", "%"); 30 | define("ST_PARENTHESES_CLOSE", ")"); 31 | define("ST_PARENTHESES_OPEN", "("); 32 | define("ST_PLUS", "+"); 33 | define("ST_QUESTION", "?"); 34 | define("ST_QUOTE", '"'); 35 | define("ST_REFERENCE", "&"); 36 | define("ST_SEMI_COLON", ";"); 37 | define("ST_TIMES", "*"); 38 | define("ST_BITWISE_OR", "|"); 39 | define("ST_BITWISE_XOR", "^"); 40 | if (!defined("T_POW")) { 41 | define("T_POW", "**"); 42 | } 43 | if (!defined("T_POW_EQUAL")) { 44 | define("T_POW_EQUAL", "**="); 45 | } 46 | if (!defined("T_YIELD")) { 47 | define("T_YIELD", "yield"); 48 | } 49 | if (!defined("T_FINALLY")) { 50 | define("T_FINALLY", "finally"); 51 | } 52 | ; 53 | abstract class FormatterPass { 54 | protected $indentChar = "\t"; 55 | protected $newLine = "\n"; 56 | protected $indent = 0; 57 | protected $code = ''; 58 | protected $ptr = 0; 59 | protected $tkns = []; 60 | protected $useCache = false; 61 | protected $cache = []; 62 | protected $ignoreFutileTokens = [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT]; 63 | 64 | protected function appendCode($code = "") { 65 | $this->code .= $code; 66 | } 67 | 68 | private function calculateCacheKey($direction, $ignoreList, $token) { 69 | return $direction . "\x2" . implode('', $ignoreList) . "\x2" . (is_array($token) ? implode("\x2", $token) : $token); 70 | } 71 | 72 | abstract public function candidate($source, $foundTokens); 73 | abstract public function format($source); 74 | 75 | protected function getToken($token) { 76 | if (isset($token[1])) { 77 | return $token; 78 | } else { 79 | return [$token, $token]; 80 | } 81 | } 82 | 83 | protected function getCrlf($true = true) { 84 | return $true ? $this->newLine : ""; 85 | } 86 | 87 | protected function getCrlfIndent() { 88 | return $this->getCrlf() . $this->getIndent(); 89 | } 90 | 91 | protected function getIndent($increment = 0) { 92 | return str_repeat($this->indentChar, $this->indent + $increment); 93 | } 94 | 95 | protected function getSpace($true = true) { 96 | return $true ? " " : ""; 97 | } 98 | 99 | protected function hasLn($text) { 100 | return (false !== strpos($text, $this->newLine)); 101 | } 102 | 103 | protected function hasLnAfter() { 104 | $id = null; 105 | $text = null; 106 | list($id, $text) = $this->inspectToken(); 107 | return T_WHITESPACE === $id && $this->hasLn($text); 108 | } 109 | 110 | protected function hasLnBefore() { 111 | $id = null; 112 | $text = null; 113 | list($id, $text) = $this->inspectToken(-1); 114 | return T_WHITESPACE === $id && $this->hasLn($text); 115 | } 116 | 117 | protected function hasLnLeftToken() { 118 | list($id, $text) = $this->getToken($this->leftToken()); 119 | return $this->hasLn($text); 120 | } 121 | 122 | protected function hasLnRightToken() { 123 | list($id, $text) = $this->getToken($this->rightToken()); 124 | return $this->hasLn($text); 125 | } 126 | 127 | protected function inspectToken($delta = 1) { 128 | if (!isset($this->tkns[$this->ptr + $delta])) { 129 | return [null, null]; 130 | } 131 | return $this->getToken($this->tkns[$this->ptr + $delta]); 132 | } 133 | 134 | protected function leftToken($ignoreList = [], $idx = false) { 135 | $i = $this->leftTokenIdx($ignoreList); 136 | 137 | return $this->tkns[$i]; 138 | } 139 | 140 | protected function leftTokenIdx($ignoreList = []) { 141 | $ignoreList = $this->resolveIgnoreList($ignoreList); 142 | 143 | $i = $this->walkLeft($this->tkns, $this->ptr, $ignoreList); 144 | 145 | return $i; 146 | } 147 | 148 | protected function leftTokenIs($token, $ignoreList = []) { 149 | return $this->tokenIs('left', $token, $ignoreList); 150 | } 151 | 152 | protected function leftTokenSubsetIsAtIdx($tkns, $idx, $token, $ignoreList = []) { 153 | $ignoreList = $this->resolveIgnoreList($ignoreList); 154 | 155 | $idx = $this->walkLeft($tkns, $idx, $ignoreList); 156 | 157 | return $this->resolveTokenMatch($tkns, $idx, $token); 158 | } 159 | 160 | protected function leftUsefulToken() { 161 | return $this->leftToken($this->ignoreFutileTokens); 162 | } 163 | 164 | protected function leftUsefulTokenIdx() { 165 | return $this->leftTokenIdx($this->ignoreFutileTokens); 166 | } 167 | 168 | protected function leftUsefulTokenIs($token) { 169 | return $this->leftTokenIs($token, $this->ignoreFutileTokens); 170 | } 171 | 172 | protected function printAndStopAt($tknids) { 173 | if (is_scalar($tknids)) { 174 | $tknids = [$tknids]; 175 | } 176 | $tknids = array_flip($tknids); 177 | while (list($index, $token) = each($this->tkns)) { 178 | list($id, $text) = $this->getToken($token); 179 | $this->ptr = $index; 180 | $this->cache = []; 181 | if (isset($tknids[$id])) { 182 | return [$id, $text]; 183 | } 184 | $this->appendCode($text); 185 | } 186 | } 187 | 188 | protected function printBlock($start, $end) { 189 | $count = 1; 190 | while (list($index, $token) = each($this->tkns)) { 191 | list($id, $text) = $this->getToken($token); 192 | $this->ptr = $index; 193 | $this->cache = []; 194 | $this->appendCode($text); 195 | 196 | if ($start == $id) { 197 | ++$count; 198 | } 199 | if ($end == $id) { 200 | --$count; 201 | } 202 | if (0 == $count) { 203 | break; 204 | } 205 | } 206 | } 207 | 208 | protected function printCurlyBlock() { 209 | $count = 1; 210 | while (list($index, $token) = each($this->tkns)) { 211 | list($id, $text) = $this->getToken($token); 212 | $this->ptr = $index; 213 | $this->cache = []; 214 | $this->appendCode($text); 215 | 216 | if (ST_CURLY_OPEN == $id) { 217 | ++$count; 218 | } 219 | if (T_CURLY_OPEN == $id) { 220 | ++$count; 221 | } 222 | if (T_DOLLAR_OPEN_CURLY_BRACES == $id) { 223 | ++$count; 224 | } 225 | if (ST_CURLY_CLOSE == $id) { 226 | --$count; 227 | } 228 | if (0 == $count) { 229 | break; 230 | } 231 | } 232 | } 233 | 234 | protected function printUntil($tknid) { 235 | while (list($index, $token) = each($this->tkns)) { 236 | list($id, $text) = $this->getToken($token); 237 | $this->ptr = $index; 238 | $this->cache = []; 239 | $this->appendCode($text); 240 | if ($tknid == $id) { 241 | break; 242 | } 243 | } 244 | } 245 | 246 | protected function printUntilAny($tknids) { 247 | $tknids = array_flip($tknids); 248 | $whitespaceNewLine = false; 249 | if (isset($tknids[$this->newLine])) { 250 | $whitespaceNewLine = true; 251 | } 252 | while (list($index, $token) = each($this->tkns)) { 253 | list($id, $text) = $this->getToken($token); 254 | $this->ptr = $index; 255 | $this->cache = []; 256 | $this->appendCode($text); 257 | if ($whitespaceNewLine && T_WHITESPACE == $id && $this->hasLn($text)) { 258 | break; 259 | } 260 | if (isset($tknids[$id])) { 261 | break; 262 | } 263 | } 264 | return $id; 265 | } 266 | 267 | protected function printUntilTheEndOfString() { 268 | $this->printUntil(ST_QUOTE); 269 | } 270 | 271 | protected function render($tkns = null) { 272 | if (null == $tkns) { 273 | $tkns = $this->tkns; 274 | } 275 | 276 | $tkns = array_filter($tkns); 277 | $str = ''; 278 | foreach ($tkns as $token) { 279 | list($id, $text) = $this->getToken($token); 280 | $str .= $text; 281 | } 282 | return $str; 283 | } 284 | 285 | protected function renderLight($tkns = null) { 286 | if (null == $tkns) { 287 | $tkns = $this->tkns; 288 | } 289 | $str = ''; 290 | foreach ($tkns as $token) { 291 | $str .= $token[1]; 292 | } 293 | return $str; 294 | } 295 | 296 | private function resolveIgnoreList($ignoreList = []) { 297 | if (empty($ignoreList)) { 298 | $ignoreList[T_WHITESPACE] = true; 299 | } else { 300 | $ignoreList = array_flip($ignoreList); 301 | } 302 | return $ignoreList; 303 | } 304 | 305 | private function resolveTokenMatch($tkns, $idx, $token) { 306 | if (!isset($tkns[$idx])) { 307 | return false; 308 | } 309 | 310 | $foundToken = $tkns[$idx]; 311 | if ($foundToken === $token) { 312 | return true; 313 | } elseif (is_array($token) && isset($foundToken[1]) && in_array($foundToken[0], $token)) { 314 | return true; 315 | } elseif (is_array($token) && !isset($foundToken[1]) && in_array($foundToken, $token)) { 316 | return true; 317 | } elseif (isset($foundToken[1]) && $foundToken[0] == $token) { 318 | return true; 319 | } 320 | 321 | return false; 322 | } 323 | 324 | protected function rightToken($ignoreList = []) { 325 | $i = $this->rightTokenIdx($ignoreList); 326 | 327 | return $this->tkns[$i]; 328 | } 329 | 330 | protected function rightTokenIdx($ignoreList = []) { 331 | $ignoreList = $this->resolveIgnoreList($ignoreList); 332 | 333 | $i = $this->walkRight($this->tkns, $this->ptr, $ignoreList); 334 | 335 | return $i; 336 | } 337 | 338 | protected function rightTokenIs($token, $ignoreList = []) { 339 | return $this->tokenIs('right', $token, $ignoreList); 340 | } 341 | 342 | protected function rightTokenSubsetIsAtIdx($tkns, $idx, $token, $ignoreList = []) { 343 | $ignoreList = $this->resolveIgnoreList($ignoreList); 344 | 345 | $idx = $this->walkRight($tkns, $idx, $ignoreList); 346 | 347 | return $this->resolveTokenMatch($tkns, $idx, $token); 348 | } 349 | 350 | protected function rightUsefulToken() { 351 | return $this->rightToken($this->ignoreFutileTokens); 352 | } 353 | 354 | // protected function rightUsefulTokenIdx($idx = false) { 355 | // return $this->rightTokenIdx($this->ignoreFutileTokens); 356 | // } 357 | 358 | protected function rightUsefulTokenIs($token) { 359 | return $this->rightTokenIs($token, $this->ignoreFutileTokens); 360 | } 361 | 362 | protected function rtrimAndAppendCode($code = "") { 363 | $this->code = rtrim($this->code) . $code; 364 | } 365 | 366 | protected function scanAndReplace(&$tkns, &$ptr, $start, $end, $call, $look_for) { 367 | $look_for = array_flip($look_for); 368 | $placeholder = 'getToken($token); 374 | if (isset($look_for[$id])) { 375 | $foundPotentialTokens = true; 376 | } 377 | if ($start == $id) { 378 | ++$tknCount; 379 | } 380 | if ($end == $id) { 381 | --$tknCount; 382 | } 383 | $tkns[$ptr] = null; 384 | if (0 == $tknCount) { 385 | break; 386 | } 387 | $tmp .= $text; 388 | } 389 | if ($foundPotentialTokens) { 390 | return $start . str_replace($placeholder, '', $this->{$call}($placeholder . $tmp)) . $end; 391 | } 392 | return $start . $tmp . $end; 393 | 394 | } 395 | 396 | protected function setIndent($increment) { 397 | $this->indent += $increment; 398 | if ($this->indent < 0) { 399 | $this->indent = 0; 400 | } 401 | } 402 | 403 | protected function siblings($tkns, $ptr) { 404 | $ignoreList = $this->resolveIgnoreList([T_WHITESPACE]); 405 | $left = $this->walkLeft($tkns, $ptr, $ignoreList); 406 | $right = $this->walkRight($tkns, $ptr, $ignoreList); 407 | return [$left, $right]; 408 | } 409 | 410 | protected function substrCountTrailing($haystack, $needle) { 411 | return strlen(rtrim($haystack, " \t")) - strlen(rtrim($haystack, " \t" . $needle)); 412 | } 413 | 414 | protected function tokenIs($direction, $token, $ignoreList = []) { 415 | if ('left' != $direction) { 416 | $direction = 'right'; 417 | } 418 | if (!$this->useCache) { 419 | return $this->{$direction . 'tokenSubsetIsAtIdx'}($this->tkns, $this->ptr, $token, $ignoreList); 420 | } 421 | 422 | $key = $this->calculateCacheKey($direction, $ignoreList, $token); 423 | if (isset($this->cache[$key])) { 424 | return $this->cache[$key]; 425 | } 426 | 427 | $ret = $this->{$direction . 'tokenSubsetIsAtIdx'}($this->tkns, $this->ptr, $token, $ignoreList); 428 | $this->cache[$key] = $ret; 429 | 430 | return $ret; 431 | } 432 | 433 | protected function walkAndAccumulateUntil(&$tkns, $tknid) { 434 | $ret = ''; 435 | while (list($index, $token) = each($tkns)) { 436 | list($id, $text) = $this->getToken($token); 437 | $this->ptr = $index; 438 | $ret .= $text; 439 | if ($tknid == $id) { 440 | break; 441 | } 442 | } 443 | return $ret; 444 | } 445 | 446 | private function walkLeft($tkns, $idx, $ignoreList) { 447 | $i = $idx; 448 | while (--$i >= 0 && isset($tkns[$i][1]) && isset($ignoreList[$tkns[$i][0]])); 449 | return $i; 450 | } 451 | 452 | private function walkRight($tkns, $idx, $ignoreList) { 453 | $i = $idx; 454 | $tknsSize = sizeof($tkns) - 1; 455 | while (++$i < $tknsSize && isset($tkns[$i][1]) && isset($ignoreList[$tkns[$i][0]])); 456 | return $i; 457 | } 458 | 459 | protected function walkUntil($tknid) { 460 | while (list($index, $token) = each($this->tkns)) { 461 | list($id, $text) = $this->getToken($token); 462 | $this->ptr = $index; 463 | if ($id == $tknid) { 464 | return [$id, $text]; 465 | } 466 | } 467 | } 468 | } 469 | ; 470 | class ParseException extends Exception {}; 471 | abstract class Parser extends FormatterPass { 472 | protected $filename = ''; 473 | protected $debug = false; 474 | 475 | public function __construct($filename, $debug) { 476 | $this->filename = $filename; 477 | $this->debug = $debug; 478 | } 479 | 480 | protected function accumulateAndStopAtAny(&$tkns, $tknids, $ignoreList = []) { 481 | if (empty($ignoreList)) { 482 | $ignoreList[T_WHITESPACE] = true; 483 | } else { 484 | $ignoreList = array_flip($ignoreList); 485 | } 486 | $tknids = array_flip($tknids); 487 | $ret = ''; 488 | $id = null; 489 | $text = null; 490 | while (list($index, $token) = each($tkns)) { 491 | list($id, $text) = $this->getToken($token); 492 | $this->ptr = $index; 493 | if (isset($ignoreList[$id])) { 494 | continue; 495 | } 496 | if (isset($tknids[$id])) { 497 | break; 498 | } 499 | $ret .= $text; 500 | } 501 | return [$ret, $id, $text]; 502 | } 503 | 504 | public function candidate($source, $foundTokens) { 505 | return true; 506 | } 507 | 508 | protected function debug($msg) { 509 | $this->debug && fwrite(STDERR, $msg . PHP_EOL); 510 | } 511 | 512 | protected function detectsNamespace() { 513 | $namespace = ''; 514 | while (list($index, $token) = each($this->tkns)) { 515 | list($id, $text) = $this->getToken($token); 516 | $this->ptr = $index; 517 | switch ($id) { 518 | case T_NAMESPACE: 519 | list($namespace, $foundId) = $this->accumulateAndStopAtAny($this->tkns, [ST_SEMI_COLON, ST_CURLY_OPEN], $this->ignoreFutileTokens); 520 | if ('{' == $foundId) { 521 | throw new ParseException("Namespaces with curly braces are not yet supported."); 522 | } 523 | break 2; 524 | } 525 | } 526 | if ('\\' == substr($namespace, 0, -1)) { 527 | $namespace = substr($namespace, 0, -1); 528 | } 529 | $namespace .= '\\'; 530 | return $namespace; 531 | } 532 | 533 | public function format($source) { 534 | return $source; 535 | } 536 | 537 | abstract public function parse($source); 538 | 539 | protected function walkAndAccumulateCurlyBlock() { 540 | $tokens = []; 541 | $count = 1; 542 | while (list($index, $token) = each($this->tkns)) { 543 | list($id, $text) = $this->getToken($token); 544 | $this->ptr = $index; 545 | $this->cache = []; 546 | $tokens[] = [$id, $text]; 547 | 548 | if (ST_CURLY_OPEN == $id) { 549 | ++$count; 550 | } 551 | if (T_CURLY_OPEN == $id) { 552 | ++$count; 553 | } 554 | if (T_DOLLAR_OPEN_CURLY_BRACES == $id) { 555 | ++$count; 556 | } 557 | if (ST_CURLY_CLOSE == $id) { 558 | --$count; 559 | } 560 | if (0 == $count) { 561 | break; 562 | } 563 | } 564 | return $tokens; 565 | } 566 | }; 567 | class ClassParser extends Parser { 568 | public function parse($source) { 569 | $parsedClasses = []; 570 | $parsedExtendedClasses = []; 571 | $parsedImplementedClasses = []; 572 | $this->tkns = token_get_all($source); 573 | $this->code = ''; 574 | 575 | $namespace = $this->detectsNamespace(); 576 | reset($this->tkns); 577 | while (list($index, $token) = each($this->tkns)) { 578 | list($id, $text) = $this->getToken($token); 579 | $this->ptr = $index; 580 | switch ($id) { 581 | case T_INTERFACE: 582 | case T_CLASS: 583 | if ($this->leftUsefulTokenIs(T_DOUBLE_COLON)) { 584 | continue; 585 | } 586 | $className = null; 587 | $extends = null; 588 | $implements = null; 589 | 590 | list($className, $foundId) = $this->accumulateAndStopAtAny($this->tkns, [ST_CURLY_OPEN, T_EXTENDS, T_IMPLEMENTS], $this->ignoreFutileTokens); 591 | 592 | if (T_EXTENDS == $foundId) { 593 | list($extends, $foundId) = $this->accumulateAndStopAtAny($this->tkns, [ST_CURLY_OPEN, T_IMPLEMENTS], $this->ignoreFutileTokens); 594 | } 595 | 596 | if (T_IMPLEMENTS == $foundId) { 597 | list($implements) = $this->accumulateAndStopAtAny($this->tkns, [ST_CURLY_OPEN], $this->ignoreFutileTokens); 598 | } 599 | 600 | $this->debug('[' . $className . ' e:' . $extends . ' i:' . $implements . ']'); 601 | $parsedClasses[$namespace . $className][] = [ 602 | 'filename' => $this->filename, 603 | 'extends' => $extends, 604 | 'implements' => $implements, 605 | ]; 606 | if (!empty($extends)) { 607 | $parsedExtendedClasses[$extends] = [ 608 | 'filename' => $this->filename, 609 | 'extended_by' => $className, 610 | 'implements' => $implements, 611 | ]; 612 | } 613 | if (!empty($implements)) { 614 | $implements = explode(',', $implements); 615 | foreach ($implements as $implement) { 616 | $parsedImplementedClasses[$implement] = [ 617 | 'filename' => $this->filename, 618 | 'implemented_by' => $className, 619 | 'extends' => $extends, 620 | ]; 621 | } 622 | } 623 | break; 624 | } 625 | } 626 | return [ 627 | $parsedClasses, 628 | $parsedExtendedClasses, 629 | $parsedImplementedClasses, 630 | ]; 631 | } 632 | }; 633 | class ClassMethodParser extends Parser { 634 | public function parse($source) { 635 | $this->tkns = token_get_all($source); 636 | $this->code = ''; 637 | 638 | $foundMethods = []; 639 | $namespace = $this->detectsNamespace(); 640 | reset($this->tkns); 641 | while (list($index, $token) = each($this->tkns)) { 642 | list($id, $text) = $this->getToken($token); 643 | $this->ptr = $index; 644 | switch ($id) { 645 | case T_CLASS: 646 | if ($this->leftUsefulTokenIs(T_DOUBLE_COLON)) { 647 | continue; 648 | } 649 | 650 | list($className, $foundId, $foundText) = $this->accumulateAndStopAtAny($this->tkns, [ST_CURLY_OPEN, T_EXTENDS, T_IMPLEMENTS], $this->ignoreFutileTokens); 651 | if (T_EXTENDS == $foundId) { 652 | list(, $foundId, $foundText) = $this->accumulateAndStopAtAny($this->tkns, [ST_CURLY_OPEN, T_IMPLEMENTS], $this->ignoreFutileTokens); 653 | } 654 | 655 | if (T_IMPLEMENTS == $foundId) { 656 | list(, $foundId, $foundText) = $this->accumulateAndStopAtAny($this->tkns, [ST_CURLY_OPEN], $this->ignoreFutileTokens); 657 | } 658 | 659 | $classBody = array_merge([[$foundId, $foundText]], $this->walkAndAccumulateCurlyBlock()); 660 | $foundMethods[$namespace . $className] = $this->parseMethods($namespace, $className, $classBody); 661 | break; 662 | } 663 | } 664 | return $foundMethods; 665 | } 666 | private function parseMethods($namespace, $className, $tokens) { 667 | $methodList = []; 668 | while (list(, $token) = each($tokens)) { 669 | list($id, $text) = $this->getToken($token); 670 | switch ($id) { 671 | case T_FUNCTION: 672 | list($functionName, $foundId, $foundText) = $this->accumulateAndStopAtAny($tokens, [ST_PARENTHESES_OPEN], $this->ignoreFutileTokens); 673 | if (empty($functionName)) { 674 | break; 675 | } 676 | 677 | if ("__construct" == $functionName) { 678 | $functionName = $className; 679 | $functionCall = $className; 680 | $functionSignature = $className . '('; 681 | } else { 682 | $functionCall = $functionName . $foundText; 683 | $functionSignature = $functionName . $foundText; 684 | } 685 | 686 | while (list(, $token) = each($tokens)) { 687 | list($id, $text) = $this->getToken($token); 688 | if (T_WHITESPACE == $id) { 689 | continue; 690 | } 691 | if (T_VARIABLE == $id) { 692 | $functionSignature .= ' '; 693 | $functionCall .= ' '; 694 | } 695 | if (T_VARIABLE == $id || ',' == $text || '(' == $text || ')' == $text) { 696 | $functionCall .= $text; 697 | } 698 | if (ST_CURLY_OPEN == $id || ST_SEMI_COLON == $id) { 699 | break; 700 | } 701 | $functionSignature .= $text; 702 | } 703 | $methodList[] = [ 704 | $functionName, 705 | str_replace('( ', '(', $functionCall), 706 | str_replace('( ', '(', $functionSignature), 707 | ]; 708 | break; 709 | } 710 | } 711 | return $methodList; 712 | } 713 | 714 | }; 715 | class ClassInstantiationsParser extends Parser { 716 | public function parse($source) { 717 | $uses = []; 718 | $this->tkns = token_get_all($source); 719 | $this->code = ''; 720 | $namespace = $this->detectsNamespace(); 721 | reset($this->tkns); 722 | while (list($index, $token) = each($this->tkns)) { 723 | list($id, $text) = $this->getToken($token); 724 | $this->ptr = $index; 725 | switch ($id) { 726 | case T_USE: 727 | if ($this->rightUsefulTokenIs('(')) { 728 | continue; 729 | } 730 | list($class, $id) = $this->accumulateAndStopAtAny($this->tkns, [T_AS, ';'], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT]); 731 | if (';' == $id) { 732 | $alias = substr(strrchr($class, '\\'), 1); 733 | $uses[$alias] = $class; 734 | } elseif (T_AS == $id) { 735 | list($alias) = $this->accumulateAndStopAtAny($this->tkns, [';'], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT]); 736 | $uses[$alias] = $class; 737 | } 738 | } 739 | } 740 | 741 | reset($this->tkns); 742 | $used = []; 743 | while (list($index, $token) = each($this->tkns)) { 744 | list($id, $text) = $this->getToken($token); 745 | $this->ptr = $index; 746 | switch ($id) { 747 | case T_NEW: 748 | if ($this->rightUsefulTokenIs(T_NS_SEPARATOR)) { 749 | // TODO! Analyse FQNs 750 | continue; 751 | } 752 | list($called) = $this->accumulateAndStopAtAny($this->tkns, ['('], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT]); 753 | if (isset($uses[$called])) { 754 | $used[$called] = $uses[$called]; 755 | } 756 | } 757 | } 758 | return array_flip($used); 759 | } 760 | }; 761 | 762 | function flushDb($uri, $fnDb, $ignoreList) { 763 | 764 | file_exists($fnDb) && unlink($fnDb); 765 | $db = new SQLite3($fnDb); 766 | $db->exec( 767 | 'CREATE TABLE classes ( 768 | filename text, 769 | class text, 770 | extends text, 771 | implements text 772 | );' 773 | ); 774 | $db->exec( 775 | 'CREATE TABLE extends ( 776 | filename text, 777 | extends text, 778 | extended_by text, 779 | implements text 780 | );' 781 | ); 782 | $db->exec( 783 | 'CREATE TABLE implements ( 784 | filename text, 785 | implements text, 786 | implemented_by text, 787 | extends text 788 | );' 789 | ); 790 | $db->exec( 791 | 'CREATE TABLE methods ( 792 | filename text, 793 | class text, 794 | method_name text, 795 | method_call text, 796 | method_signature text 797 | );' 798 | ); 799 | $db->exec( 800 | 'CREATE TABLE calls ( 801 | filename text, 802 | called text 803 | );' 804 | ); 805 | 806 | fwrite(STDERR, "Database not found... generating" . PHP_EOL); 807 | $debug = false; 808 | 809 | $dir = new RecursiveDirectoryIterator($uri); 810 | $it = new RecursiveIteratorIterator($dir); 811 | $files = new RegexIterator($it, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH); 812 | $all_classes = []; 813 | $all_extends = []; 814 | $all_implements = []; 815 | $all_methods = []; 816 | foreach ($files as $file) { 817 | $file = $file[0]; 818 | foreach ((array) $ignoreList as $ignore) { 819 | $ignore = trim($ignore); 820 | if ( 821 | substr(str_replace(getcwd() . '/', '', $file), 0, strlen($ignore)) == $ignore || 822 | substr($file, 0, strlen($ignore)) == $ignore 823 | ) { 824 | continue 2; 825 | } 826 | } 827 | echo $file; 828 | 829 | $content = file_get_contents($file); 830 | 831 | try { 832 | list($class, $extends, $implements) = (new ClassParser($file, $debug))->parse($content); 833 | $methods = (new ClassMethodParser($file, $debug))->parse($content); 834 | $calls = (new ClassInstantiationsParser($file, $debug))->parse($content); 835 | 836 | foreach ($calls as $class_name => $class_alias) { 837 | $db->exec(' 838 | INSERT INTO 839 | calls 840 | VALUES 841 | ( 842 | "' . SQLite3::escapeString($file) . '", 843 | "' . SQLite3::escapeString($class_name) . '" 844 | ); 845 | '); 846 | } 847 | 848 | foreach ($class as $class_name => $class_data) { 849 | foreach ($class_data as $data) { 850 | $db->exec(' 851 | INSERT INTO 852 | classes 853 | VALUES 854 | ( 855 | "' . SQLite3::escapeString($file) . '", 856 | "' . SQLite3::escapeString($class_name) . '", 857 | "' . SQLite3::escapeString($data['extends']) . '", 858 | "' . SQLite3::escapeString($data['implements']) . '" 859 | ); 860 | '); 861 | } 862 | } 863 | 864 | foreach ($extends as $extends_name => $data) { 865 | $db->exec(' 866 | INSERT INTO 867 | extends 868 | VALUES 869 | ( 870 | "' . SQLite3::escapeString($file) . '", 871 | "' . SQLite3::escapeString($extends_name) . '", 872 | "' . SQLite3::escapeString($data['extended_by']) . '", 873 | "' . SQLite3::escapeString($data['implements']) . '" 874 | ); 875 | '); 876 | } 877 | 878 | foreach ($implements as $implements_name => $data) { 879 | $db->exec(' 880 | INSERT INTO 881 | implements 882 | VALUES 883 | ( 884 | "' . SQLite3::escapeString($file) . '", 885 | "' . SQLite3::escapeString($implements_name) . '", 886 | "' . SQLite3::escapeString($data['implemented_by']) . '", 887 | "' . SQLite3::escapeString($data['extends']) . '" 888 | ); 889 | '); 890 | } 891 | 892 | foreach ($methods as $class => $class_methods) { 893 | foreach ($class_methods as $data) { 894 | $db->exec(" 895 | INSERT INTO 896 | methods 897 | VALUES 898 | ( 899 | '" . SQLite3::escapeString($file) . "', 900 | '" . SQLite3::escapeString($class) . "', 901 | '" . SQLite3::escapeString($data[0]) . "', 902 | '" . SQLite3::escapeString($data[1]) . "', 903 | '" . SQLite3::escapeString($data[2]) . "' 904 | ); 905 | "); 906 | } 907 | } 908 | echo ' done', PHP_EOL; 909 | } catch (ParseException $pe) { 910 | echo ' skipped - ' . $pe->getMessage() . PHP_EOL; 911 | } 912 | } 913 | $db->close(); 914 | }; 915 | 916 | if (!isset($argv[1])) { 917 | exit(255); 918 | } 919 | $cmd = trim(strtolower($argv[1])); 920 | $ignoreList = []; 921 | $ignoreListFn = 'oracle.ignore'; 922 | if (file_exists($ignoreListFn)) { 923 | $ignoreList = file($ignoreListFn); 924 | } 925 | 926 | if (!isset($fnDb)) { 927 | $fnDb = 'oracle.sqlite'; 928 | } 929 | 930 | if (!file_exists($fnDb) || 'flush' == $cmd) { 931 | $uri = $argv[2]; 932 | flushDb($uri, $fnDb, $ignoreList); 933 | exit(0); 934 | } 935 | if (time() - filemtime($fnDb) > 86400) { 936 | fwrite(STDERR, "Warning: database file older than a day" . PHP_EOL); 937 | } 938 | 939 | $db = new SQLite3($fnDb); 940 | 941 | function introspectInterface(&$found_implements) { 942 | foreach ($found_implements as $row) { 943 | echo "\t", $row['implemented_by'], " - ", $row["filename"], PHP_EOL; 944 | } 945 | echo PHP_EOL; 946 | } 947 | if ("implements" == $cmd) { 948 | $results = $db->query("SELECT * FROM implements WHERE implements LIKE '%" . SQLite3::escapeString($argv[2]) . "'"); 949 | $found_implements = []; 950 | while ($row = $results->fetchArray()) { 951 | $found_implements[] = [ 952 | 'filename' => $row['filename'], 953 | 'implemented_by' => $row['implemented_by'], 954 | ]; 955 | } 956 | if (empty($found_implements)) { 957 | fwrite(STDERR, "Interface not found: " . $argv[2] . PHP_EOL); 958 | exit(255); 959 | } 960 | echo $argv[2] . ' implemented by' . PHP_EOL; 961 | introspectInterface($found_implements); 962 | } 963 | 964 | function introspectExtends(&$found_extends) { 965 | foreach ($found_extends as $row) { 966 | echo "\t", $row['extended_by'], " - ", $row["filename"], PHP_EOL; 967 | } 968 | echo PHP_EOL; 969 | } 970 | if ("extends" == $cmd) { 971 | $results = $db->query("SELECT * FROM extends WHERE extends LIKE '%" . SQLite3::escapeString($argv[2]) . "'"); 972 | $found_extends = []; 973 | while ($row = $results->fetchArray()) { 974 | $found_extends[] = [ 975 | 'filename' => $row['filename'], 976 | 'extended_by' => $row['extended_by'], 977 | ]; 978 | } 979 | if (empty($found_extends)) { 980 | fwrite(STDERR, "Class not found: " . $argv[2] . PHP_EOL); 981 | exit(255); 982 | } 983 | 984 | echo $argv[2] . ' extended by' . PHP_EOL; 985 | introspectExtends($found_extends); 986 | } 987 | 988 | function introspectClass(&$found_classes) { 989 | if (!empty($found_classes['extends'])) { 990 | echo "\t extends ", $found_classes['extends'], PHP_EOL; 991 | } 992 | 993 | if (!empty($found_classes['implements'])) { 994 | echo "\t implements ", $found_classes['implements'], PHP_EOL; 995 | } 996 | 997 | echo PHP_EOL; 998 | } 999 | if ("class" == $cmd) { 1000 | $results = $db->query("SELECT * FROM classes WHERE class LIKE '%" . SQLite3::escapeString($argv[2]) . "'"); 1001 | $found_classes = []; 1002 | while ($row = $results->fetchArray()) { 1003 | $found_classes = [ 1004 | 'filename' => $row['filename'], 1005 | 'class' => $row['class'], 1006 | 'extends' => $row['extends'], 1007 | 'implements' => $row['implements'], 1008 | ]; 1009 | break; 1010 | } 1011 | 1012 | if (empty($found_classes)) { 1013 | fwrite(STDERR, "Class not found: " . $argv[2] . PHP_EOL); 1014 | exit(255); 1015 | } 1016 | 1017 | echo $argv[2], PHP_EOL; 1018 | introspectClass($found_classes); 1019 | } 1020 | 1021 | function introspectCall(&$found_calls) { 1022 | foreach ($found_calls as $row) { 1023 | echo "\t ", $row['filename'], ' called ', $row['called'], PHP_EOL; 1024 | } 1025 | echo PHP_EOL; 1026 | } 1027 | if ("calls" == $cmd) { 1028 | $results = $db->query("SELECT * FROM calls WHERE called LIKE '%" . SQLite3::escapeString($argv[2]) . "'"); 1029 | $found_calls = []; 1030 | while ($row = $results->fetchArray()) { 1031 | $found_calls[] = [ 1032 | 'filename' => $row['filename'], 1033 | 'called' => $row['called'], 1034 | ]; 1035 | } 1036 | 1037 | if (empty($found_calls)) { 1038 | fwrite(STDERR, "Call not found: " . $argv[2] . PHP_EOL); 1039 | exit(255); 1040 | } 1041 | 1042 | echo $argv[2], PHP_EOL; 1043 | introspectCall($found_calls); 1044 | } 1045 | if ("introspect" == $cmd) { 1046 | $target = $argv[2]; 1047 | 1048 | $results = $db->query("SELECT * FROM implements WHERE implements LIKE '%" . SQLite3::escapeString($target) . "'"); 1049 | $all_found_implements = []; 1050 | while ($row = $results->fetchArray()) { 1051 | $all_found_implements[$row['implements']][] = [ 1052 | 'filename' => realpath($row['filename']), 1053 | 'implemented_by' => $row['implemented_by'], 1054 | ]; 1055 | } 1056 | foreach ($all_found_implements as $implements => $found_implements) { 1057 | echo $implements . ' implemented by' . PHP_EOL; 1058 | introspectInterface($found_implements); 1059 | } 1060 | 1061 | $results = $db->query("SELECT * FROM extends WHERE extends LIKE '%" . SQLite3::escapeString($target) . "'"); 1062 | $all_found_extends = []; 1063 | while ($row = $results->fetchArray()) { 1064 | $all_found_extends[$row['extends']][] = [ 1065 | 'filename' => realpath($row['filename']), 1066 | 'extended_by' => $row['extended_by'], 1067 | ]; 1068 | } 1069 | foreach ($all_found_extends as $extends => $found_extends) { 1070 | echo $extends . ' extended by' . PHP_EOL; 1071 | introspectExtends($found_extends); 1072 | } 1073 | 1074 | $results = $db->query("SELECT * FROM classes WHERE class LIKE '%" . SQLite3::escapeString($target) . "'"); 1075 | while ($row = $results->fetchArray()) { 1076 | $found_classes = [[ 1077 | 'filename' => realpath($row['filename']), 1078 | 'class' => $row['class'], 1079 | 'extends' => $row['extends'], 1080 | 'implements' => $row['implements'], 1081 | ]]; 1082 | echo "class " . $row['class'], PHP_EOL; 1083 | introspectClass($found_classes); 1084 | } 1085 | 1086 | ob_start(); 1087 | $foundMethod = false; 1088 | $results = $db->query("SELECT * FROM methods WHERE method_name LIKE '%" . SQLite3::escapeString($target) . "'"); 1089 | while ($row = $results->fetchArray()) { 1090 | $foundMethod = true; 1091 | echo ' - ' . $row['class'] . '::' . $row['method_signature'], PHP_EOL; 1092 | } 1093 | $methodOutput = ob_get_clean(); 1094 | if ($foundMethod) { 1095 | echo "Methods", PHP_EOL, $methodOutput; 1096 | } 1097 | 1098 | $results = $db->query("SELECT * FROM calls WHERE called LIKE '%" . SQLite3::escapeString($target) . "'"); 1099 | $found_calls = []; 1100 | while ($row = $results->fetchArray()) { 1101 | $found_calls[] = [ 1102 | 'filename' => realpath($row['filename']), 1103 | 'called' => $row['called'], 1104 | ]; 1105 | } 1106 | echo PHP_EOL, "Calls " . PHP_EOL; 1107 | introspectCall($found_calls); 1108 | } 1109 | 1110 | if ("calltip" == $cmd) { 1111 | ob_start(); 1112 | $foundMethod = false; 1113 | $results = $db->query("SELECT * FROM methods WHERE method_name LIKE '%" . SQLite3::escapeString($argv[2]) . "'"); 1114 | while ($row = $results->fetchArray()) { 1115 | $foundMethod = true; 1116 | echo $row['class'] . '::' . $row['method_signature'], PHP_EOL; 1117 | break; 1118 | } 1119 | $methodOutput = ob_get_clean(); 1120 | if ($foundMethod) { 1121 | echo $methodOutput; 1122 | } 1123 | } 1124 | 1125 | if ("autocomplete" == $cmd) { 1126 | $searchFor = $argv[2]; 1127 | 1128 | echo "term,match,class,type\n"; 1129 | 1130 | $results = $db->query("SELECT * FROM classes WHERE class LIKE '%" . SQLite3::escapeString($searchFor) . "%' ORDER BY class"); 1131 | while ($row = $results->fetchArray()) { 1132 | $tmp = explode('\\', $row['class']); 1133 | fputcsv(STDOUT, [$row['class'], array_pop($tmp), $row['class'], 'class'], ',', '"'); 1134 | } 1135 | $results = $db->query("SELECT * FROM classes WHERE class LIKE '%" . SQLite3::escapeString($searchFor) . "' ORDER BY class"); 1136 | while ($row = $results->fetchArray()) { 1137 | $tmp = explode('\\', $row['class']); 1138 | fputcsv(STDOUT, [$row['class'], array_pop($tmp), $row['class'], 'class'], ',', '"'); 1139 | } 1140 | $results = $db->query("SELECT * FROM methods WHERE method_name LIKE '%" . SQLite3::escapeString($searchFor) . "%'"); 1141 | while ($row = $results->fetchArray()) { 1142 | fputcsv(STDOUT, [$row['method_call'], $row['method_signature'], $row['class'], 'method'], ',', '"'); 1143 | } 1144 | } -------------------------------------------------------------------------------- /php.tools.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanch/phpfmt_stable/98de6272d5dd98667a49d7d2f831b13dfac7838c/php.tools.ini -------------------------------------------------------------------------------- /phpfmt.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | import os.path 4 | import shutil 5 | import sublime 6 | import sublime_plugin 7 | import subprocess 8 | import time 9 | import sys 10 | from os.path import dirname, realpath 11 | 12 | dist_dir = os.path.dirname(os.path.abspath(__file__)) 13 | sys.path.insert(0, dist_dir) 14 | from diff_match_patch.python3.diff_match_patch import diff_match_patch 15 | 16 | def print_debug(*msg): 17 | if getSetting(sublime.active_window().active_view(), sublime.load_settings('phpfmt.sublime-settings'), "debug", False): 18 | print(msg) 19 | 20 | def getSetting( view, settings, key, default ): 21 | local = 'phpfmt.' + key 22 | return view.settings().get( local, settings.get( key, default ) ) 23 | 24 | def dofmt(eself, eview, sgter = None, src = None, force = False): 25 | if int(sublime.version()) < 3000: 26 | print_debug("phpfmt: ST2 not supported") 27 | return False 28 | 29 | self = eself 30 | view = eview 31 | s = sublime.load_settings('phpfmt.sublime-settings') 32 | 33 | 34 | additional_extensions = getSetting( view, s, "additional_extensions", []) 35 | autoimport = getSetting( view, s, "autoimport", True) 36 | debug = getSetting( view, s, "debug", False) 37 | enable_auto_align = getSetting( view, s, "enable_auto_align", False) 38 | ignore_list = getSetting( view, s, "ignore_list", "") 39 | indent_with_space = getSetting( view, s, "indent_with_space", False) 40 | psr1 = getSetting( view, s, "psr1", False) 41 | psr1_naming = getSetting( view, s, "psr1_naming", psr1) 42 | psr2 = getSetting( view, s, "psr2", False) 43 | smart_linebreak_after_curly = getSetting( view, s, "smart_linebreak_after_curly", True) 44 | skip_if_ini_missing = getSetting( view, s, "skip_if_ini_missing", False) 45 | visibility_order = getSetting( view, s, "visibility_order", False) 46 | yoda = getSetting( view, s, "yoda", False) 47 | readini = getSetting( view, s, "readini", False) 48 | 49 | passes = getSetting( view, s, "passes", []) 50 | excludes = getSetting( view, s, "excludes", []) 51 | 52 | php_bin = getSetting( view, s, "php_bin", "php") 53 | formatter_path = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "fmt.phar") 54 | 55 | config_file = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "php.tools.ini") 56 | 57 | uri = view.file_name() 58 | dirnm, sfn = os.path.split(uri) 59 | ext = os.path.splitext(uri)[1][1:] 60 | 61 | if force is False and "php" != ext and not ext in additional_extensions: 62 | print_debug("phpfmt: not a PHP file") 63 | return False 64 | 65 | if "" != ignore_list: 66 | if type(ignore_list) is not list: 67 | ignore_list = ignore_list.split(" ") 68 | for v in ignore_list: 69 | pos = uri.find(v) 70 | if -1 != pos and v != "": 71 | print_debug("phpfmt: skipping file") 72 | return False 73 | 74 | if not os.path.isfile(php_bin) and not php_bin == "php": 75 | print_debug("Can't find PHP binary file at "+php_bin) 76 | sublime.error_message("Can't find PHP binary file at "+php_bin) 77 | 78 | # Look for oracle.sqlite 79 | if dirnm != "": 80 | oracleDirNm = dirnm 81 | while oracleDirNm != "/": 82 | oracleFname = oracleDirNm+os.path.sep+"oracle.sqlite" 83 | if os.path.isfile(oracleFname): 84 | break 85 | origOracleDirNm = oracleDirNm 86 | oracleDirNm = os.path.dirname(oracleDirNm) 87 | if origOracleDirNm == oracleDirNm: 88 | break 89 | 90 | if not os.path.isfile(oracleFname): 91 | print_debug("phpfmt (oracle file): not found") 92 | oracleFname = None 93 | else: 94 | print_debug("phpfmt (oracle file): "+oracleFname) 95 | 96 | if readini: 97 | iniDirNm = dirnm 98 | while iniDirNm != "/": 99 | iniFname = iniDirNm+os.path.sep+".php.tools.ini" 100 | if os.path.isfile(iniFname): 101 | break 102 | originiDirNm = iniDirNm 103 | iniDirNm = os.path.dirname(iniDirNm) 104 | if originiDirNm == iniDirNm: 105 | break 106 | 107 | if os.path.isfile(iniFname): 108 | print_debug("phpfmt (ini file): "+iniFname) 109 | config_file = iniFname 110 | elif skip_if_ini_missing: 111 | print_debug("phpfmt (ini file): not found - skipping") 112 | return False 113 | else: 114 | oracleFname = None 115 | 116 | cmd_ver = [php_bin, '-v']; 117 | if os.name == 'nt': 118 | startupinfo = subprocess.STARTUPINFO() 119 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 120 | p = subprocess.Popen(cmd_ver, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, startupinfo=startupinfo) 121 | else: 122 | p = subprocess.Popen(cmd_ver, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) 123 | res, err = p.communicate() 124 | print_debug("phpfmt (php_ver) cmd:\n", cmd_ver) 125 | print_debug("phpfmt (php_ver) out:\n", res.decode('utf-8')) 126 | print_debug("phpfmt (php_ver) err:\n", err.decode('utf-8')) 127 | if ('PHP 5.3' in res.decode('utf-8') or 'PHP 5.3' in err.decode('utf-8') or 'PHP 5.4' in res.decode('utf-8') or 'PHP 5.4' in err.decode('utf-8') or 'PHP 5.5' in res.decode('utf-8') or 'PHP 5.5' in err.decode('utf-8') or 'PHP 5.6' in res.decode('utf-8') or 'PHP 5.6' in err.decode('utf-8')): 128 | s = debugEnvironment(php_bin, formatter_path) 129 | sublime.message_dialog('Warning.\nPHP 7.0 or newer is required.\nPlease, upgrade your local PHP installation.\nDebug information:'+s) 130 | return False 131 | 132 | s = debugEnvironment(php_bin, formatter_path) 133 | print_debug(s) 134 | 135 | lintret = 1 136 | if "AutoSemicolon" in passes: 137 | lintret = 0 138 | else: 139 | cmd_lint = [php_bin,"-ddisplay_errors=1","-l"]; 140 | if src is None: 141 | cmd_lint.append(uri) 142 | if os.name == 'nt': 143 | startupinfo = subprocess.STARTUPINFO() 144 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 145 | p = subprocess.Popen(cmd_lint, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False, startupinfo=startupinfo) 146 | else: 147 | p = subprocess.Popen(cmd_lint, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False) 148 | else: 149 | if os.name == 'nt': 150 | startupinfo = subprocess.STARTUPINFO() 151 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 152 | p = subprocess.Popen(cmd_lint, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, startupinfo=startupinfo) 153 | else: 154 | p = subprocess.Popen(cmd_lint, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) 155 | p.stdin.write(src.encode('utf-8')) 156 | 157 | lint_out, lint_err = p.communicate() 158 | lintret = p.returncode 159 | 160 | if(lintret==0): 161 | cmd_fmt = [php_bin] 162 | 163 | if not debug: 164 | cmd_fmt.append("-ddisplay_errors=stderr") 165 | 166 | if psr1: 167 | cmd_fmt.append("-dshort_open_tag=On") 168 | 169 | cmd_fmt.append(formatter_path) 170 | cmd_fmt.append("--config="+config_file) 171 | 172 | if psr1: 173 | cmd_fmt.append("--psr1") 174 | 175 | if psr1_naming: 176 | cmd_fmt.append("--psr1-naming") 177 | 178 | if psr2: 179 | cmd_fmt.append("--psr2") 180 | 181 | if indent_with_space is True: 182 | cmd_fmt.append("--indent_with_space") 183 | elif indent_with_space > 0: 184 | cmd_fmt.append("--indent_with_space="+str(indent_with_space)) 185 | 186 | if enable_auto_align: 187 | cmd_fmt.append("--enable_auto_align") 188 | 189 | if visibility_order: 190 | cmd_fmt.append("--visibility_order") 191 | 192 | if smart_linebreak_after_curly: 193 | cmd_fmt.append("--smart_linebreak_after_curly") 194 | 195 | if yoda: 196 | cmd_fmt.append("--yoda") 197 | 198 | if sgter is not None: 199 | cmd_fmt.append("--setters_and_getters="+sgter) 200 | cmd_fmt.append("--constructor="+sgter) 201 | 202 | if autoimport is True and oracleFname is not None: 203 | cmd_fmt.append("--oracleDB="+oracleFname) 204 | 205 | if len(passes) > 0: 206 | cmd_fmt.append("--passes="+','.join(passes)) 207 | 208 | if len(excludes) > 0: 209 | cmd_fmt.append("--exclude="+','.join(excludes)) 210 | 211 | if debug: 212 | cmd_fmt.append("-v") 213 | 214 | if sgter is None: 215 | cmd_fmt.append("-o=-") 216 | 217 | if src is None: 218 | cmd_fmt.append(uri) 219 | else: 220 | cmd_fmt.append("-") 221 | 222 | print_debug("cmd_fmt: ", cmd_fmt) 223 | 224 | if src is None: 225 | if os.name == 'nt': 226 | startupinfo = subprocess.STARTUPINFO() 227 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 228 | p = subprocess.Popen(cmd_fmt, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False, startupinfo=startupinfo) 229 | else: 230 | p = subprocess.Popen(cmd_fmt, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False) 231 | else: 232 | if os.name == 'nt': 233 | startupinfo = subprocess.STARTUPINFO() 234 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 235 | p = subprocess.Popen(cmd_fmt, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, startupinfo=startupinfo) 236 | else: 237 | p = subprocess.Popen(cmd_fmt, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) 238 | 239 | if src is not None: 240 | p.stdin.write(src.encode('utf-8')) 241 | 242 | res, err = p.communicate() 243 | 244 | print_debug("p:\n", p.returncode) 245 | print_debug("err:\n", err.decode('utf-8')) 246 | 247 | if p.returncode != 0: 248 | return '' 249 | 250 | if sgter is not None: 251 | sublime.set_timeout(revert_active_window, 50) 252 | 253 | return res.decode('utf-8') 254 | else: 255 | sublime.status_message("phpfmt: format failed - syntax errors found") 256 | print_debug("lint error: ", lint_out) 257 | 258 | 259 | def dogeneratephpdoc(eself, eview): 260 | self = eself 261 | view = eview 262 | s = sublime.load_settings('phpfmt.sublime-settings') 263 | 264 | additional_extensions = s.get("additional_extensions", []) 265 | autoimport = s.get("autoimport", True) 266 | debug = s.get("debug", False) 267 | enable_auto_align = s.get("enable_auto_align", False) 268 | ignore_list = s.get("ignore_list", "") 269 | indent_with_space = s.get("indent_with_space", False) 270 | psr1 = s.get("psr1", False) 271 | psr1_naming = s.get("psr1_naming", psr1) 272 | psr2 = s.get("psr2", False) 273 | smart_linebreak_after_curly = s.get("smart_linebreak_after_curly", True) 274 | visibility_order = s.get("visibility_order", False) 275 | yoda = s.get("yoda", False) 276 | 277 | passes = s.get("passes", []) 278 | 279 | php_bin = s.get("php_bin", "php") 280 | formatter_path = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "fmt.phar") 281 | 282 | config_file = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "php.tools.ini") 283 | 284 | uri = view.file_name() 285 | dirnm, sfn = os.path.split(uri) 286 | ext = os.path.splitext(uri)[1][1:] 287 | 288 | if "php" != ext and not ext in additional_extensions: 289 | print_debug("phpfmt: not a PHP file") 290 | sublime.status_message("phpfmt: not a PHP file") 291 | return False 292 | 293 | if not os.path.isfile(php_bin) and not php_bin == "php": 294 | print_debug("Can't find PHP binary file at "+php_bin) 295 | sublime.error_message("Can't find PHP binary file at "+php_bin) 296 | 297 | print_debug("phpfmt:", uri) 298 | if enable_auto_align: 299 | print_debug("auto align: enabled") 300 | else: 301 | print_debug("auto align: disabled") 302 | 303 | 304 | 305 | cmd_lint = [php_bin,"-l",uri]; 306 | if os.name == 'nt': 307 | startupinfo = subprocess.STARTUPINFO() 308 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 309 | p = subprocess.Popen(cmd_lint, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False, startupinfo=startupinfo) 310 | else: 311 | p = subprocess.Popen(cmd_lint, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False) 312 | lint_out, lint_err = p.communicate() 313 | 314 | if(p.returncode==0): 315 | cmd_fmt = [php_bin] 316 | 317 | if not debug: 318 | cmd_fmt.append("-ddisplay_errors=stderr") 319 | 320 | cmd_fmt.append(formatter_path) 321 | cmd_fmt.append("--config="+config_file) 322 | 323 | if psr1: 324 | cmd_fmt.append("--psr1") 325 | 326 | if psr1_naming: 327 | cmd_fmt.append("--psr1-naming") 328 | 329 | if psr2: 330 | cmd_fmt.append("--psr2") 331 | 332 | if indent_with_space: 333 | cmd_fmt.append("--indent_with_space") 334 | elif indent_with_space > 0: 335 | cmd_fmt.append("--indent_with_space="+str(indent_with_space)) 336 | 337 | if enable_auto_align: 338 | cmd_fmt.append("--enable_auto_align") 339 | 340 | if visibility_order: 341 | cmd_fmt.append("--visibility_order") 342 | 343 | passes.append("GeneratePHPDoc") 344 | if len(passes) > 0: 345 | cmd_fmt.append("--passes="+','.join(passes)) 346 | 347 | cmd_fmt.append(uri) 348 | 349 | uri_tmp = uri + "~" 350 | 351 | print_debug("cmd_fmt: ", cmd_fmt) 352 | 353 | if os.name == 'nt': 354 | startupinfo = subprocess.STARTUPINFO() 355 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 356 | p = subprocess.Popen(cmd_fmt, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False, startupinfo=startupinfo) 357 | else: 358 | p = subprocess.Popen(cmd_fmt, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False) 359 | res, err = p.communicate() 360 | print_debug("err:\n", err.decode('utf-8')) 361 | sublime.set_timeout(revert_active_window, 50) 362 | else: 363 | print_debug("lint error: ", lint_out) 364 | 365 | def doreordermethod(eself, eview): 366 | self = eself 367 | view = eview 368 | s = sublime.load_settings('phpfmt.sublime-settings') 369 | 370 | additional_extensions = s.get("additional_extensions", []) 371 | autoimport = s.get("autoimport", True) 372 | debug = s.get("debug", False) 373 | enable_auto_align = s.get("enable_auto_align", False) 374 | ignore_list = s.get("ignore_list", "") 375 | indent_with_space = s.get("indent_with_space", False) 376 | psr1 = s.get("psr1", False) 377 | psr1_naming = s.get("psr1_naming", psr1) 378 | psr2 = s.get("psr2", False) 379 | smart_linebreak_after_curly = s.get("smart_linebreak_after_curly", True) 380 | visibility_order = s.get("visibility_order", False) 381 | yoda = s.get("yoda", False) 382 | 383 | passes = s.get("passes", []) 384 | 385 | php_bin = s.get("php_bin", "php") 386 | formatter_path = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "fmt.phar") 387 | 388 | config_file = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "php.tools.ini") 389 | 390 | uri = view.file_name() 391 | dirnm, sfn = os.path.split(uri) 392 | ext = os.path.splitext(uri)[1][1:] 393 | 394 | if "php" != ext and not ext in additional_extensions: 395 | print_debug("phpfmt: not a PHP file") 396 | sublime.status_message("phpfmt: not a PHP file") 397 | return False 398 | 399 | if not os.path.isfile(php_bin) and not php_bin == "php": 400 | print_debug("Can't find PHP binary file at "+php_bin) 401 | sublime.error_message("Can't find PHP binary file at "+php_bin) 402 | 403 | 404 | print_debug("phpfmt:", uri) 405 | if enable_auto_align: 406 | print_debug("auto align: enabled") 407 | else: 408 | print_debug("auto align: disabled") 409 | 410 | 411 | 412 | cmd_lint = [php_bin,"-l",uri]; 413 | if os.name == 'nt': 414 | startupinfo = subprocess.STARTUPINFO() 415 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 416 | p = subprocess.Popen(cmd_lint, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False, startupinfo=startupinfo) 417 | else: 418 | p = subprocess.Popen(cmd_lint, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False) 419 | lint_out, lint_err = p.communicate() 420 | 421 | if(p.returncode==0): 422 | cmd_fmt = [php_bin] 423 | 424 | if not debug: 425 | cmd_fmt.append("-ddisplay_errors=stderr") 426 | 427 | cmd_fmt.append(formatter_path) 428 | cmd_fmt.append("--config="+config_file) 429 | 430 | if psr1: 431 | cmd_fmt.append("--psr1") 432 | 433 | if psr1_naming: 434 | cmd_fmt.append("--psr1-naming") 435 | 436 | if psr2: 437 | cmd_fmt.append("--psr2") 438 | 439 | if indent_with_space: 440 | cmd_fmt.append("--indent_with_space") 441 | elif indent_with_space > 0: 442 | cmd_fmt.append("--indent_with_space="+str(indent_with_space)) 443 | 444 | if enable_auto_align: 445 | cmd_fmt.append("--enable_auto_align") 446 | 447 | if visibility_order: 448 | cmd_fmt.append("--visibility_order") 449 | 450 | passes.append("OrganizeClass") 451 | if len(passes) > 0: 452 | cmd_fmt.append("--passes="+','.join(passes)) 453 | 454 | cmd_fmt.append(uri) 455 | 456 | uri_tmp = uri + "~" 457 | 458 | print_debug("cmd_fmt: ", cmd_fmt) 459 | 460 | if os.name == 'nt': 461 | startupinfo = subprocess.STARTUPINFO() 462 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 463 | p = subprocess.Popen(cmd_fmt, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False, startupinfo=startupinfo) 464 | else: 465 | p = subprocess.Popen(cmd_fmt, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False) 466 | res, err = p.communicate() 467 | print_debug("err:\n", err.decode('utf-8')) 468 | sublime.set_timeout(revert_active_window, 50) 469 | else: 470 | print_debug("lint error: ", lint_out) 471 | 472 | def debugEnvironment(php_bin, formatter_path): 473 | ret = "" 474 | cmd_ver = [php_bin,"-v"]; 475 | if os.name == 'nt': 476 | startupinfo = subprocess.STARTUPINFO() 477 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 478 | p = subprocess.Popen(cmd_ver, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, startupinfo=startupinfo) 479 | else: 480 | p = subprocess.Popen(cmd_ver, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) 481 | res, err = p.communicate() 482 | ret += ("phpfmt (php version):\n"+res.decode('utf-8')) 483 | if err.decode('utf-8'): 484 | ret += ("phpfmt (php version) err:\n"+err.decode('utf-8')) 485 | ret += "\n" 486 | 487 | cmd_ver = [php_bin,"-m"]; 488 | if os.name == 'nt': 489 | startupinfo = subprocess.STARTUPINFO() 490 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 491 | p = subprocess.Popen(cmd_ver, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, startupinfo=startupinfo) 492 | else: 493 | p = subprocess.Popen(cmd_ver, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) 494 | res, err = p.communicate() 495 | if res.decode('utf-8').find("tokenizer") != -1: 496 | ret += ("phpfmt (php tokenizer) found\n") 497 | else: 498 | ret += ("phpfmt (php tokenizer):\n"+res.decode('utf-8')) 499 | if err.decode('utf-8'): 500 | ret += ("phpfmt (php tokenizer) err:\n"+err.decode('utf-8')) 501 | ret += "\n" 502 | 503 | cmd_ver = [php_bin,formatter_path,"--version"]; 504 | if os.name == 'nt': 505 | startupinfo = subprocess.STARTUPINFO() 506 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 507 | p = subprocess.Popen(cmd_ver, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, startupinfo=startupinfo) 508 | else: 509 | p = subprocess.Popen(cmd_ver, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) 510 | res, err = p.communicate() 511 | ret += ("phpfmt (fmt.phar version):\n"+res.decode('utf-8')) 512 | if err.decode('utf-8'): 513 | ret += ("phpfmt (fmt.phar version) err:\n"+err.decode('utf-8')) 514 | ret += "\n" 515 | 516 | return ret 517 | 518 | def revert_active_window(): 519 | sublime.active_window().active_view().run_command("revert") 520 | sublime.active_window().active_view().run_command("phpcs_sniff_this_file") 521 | 522 | def lookForOracleFile(view): 523 | uri = view.file_name() 524 | oracleDirNm, sfn = os.path.split(uri) 525 | originalDirNm = oracleDirNm 526 | 527 | while oracleDirNm != "/": 528 | oracleFname = oracleDirNm+os.path.sep+"oracle.sqlite" 529 | if os.path.isfile(oracleFname): 530 | return True 531 | origOracleDirNm = oracleDirNm 532 | oracleDirNm = os.path.dirname(oracleDirNm) 533 | if origOracleDirNm == oracleDirNm: 534 | return False 535 | return False 536 | 537 | def outputToPanel(name, eself, eedit, message): 538 | eself.output_view = eself.view.window().get_output_panel(name) 539 | eself.view.window().run_command("show_panel", {"panel": "output."+name}) 540 | eself.output_view.set_read_only(False) 541 | eself.output_view.insert(eedit, eself.output_view.size(), message) 542 | eself.output_view.set_read_only(True) 543 | 544 | def hidePanel(name, eself, eedit): 545 | eself.output_view = eself.view.window().get_output_panel(name) 546 | eself.view.window().run_command("hide_panel", {"panel": "output."+name}) 547 | 548 | class phpfmt(sublime_plugin.EventListener): 549 | def on_pre_save(self, view): 550 | s = sublime.load_settings('phpfmt.sublime-settings') 551 | format_on_save = s.get("format_on_save", True) 552 | 553 | if format_on_save: 554 | view.run_command('php_fmt') 555 | 556 | class AnalyseThisCommand(sublime_plugin.TextCommand): 557 | def run(self, edit): 558 | if not lookForOracleFile(self.view): 559 | sublime.active_window().active_view().run_command("build_oracle") 560 | return False 561 | 562 | lookTerm = (self.view.substr(self.view.word(self.view.sel()[0].a))) 563 | 564 | s = sublime.load_settings('phpfmt.sublime-settings') 565 | php_bin = s.get("php_bin", "php") 566 | oraclePath = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "oracle.php") 567 | 568 | uri = self.view.file_name() 569 | dirNm, sfn = os.path.split(uri) 570 | ext = os.path.splitext(uri)[1][1:] 571 | 572 | oracleDirNm = dirNm 573 | while oracleDirNm != "/": 574 | oracleFname = oracleDirNm+os.path.sep+"oracle.sqlite" 575 | if os.path.isfile(oracleFname): 576 | break 577 | origOracleDirNm = oracleDirNm 578 | oracleDirNm = os.path.dirname(oracleDirNm) 579 | if origOracleDirNm == oracleDirNm: 580 | break 581 | 582 | cmdOracle = [php_bin] 583 | cmdOracle.append(oraclePath) 584 | cmdOracle.append("introspect") 585 | cmdOracle.append(lookTerm) 586 | print_debug(cmdOracle+'asdasd') 587 | if os.name == 'nt': 588 | startupinfo = subprocess.STARTUPINFO() 589 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 590 | p = subprocess.Popen(cmdOracle, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=oracleDirNm, shell=False, startupinfo=startupinfo) 591 | else: 592 | p = subprocess.Popen(cmdOracle, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=oracleDirNm, shell=False) 593 | res, err = p.communicate() 594 | 595 | print_debug("phpfmt (introspect): "+res.decode('utf-8')) 596 | print_debug("phpfmt (introspect) err: "+err.decode('utf-8')) 597 | 598 | outputToPanel("phpfmtintrospect", self, edit, "Analysis:\n"+res.decode('utf-8')); 599 | 600 | 601 | lastCalltip = "" 602 | class CalltipCommand(sublime_plugin.TextCommand): 603 | def run(self, edit): 604 | global lastCalltip 605 | uri = self.view.file_name() 606 | dirnm, sfn = os.path.split(uri) 607 | ext = os.path.splitext(uri)[1][1:] 608 | 609 | s = sublime.load_settings('phpfmt.sublime-settings') 610 | 611 | additional_extensions = s.get("additional_extensions", []) 612 | if "php" != ext and not ext in additional_extensions: 613 | return False 614 | 615 | if not lookForOracleFile(self.view): 616 | return False 617 | 618 | lookTerm = (self.view.substr(self.view.word(self.view.sel()[0].a))) 619 | if lastCalltip == lookTerm: 620 | return False 621 | 622 | lastCalltip = lookTerm 623 | 624 | php_bin = s.get("php_bin", "php") 625 | oraclePath = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "oracle.php") 626 | 627 | uri = self.view.file_name() 628 | dirNm, sfn = os.path.split(uri) 629 | ext = os.path.splitext(uri)[1][1:] 630 | 631 | oracleDirNm = dirNm 632 | while oracleDirNm != "/": 633 | oracleFname = oracleDirNm+os.path.sep+"oracle.sqlite" 634 | if os.path.isfile(oracleFname): 635 | break 636 | origOracleDirNm = oracleDirNm 637 | oracleDirNm = os.path.dirname(oracleDirNm) 638 | if origOracleDirNm == oracleDirNm: 639 | break 640 | 641 | cmdOracle = [php_bin] 642 | cmdOracle.append(oraclePath) 643 | cmdOracle.append("calltip") 644 | cmdOracle.append(lookTerm) 645 | if os.name == 'nt': 646 | startupinfo = subprocess.STARTUPINFO() 647 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 648 | p = subprocess.Popen(cmdOracle, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=oracleDirNm, shell=False, startupinfo=startupinfo) 649 | else: 650 | p = subprocess.Popen(cmdOracle, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=oracleDirNm, shell=False) 651 | res, err = p.communicate() 652 | 653 | output = res.decode('utf-8'); 654 | 655 | self.view.set_status("phpfmt", output) 656 | 657 | class DebugEnvCommand(sublime_plugin.TextCommand): 658 | def run(self, edit): 659 | s = sublime.load_settings('phpfmt.sublime-settings') 660 | 661 | php_bin = s.get("php_bin", "php") 662 | formatter_path = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "fmt.phar") 663 | 664 | s = debugEnvironment(php_bin, formatter_path) 665 | sublime.message_dialog(s) 666 | 667 | class FmtNowCommand(sublime_plugin.TextCommand): 668 | def run(self, edit): 669 | vsize = self.view.size() 670 | src = self.view.substr(sublime.Region(0, vsize)) 671 | if not src.strip(): 672 | return 673 | 674 | src = dofmt(self, self.view, None, src, True) 675 | if src is False or src == "": 676 | return False 677 | 678 | _, err = merge(self.view, vsize, src, edit) 679 | print_debug(err) 680 | 681 | class TogglePassMenuCommand(sublime_plugin.TextCommand): 682 | def run(self, edit): 683 | s = sublime.load_settings('phpfmt.sublime-settings') 684 | php_bin = s.get("php_bin", "php") 685 | formatter_path = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "fmt.phar") 686 | 687 | cmd_passes = [php_bin,formatter_path,'--list-simple']; 688 | print_debug(cmd_passes) 689 | 690 | if os.name == 'nt': 691 | startupinfo = subprocess.STARTUPINFO() 692 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 693 | p = subprocess.Popen(cmd_passes, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, startupinfo=startupinfo) 694 | else: 695 | p = subprocess.Popen(cmd_passes, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) 696 | 697 | out, err = p.communicate() 698 | 699 | descriptions = out.decode("utf-8").strip().split(os.linesep) 700 | 701 | def on_done(i): 702 | if i >= 0 : 703 | s = sublime.load_settings('phpfmt.sublime-settings') 704 | passes = s.get('passes', []) 705 | chosenPass = descriptions[i].split(' ') 706 | option = chosenPass[0] 707 | 708 | passDesc = option 709 | 710 | if option in passes: 711 | passes.remove(option) 712 | msg = "phpfmt: "+passDesc+" disabled" 713 | print_debug(msg) 714 | sublime.status_message(msg) 715 | else: 716 | passes.append(option) 717 | msg = "phpfmt: "+passDesc+" enabled" 718 | print_debug(msg) 719 | sublime.status_message(msg) 720 | 721 | s.set('passes', passes) 722 | sublime.save_settings('phpfmt.sublime-settings') 723 | 724 | self.view.window().show_quick_panel(descriptions, on_done, sublime.MONOSPACE_FONT) 725 | 726 | class ToggleExcludeMenuCommand(sublime_plugin.TextCommand): 727 | def run(self, edit): 728 | s = sublime.load_settings('phpfmt.sublime-settings') 729 | php_bin = s.get("php_bin", "php") 730 | formatter_path = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "fmt.phar") 731 | 732 | cmd_passes = [php_bin,formatter_path,'--list-simple']; 733 | print_debug(cmd_passes) 734 | 735 | if os.name == 'nt': 736 | startupinfo = subprocess.STARTUPINFO() 737 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 738 | p = subprocess.Popen(cmd_passes, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, startupinfo=startupinfo) 739 | else: 740 | p = subprocess.Popen(cmd_passes, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) 741 | 742 | out, err = p.communicate() 743 | 744 | descriptions = out.decode("utf-8").strip().split(os.linesep) 745 | 746 | def on_done(i): 747 | if i >= 0 : 748 | s = sublime.load_settings('phpfmt.sublime-settings') 749 | excludes = s.get('excludes', []) 750 | chosenPass = descriptions[i].split(' ') 751 | option = chosenPass[0] 752 | 753 | passDesc = option 754 | 755 | if option in excludes: 756 | excludes.remove(option) 757 | msg = "phpfmt: "+passDesc+" disabled" 758 | print_debug(msg) 759 | sublime.status_message(msg) 760 | else: 761 | excludes.append(option) 762 | msg = "phpfmt: "+passDesc+" enabled" 763 | print_debug(msg) 764 | sublime.status_message(msg) 765 | 766 | s.set('excludes', excludes) 767 | sublime.save_settings('phpfmt.sublime-settings') 768 | 769 | self.view.window().show_quick_panel(descriptions, on_done, sublime.MONOSPACE_FONT) 770 | 771 | class ToggleCommand(sublime_plugin.TextCommand): 772 | def run(self, edit, option): 773 | s = sublime.load_settings('phpfmt.sublime-settings') 774 | options = { 775 | "autocomplete":"autocomplete", 776 | "autoimport":"dependency autoimport", 777 | "enable_auto_align":"auto align", 778 | "format_on_save":"format on save", 779 | "psr1":"PSR1", 780 | "psr1_naming":"PSR1 Class and Method Naming", 781 | "psr2":"PSR2", 782 | "readini":"look for .php.tools.ini", 783 | "smart_linebreak_after_curly":"smart linebreak after curly", 784 | "skip_if_ini_missing":"skip if ini file is missing", 785 | "visibility_order":"visibility order", 786 | "yoda":"yoda mode", 787 | } 788 | s = sublime.load_settings('phpfmt.sublime-settings') 789 | value = s.get(option, False) 790 | 791 | if value: 792 | s.set(option, False) 793 | msg = "phpfmt: "+options[option]+" disabled" 794 | print_debug(msg) 795 | sublime.status_message(msg) 796 | else: 797 | s.set(option, True) 798 | msg = "phpfmt: "+options[option]+" enabled" 799 | print_debug(msg) 800 | sublime.status_message(msg) 801 | 802 | sublime.save_settings('phpfmt.sublime-settings') 803 | 804 | class UpdatePhpBinCommand(sublime_plugin.TextCommand): 805 | def run(self, edit): 806 | def execute(text): 807 | s = sublime.load_settings('phpfmt.sublime-settings') 808 | s.set("php_bin", text) 809 | 810 | s = sublime.load_settings('phpfmt.sublime-settings') 811 | self.view.window().show_input_panel('php binary path:', s.get("php_bin", ""), execute, None, None) 812 | 813 | class OrderMethodCommand(sublime_plugin.TextCommand): 814 | def run(self, edit): 815 | doreordermethod(self, self.view) 816 | 817 | class GeneratePhpdocCommand(sublime_plugin.TextCommand): 818 | def run(self, edit): 819 | dogeneratephpdoc(self, self.view) 820 | 821 | class SgterSnakeCommand(sublime_plugin.TextCommand): 822 | def run(self, edit): 823 | dofmt(self, self.view, 'snake') 824 | 825 | class SgterCamelCommand(sublime_plugin.TextCommand): 826 | def run(self, edit): 827 | dofmt(self, self.view, 'camel') 828 | 829 | class SgterGoCommand(sublime_plugin.TextCommand): 830 | def run(self, edit): 831 | dofmt(self, self.view, 'golang') 832 | 833 | class BuildOracleCommand(sublime_plugin.TextCommand): 834 | def run(self, edit): 835 | def buildDB(): 836 | if self.msgFile is not None: 837 | self.msgFile.window().run_command('close_file') 838 | s = sublime.load_settings('phpfmt.sublime-settings') 839 | php_bin = s.get("php_bin", "php") 840 | oraclePath = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "oracle.php") 841 | cmdOracle = [php_bin] 842 | cmdOracle.append(oraclePath) 843 | cmdOracle.append("flush") 844 | cmdOracle.append(self.dirNm) 845 | if os.name == 'nt': 846 | startupinfo = subprocess.STARTUPINFO() 847 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 848 | p = subprocess.Popen(cmdOracle, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.dirNm, shell=False, startupinfo=startupinfo) 849 | else: 850 | p = subprocess.Popen(cmdOracle, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.dirNm, shell=False) 851 | res, err = p.communicate() 852 | print_debug("phpfmt (oracle): "+res.decode('utf-8')) 853 | print_debug("phpfmt (oracle) err: "+err.decode('utf-8')) 854 | sublime.status_message("phpfmt (oracle): done") 855 | 856 | 857 | #sublime.set_timeout_async(self.long_command, 0) 858 | def askForDirectory(text): 859 | self.dirNm = text 860 | sublime.set_timeout_async(buildDB, 0) 861 | 862 | view = self.view 863 | s = sublime.load_settings('phpfmt.sublime-settings') 864 | php_bin = s.get("php_bin", "php") 865 | 866 | uri = view.file_name() 867 | oracleDirNm, sfn = os.path.split(uri) 868 | originalDirNm = oracleDirNm 869 | 870 | while oracleDirNm != "/": 871 | oracleFname = oracleDirNm+os.path.sep+"oracle.sqlite" 872 | if os.path.isfile(oracleFname): 873 | break 874 | origOracleDirNm = oracleDirNm 875 | oracleDirNm = os.path.dirname(oracleDirNm) 876 | if origOracleDirNm == oracleDirNm: 877 | break 878 | 879 | self.msgFile = None 880 | if not os.path.isfile(oracleFname): 881 | print_debug("phpfmt (oracle file): not found -- dialog") 882 | self.msgFile = self.view.window().open_file(os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "message")) 883 | self.msgFile.set_read_only(True) 884 | self.view.window().show_input_panel('location:', originalDirNm, askForDirectory, None, None) 885 | else: 886 | print_debug("phpfmt (oracle file): "+oracleFname) 887 | print_debug("phpfmt (oracle dir): "+oracleDirNm) 888 | self.dirNm = oracleDirNm 889 | sublime.set_timeout_async(buildDB, 0) 890 | 891 | class IndentWithSpacesCommand(sublime_plugin.TextCommand): 892 | def run(self, edit): 893 | def setIndentWithSpace(text): 894 | s = sublime.load_settings('phpfmt.sublime-settings') 895 | v = text.strip() 896 | if not v: 897 | v = False 898 | else: 899 | v = int(v) 900 | s.set("indent_with_space", v) 901 | sublime.save_settings('phpfmt.sublime-settings') 902 | sublime.status_message("phpfmt (indentation): done") 903 | sublime.active_window().active_view().run_command("fmt_now") 904 | 905 | s = sublime.load_settings('phpfmt.sublime-settings') 906 | spaces = s.get("indent_with_space", 4) 907 | if not spaces: 908 | spaces = "" 909 | spaces = str(spaces) 910 | self.view.window().show_input_panel('how many spaces? (leave it empty to return to tabs)', spaces, setIndentWithSpace, None, None) 911 | 912 | class PHPFmtComplete(sublime_plugin.EventListener): 913 | def on_query_completions(self, view, prefix, locations): 914 | s = sublime.load_settings('phpfmt.sublime-settings') 915 | 916 | autocomplete = s.get("autocomplete", False) 917 | if autocomplete is False: 918 | return [] 919 | 920 | pos = locations[0] 921 | scopes = view.scope_name(pos).split() 922 | if not ('source.php.embedded.block.html' in scopes or 'source.php' in scopes): 923 | return [] 924 | 925 | 926 | print_debug("phpfmt (autocomplete): "+prefix); 927 | 928 | comps = [] 929 | 930 | uri = view.file_name() 931 | dirNm, sfn = os.path.split(uri) 932 | ext = os.path.splitext(uri)[1][1:] 933 | 934 | 935 | oracleDirNm = dirNm 936 | while oracleDirNm != "/": 937 | oracleFname = oracleDirNm+os.path.sep+"oracle.sqlite" 938 | if os.path.isfile(oracleFname): 939 | break 940 | origOracleDirNm = oracleDirNm 941 | oracleDirNm = os.path.dirname(oracleDirNm) 942 | if origOracleDirNm == oracleDirNm: 943 | break 944 | 945 | 946 | if not os.path.isfile(oracleFname): 947 | sublime.status_message("phpfmt: autocomplete database not found") 948 | return [] 949 | 950 | if prefix in "namespace": 951 | ns = dirNm.replace(oracleDirNm, '').replace('/','\\') 952 | if ns.startswith('\\'): 953 | ns = ns[1:] 954 | comps.append(( 955 | '%s \t %s \t %s' % ("namespace", ns, "namespace"), 956 | '%s %s;\n${0}' % ("namespace", ns), 957 | )) 958 | 959 | if prefix in "class": 960 | print_debug("class guess") 961 | className = sfn.split(".")[0] 962 | comps.append(( 963 | '%s \t %s \t %s' % ("class", className, "class"), 964 | '%s %s {\n\t${0}\n}\n' % ("class", className), 965 | )) 966 | 967 | php_bin = s.get("php_bin", "php") 968 | oraclePath = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "oracle.php") 969 | cmdOracle = [php_bin] 970 | cmdOracle.append(oraclePath) 971 | cmdOracle.append("autocomplete") 972 | cmdOracle.append(prefix) 973 | print_debug(cmdOracle) 974 | if os.name == 'nt': 975 | startupinfo = subprocess.STARTUPINFO() 976 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 977 | p = subprocess.Popen(cmdOracle, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=oracleDirNm, shell=False, startupinfo=startupinfo) 978 | else: 979 | p = subprocess.Popen(cmdOracle, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=oracleDirNm, shell=False) 980 | res, err = p.communicate() 981 | print_debug("phpfmt (autocomplete) err: "+err.decode('utf-8')) 982 | 983 | f = res.decode('utf-8').split('\n') 984 | reader = csv.reader(f, delimiter=',') 985 | for row in reader: 986 | if len(row) > 0: 987 | if "class" == row[3]: 988 | comps.append(( 989 | '%s \t %s \t %s' % (row[1], row[0], "class"), 990 | '%s(${0})' % (row[1]), 991 | )) 992 | comps.append(( 993 | '%s \t %s \t %s' % (row[0], row[0], "class"), 994 | '%s(${0})' % (row[0]), 995 | )) 996 | if "method" == row[3]: 997 | comps.append(( 998 | '%s \t %s \t %s' % (row[1], row[2], "method"), 999 | '%s' % (row[0].replace('$','\$')), 1000 | )) 1001 | 1002 | return comps 1003 | 1004 | s = sublime.load_settings('phpfmt.sublime-settings') 1005 | version = s.get('version', 1) 1006 | s.set('version', version) 1007 | sublime.save_settings('phpfmt.sublime-settings') 1008 | 1009 | if version == 2: 1010 | # Convert to version 3 1011 | print_debug("Convert to version 3") 1012 | s.set('version', 3) 1013 | sublime.save_settings('phpfmt.sublime-settings') 1014 | 1015 | if version == 3: 1016 | # Convert to version 3 1017 | print_debug("Convert to version 4") 1018 | s.set('version', 4) 1019 | passes = s.get('passes', []) 1020 | passes.append("ReindentSwitchBlocks") 1021 | s.set('passes', passes) 1022 | sublime.save_settings('phpfmt.sublime-settings') 1023 | 1024 | 1025 | # def selfupdate(): 1026 | # s = sublime.load_settings('phpfmt.sublime-settings') 1027 | # php_bin = s.get("php_bin", "php") 1028 | # formatter_path = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "fmt.phar") 1029 | 1030 | # print_debug("Selfupdate") 1031 | # cmd_update = [php_bin, formatter_path, '--selfupdate'] 1032 | # if os.name == 'nt': 1033 | # startupinfo = subprocess.STARTUPINFO() 1034 | # startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 1035 | # p = subprocess.Popen(cmd_update, shell=False, startupinfo=startupinfo) 1036 | # else: 1037 | # p = subprocess.Popen(cmd_update, shell=False) 1038 | 1039 | # sublime.set_timeout(selfupdate, 3000) 1040 | 1041 | 1042 | def _ct_poller(): 1043 | s = sublime.load_settings('phpfmt.sublime-settings') 1044 | if s.get("calltip", False): 1045 | try: 1046 | view = sublime.active_window().active_view() 1047 | view.run_command('calltip') 1048 | except Exception: 1049 | pass 1050 | sublime.set_timeout(_ct_poller, 5000) 1051 | 1052 | _ct_poller() 1053 | 1054 | 1055 | class PhpFmtCommand(sublime_plugin.TextCommand): 1056 | def run(self, edit): 1057 | vsize = self.view.size() 1058 | src = self.view.substr(sublime.Region(0, vsize)) 1059 | if not src.strip(): 1060 | return 1061 | 1062 | src = dofmt(self, self.view, None, src) 1063 | if src is False or src == "": 1064 | return False 1065 | 1066 | _, err = merge(self.view, vsize, src, edit) 1067 | print_debug(err) 1068 | 1069 | class MergeException(Exception): 1070 | pass 1071 | 1072 | def _merge(view, size, text, edit): 1073 | def ss(start, end): 1074 | return view.substr(sublime.Region(start, end)) 1075 | dmp = diff_match_patch() 1076 | diffs = dmp.diff_main(ss(0, size), text, False) 1077 | dmp.diff_cleanupEfficiency(diffs) 1078 | i = 0 1079 | dirty = False 1080 | for d in diffs: 1081 | k, s = d 1082 | l = len(s) 1083 | if k == 0: 1084 | # match 1085 | l = len(s) 1086 | if ss(i, i+l) != s: 1087 | raise MergeException('mismatch', dirty) 1088 | i += l 1089 | else: 1090 | dirty = True 1091 | if k > 0: 1092 | # insert 1093 | view.insert(edit, i, s) 1094 | i += l 1095 | else: 1096 | # delete 1097 | if ss(i, i+l) != s: 1098 | raise MergeException('mismatch', dirty) 1099 | view.erase(edit, sublime.Region(i, i+l)) 1100 | return dirty 1101 | 1102 | def merge(view, size, text, edit): 1103 | vs = view.settings() 1104 | ttts = vs.get("translate_tabs_to_spaces") 1105 | vs.set("translate_tabs_to_spaces", False) 1106 | origin_src = view.substr(sublime.Region(0, view.size())) 1107 | if not origin_src.strip(): 1108 | vs.set("translate_tabs_to_spaces", ttts) 1109 | return (False, '') 1110 | 1111 | try: 1112 | dirty = False 1113 | err = '' 1114 | if size < 0: 1115 | size = view.size() 1116 | dirty = _merge(view, size, text, edit) 1117 | except MergeException as ex: 1118 | dirty = True 1119 | err = "Could not merge changes into the buffer, edit aborted: %s" % ex[0] 1120 | view.replace(edit, sublime.Region(0, view.size()), origin_src) 1121 | except Exception as ex: 1122 | err = "error: %s" % ex 1123 | finally: 1124 | vs.set("translate_tabs_to_spaces", ttts) 1125 | return (dirty, err) 1126 | -------------------------------------------------------------------------------- /phpfmt.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | // "php_bin":"/usr/local/bin/php", 4 | // "format_on_save":true, 5 | "option": "value" 6 | } 7 | -------------------------------------------------------------------------------- /refactor.php: -------------------------------------------------------------------------------- 1 | "); 39 | define("ST_IS_SMALLER", "<"); 40 | define("ST_MINUS", "-"); 41 | define("ST_MODULUS", "%"); 42 | define("ST_PARENTHESES_CLOSE", ")"); 43 | define("ST_PARENTHESES_OPEN", "("); 44 | define("ST_PLUS", "+"); 45 | define("ST_QUESTION", "?"); 46 | define("ST_QUOTE", '"'); 47 | define("ST_REFERENCE", "&"); 48 | define("ST_SEMI_COLON", ";"); 49 | define("ST_TIMES", "*"); 50 | if (!defined("T_POW")) { 51 | define("T_POW", "**"); 52 | } 53 | if (!defined("T_YIELD")) { 54 | define("T_YIELD", "yield"); 55 | } 56 | if (!defined("T_FINALLY")) { 57 | define("T_FINALLY", "finally"); 58 | }; 59 | abstract class FormatterPass { 60 | protected $indent_size = 1; 61 | protected $indent_char = "\t"; 62 | protected $block_size = 1; 63 | protected $new_line = "\n"; 64 | protected $indent = 0; 65 | protected $for_idx = 0; 66 | protected $code = ''; 67 | protected $ptr = 0; 68 | protected $tkns = []; 69 | 70 | abstract public function format($source); 71 | protected function get_token($token) { 72 | if (is_string($token)) { 73 | return [$token, $token]; 74 | } else { 75 | return $token; 76 | } 77 | } 78 | protected function append_code($code = "", $trim = true) { 79 | if ($trim) { 80 | $this->code = rtrim($this->code) . $code; 81 | } else { 82 | $this->code .= $code; 83 | } 84 | } 85 | protected function get_crlf_indent($in_for = false, $increment = 0) { 86 | if ($in_for) { 87 | ++$this->for_idx; 88 | if ($this->for_idx > 2) { 89 | $this->for_idx = 0; 90 | } 91 | } 92 | if ($this->for_idx === 0 || !$in_for) { 93 | return $this->get_crlf() . $this->get_indent($increment); 94 | } else { 95 | return $this->get_space(false); 96 | } 97 | } 98 | protected function get_crlf($true = true) { 99 | return $true ? $this->new_line : ""; 100 | } 101 | protected function get_space($true = true) { 102 | return $true ? " " : ""; 103 | } 104 | protected function get_indent($increment = 0) { 105 | return str_repeat($this->indent_char, ($this->indent + $increment) * $this->indent_size); 106 | } 107 | protected function set_indent($increment) { 108 | $this->indent += $increment; 109 | if ($this->indent < 0) { 110 | $this->indent = 0; 111 | } 112 | } 113 | protected function inspect_token($delta = 1) { 114 | if (!isset($this->tkns[$this->ptr + $delta])) { 115 | return [null, null]; 116 | } 117 | return $this->get_token($this->tkns[$this->ptr + $delta]); 118 | } 119 | protected function is_token($token, $prev = false) { 120 | 121 | $i = $this->ptr; 122 | if ($prev) { 123 | while (--$i >= 0 && is_array($this->tkns[$i]) && $this->tkns[$i][0] === T_WHITESPACE); 124 | } else { 125 | while (++$i < sizeof($this->tkns) - 1 && is_array($this->tkns[$i]) && $this->tkns[$i][0] === T_WHITESPACE); 126 | } 127 | 128 | if (!isset($this->tkns[$i])) { 129 | return false; 130 | } 131 | 132 | $found_token = $this->tkns[$i]; 133 | if (is_string($found_token) && $found_token === $token) { 134 | return true; 135 | } elseif (is_array($token) && is_array($found_token)) { 136 | if (in_array($found_token[0], $token)) { 137 | return true; 138 | } elseif ($prev && $found_token[0] === T_OPEN_TAG) { 139 | return true; 140 | } 141 | } elseif (is_array($token) && is_string($found_token) && in_array($found_token, $token)) { 142 | return true; 143 | } 144 | return false; 145 | } 146 | protected function prev_token() { 147 | $i = $this->ptr; 148 | while (--$i >= 0 && is_array($this->tkns[$i]) && $this->tkns[$i][0] === T_WHITESPACE); 149 | return $this->tkns[$i]; 150 | } 151 | protected function has_ln_after() { 152 | $id = null; 153 | $text = null; 154 | list($id, $text) = $this->inspect_token(); 155 | return T_WHITESPACE === $id && substr_count($text, $this->new_line) > 0; 156 | } 157 | protected function has_ln_before() { 158 | $id = null; 159 | $text = null; 160 | list($id, $text) = $this->inspect_token(-1); 161 | return T_WHITESPACE === $id && substr_count($text, $this->new_line) > 0; 162 | } 163 | protected function has_ln_prev_token() { 164 | list($id, $text) = $this->get_token($this->prev_token()); 165 | return substr_count($text, $this->new_line) > 0; 166 | } 167 | protected function substr_count_trailing($haystack, $needle) { 168 | $cnt = 0; 169 | $i = strlen($haystack) - 1; 170 | for ($i = $i; $i >= 0;--$i) { 171 | $char = substr($haystack, $i, 1); 172 | if ($needle === $char) { 173 | ++$cnt; 174 | } elseif (' ' !== $char && "\t" !== $char) { 175 | break; 176 | } 177 | } 178 | return $cnt; 179 | } 180 | protected function printUntilTheEndOfString() { 181 | while (list($index, $token) = each($this->tkns)) { 182 | list($id, $text) = $this->get_token($token); 183 | $this->ptr = $index; 184 | $this->append_code($text, false); 185 | if (ST_QUOTE == $id) { 186 | break; 187 | } 188 | } 189 | } 190 | protected function walk_until($tknid) { 191 | while (list($index, $token) = each($this->tkns)) { 192 | list($id, $text) = $this->get_token($token); 193 | $this->ptr = $index; 194 | if ($id == $tknid) { 195 | return [$id, $text]; 196 | } 197 | } 198 | } 199 | }; 200 | final class RefactorPass extends FormatterPass { 201 | private $from; 202 | private $to; 203 | public function __construct($from, $to) { 204 | $this->setFrom($from); 205 | $this->setTo($to); 206 | } 207 | private function setFrom($from) { 208 | $tkns = token_get_all('get_token($v); 212 | }, $tkns); 213 | $this->from = $tkns; 214 | return $this; 215 | } 216 | private function getFrom() { 217 | return $this->from; 218 | } 219 | private function setTo($to) { 220 | $tkns = token_get_all('get_token($v); 224 | }, $tkns); 225 | $this->to = $tkns; 226 | return $this; 227 | } 228 | private function getTo() { 229 | return $this->to; 230 | } 231 | 232 | public function format($source) { 233 | $from = $this->getFrom(); 234 | $from_size = sizeof($from); 235 | $from_str = implode('', array_map(function ($v) { 236 | return $v[1]; 237 | }, $from)); 238 | $to = $this->getTo(); 239 | $to_str = implode('', array_map(function ($v) { 240 | return $v[1]; 241 | }, $to)); 242 | 243 | $this->tkns = token_get_all($source); 244 | $this->code = ''; 245 | while (list($index, $token) = each($this->tkns)) { 246 | list($id, $text) = $this->get_token($token); 247 | $this->ptr = $index; 248 | 249 | if ($id == $from[0][0]) { 250 | $match = true; 251 | $buffer = $text; 252 | $i = 1; 253 | for ($i = 1; $i < $from_size; ++$i) { 254 | list($index, $token) = each($this->tkns); 255 | $this->ptr = $index; 256 | list($id, $text) = $this->get_token($token); 257 | $buffer .= $text; 258 | if ($id != $from[$i][0]) { 259 | $match = false; 260 | break; 261 | } 262 | } 263 | if ($match) { 264 | $buffer = str_replace($from_str, $to_str, $buffer); 265 | } 266 | $this->append_code($buffer, false); 267 | } else { 268 | $this->append_code($text, false); 269 | } 270 | } 271 | return $this->code; 272 | } 273 | }; 274 | 275 | final class CodeFormatter { 276 | private $passes = []; 277 | private $debug = false; 278 | public function __construct($debug = false) { 279 | $this->debug = (bool) $debug; 280 | } 281 | public function addPass(FormatterPass $pass) { 282 | $this->passes[] = $pass; 283 | } 284 | 285 | public function formatCode($source = '') { 286 | gc_enable(); 287 | $passes = array_map( 288 | function ($pass) { 289 | return clone $pass; 290 | }, 291 | $this->passes 292 | ); 293 | while (($pass = array_shift($passes))) { 294 | $source = $pass->format($source); 295 | gc_collect_cycles(); 296 | } 297 | gc_disable(); 298 | return $source; 299 | } 300 | } 301 | if (!isset($testEnv)) { 302 | $opts = getopt('ho:', ['from:', 'to:', 'help']); 303 | if (isset($opts['h']) || isset($opts['help'])) { 304 | echo 'Usage: ' . $argv[0] . ' [-ho] [--from=from --to=to] ', PHP_EOL; 305 | $options = [ 306 | '--from=from, --to=to' => 'Search for "from" and replace with "to" - context aware search and replace', 307 | '-h, --help' => 'this help message', 308 | '-o=file' => 'output the formatted code to "file"', 309 | ]; 310 | $maxLen = max(array_map(function ($v) { 311 | return strlen($v); 312 | }, array_keys($options))); 313 | foreach ($options as $k => $v) { 314 | echo ' ', str_pad($k, $maxLen), ' ', $v, PHP_EOL; 315 | } 316 | echo PHP_EOL, 'If is blank, it reads from stdin', PHP_EOL; 317 | die(); 318 | } 319 | if (isset($opts['from']) && !isset($opts['to'])) { 320 | fwrite(STDERR, "Refactor must have --from and --to parameters" . PHP_EOL); 321 | exit(255); 322 | } 323 | 324 | $debug = false; 325 | 326 | $fmt = new CodeFormatter($debug); 327 | 328 | if (isset($opts['from']) && isset($opts['to'])) { 329 | $argv = array_values( 330 | array_filter($argv, 331 | function ($v) { 332 | $param_from = '--from'; 333 | $param_to = '--to'; 334 | return substr($v, 0, strlen($param_from)) !== $param_from && substr($v, 0, strlen($param_to)) !== $param_to; 335 | } 336 | ) 337 | ); 338 | $fmt->addPass(new RefactorPass($opts['from'], $opts['to'])); 339 | } 340 | 341 | if (isset($opts['o'])) { 342 | unset($argv[1]); 343 | unset($argv[2]); 344 | $argv = array_values($argv); 345 | file_put_contents($opts['o'], $fmt->formatCode(file_get_contents($argv[1]))); 346 | } elseif (isset($argv[1]) && is_file($argv[1])) { 347 | echo $fmt->formatCode(file_get_contents($argv[1])); 348 | } elseif (isset($argv[1]) && is_dir($argv[1])) { 349 | $dir = new RecursiveDirectoryIterator($argv[1]); 350 | $it = new RecursiveIteratorIterator($dir); 351 | $files = new RegexIterator($it, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH); 352 | foreach ($files as $file) { 353 | $file = $file[0]; 354 | echo $file; 355 | file_put_contents($file . '-tmp', $fmt->formatCode(file_get_contents($file))); 356 | rename($file, $file . '~'); 357 | rename($file . '-tmp', $file); 358 | echo PHP_EOL; 359 | } 360 | } else { 361 | echo $fmt->formatCode(file_get_contents('php://stdin')); 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 803.10.2 --------------------------------------------------------------------------------