├── .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 "=" to " 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 | Before |
139 | After |
140 |
141 |
142 |
143 | <?php
144 | for($i = 0; $i < 10; $i++)
145 | {
146 | if($i%2==0)
147 | echo "Flipflop";
148 | }
149 |
150 | |
151 |
152 | <?php
153 | for ($i = 0; $i < 10; $i++) {
154 | if ($i%2 == 0) {
155 | echo "Flipflop";
156 | }
157 | }
158 |
159 | |
160 |
161 |
162 |
163 | <?php
164 | $a = 10;
165 | $otherVar = 20;
166 | $third = 30;
167 |
168 | |
169 |
170 | <?php
171 | $a = 10;
172 | $otherVar = 20;
173 | $third = 30;
174 |
175 | This can be enabled with the option "enable_auto_align"
176 | |
177 |
178 |
179 |
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 | |
192 |
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 | |
206 |
207 |
208 |
209 | ### What does it do? - PSR version
210 |
211 |
212 |
213 | Before |
214 | After |
215 |
216 |
217 |
218 | <?php
219 | for($i = 0; $i < 10; $i++)
220 | {
221 | if($i%2==0)
222 | echo "Flipflop";
223 | }
224 |
225 | |
226 |
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 | |
236 |
237 |
238 |
239 | <?php
240 | class A {
241 | function a(){
242 | return 10;
243 | }
244 | }
245 |
246 | |
247 |
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 | |
259 |
260 |
261 |
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 | |
274 |
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 | |
288 |
289 |
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 | Before |
53 | After |
54 |
55 |
56 |
57 | <?php
58 | for($i = 0; $i < 10; $i++)
59 | {
60 | if($i%2==0)
61 | echo "Flipflop";
62 | }
63 |
64 | |
65 |
66 | <?php
67 | for ($i = 0; $i < 10; $i++) {
68 | if ($i%2 == 0) {
69 | echo "Flipflop";
70 | }
71 | }
72 |
73 | |
74 |
75 |
76 |
77 | <?php
78 | $a = 10;
79 | $otherVar = 20;
80 | $third = 30;
81 |
82 | |
83 |
84 | <?php
85 | $a = 10;
86 | $otherVar = 20;
87 | $third = 30;
88 |
89 | This can be enabled with the option "enable_auto_align"
90 | |
91 |
92 |
93 |
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 | |
106 |
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 | |
120 |
121 |
122 |
123 | ### What does it do? - PSR version
124 |
125 |
126 |
127 | Before |
128 | After |
129 |
130 |
131 |
132 | <?php
133 | for($i = 0; $i < 10; $i++)
134 | {
135 | if($i%2==0)
136 | echo "Flipflop";
137 | }
138 |
139 | |
140 |
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 | |
150 |
151 |
152 |
153 | <?php
154 | class A {
155 | function a(){
156 | return 10;
157 | }
158 | }
159 |
160 | |
161 |
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 | |
173 |
174 |
175 |
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 | |
188 |
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 | |
202 |
203 |
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
--------------------------------------------------------------------------------