├── .no-sublime-package
├── Default (Linux).sublime-keymap
├── Default (OSX).sublime-keymap
├── Default (Windows).sublime-keymap
├── Default.sublime-commands
├── Main.sublime-menu
├── README.md
├── diff_match_patch
├── __init__.py
├── python2
│ ├── __init__.py
│ └── diff_match_patch.py
└── python3
│ ├── __init__.py
│ └── diff_match_patch.py
├── 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
├── 12.2.0.txt
├── 14.0.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
├── php.tools.ini
├── phpfmt.py
└── phpfmt.sublime-settings
/.no-sublime-package:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nanch/sublime-phpfmt/cae557c18698dc6d906a80c9677fa77e1ab09997/.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 | {"caption": "phpfmt: toggle additional transformations", "command": "toggle_pass_menu" },
5 | {"caption": "phpfmt: toggle excluded transformations", "command": "toggle_exclude_menu" },
6 | {"caption": "phpfmt: toggle format on save", "command": "toggle", "args": {"option":"format_on_save"}},
7 | {"caption": "phpfmt: reorganize content of class", "command": "order_method"},
8 | {"caption": "phpfmt: troubleshoot information", "command": "debug_env"},
9 | {"caption": "phpfmt: update PHP binary path", "command": "update_php_bin"}
10 | ]
11 |
--------------------------------------------------------------------------------
/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 | This is a fork of sublime-phpfmt plugin just before it was deleted. Relevant post at https://news.ycombinator.com/item?id=11896851
2 |
3 | ---
4 |
5 | 2016-06-14:
6 |
7 | Commit 6125cf9 is available at https://github.com/nanch/phpfmt_stable and is the current reference for the sublime text phpfmt package on packagecontrol: https://packagecontrol.io/packages/phpfmt
8 |
9 | Installing from packagecontrol is the recommended method.
10 |
11 |
12 |
13 |
14 | ---
15 |
16 | 2016-06-13:
17 |
18 | How to revert to an older version of phpfmt manually:
19 | - To revert to build 6125cf9 available at https://github.com/nanch/sublime-phpfmt/tree/6125cf9058c0666f06ed758f0f5451996f7c7211.
20 | - Clone commit 6125cf9 into C:\Users\Me\AppData\Roaming\Sublime Text 3\Packages\phpfmt and everything will work fine again.
21 |
22 |
--------------------------------------------------------------------------------
/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/python2/diff_match_patch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python2.4
2 |
3 | from __future__ import division
4 |
5 | """Diff Match and Patch
6 |
7 | Copyright 2006 Google Inc.
8 | http://code.google.com/p/google-diff-match-patch/
9 |
10 | Licensed under the Apache License, Version 2.0 (the "License");
11 | you may not use this file except in compliance with the License.
12 | You may obtain a copy of the License at
13 |
14 | http://www.apache.org/licenses/LICENSE-2.0
15 |
16 | Unless required by applicable law or agreed to in writing, software
17 | distributed under the License is distributed on an "AS IS" BASIS,
18 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19 | See the License for the specific language governing permissions and
20 | limitations under the License.
21 | """
22 |
23 | """Functions for diff, match and patch.
24 |
25 | Computes the difference between two texts to create a patch.
26 | Applies the patch onto another text, allowing for errors.
27 | """
28 |
29 | __author__ = 'fraser@google.com (Neil Fraser)'
30 |
31 | import math
32 | import re
33 | import sys
34 | import time
35 | import urllib
36 |
37 | class diff_match_patch:
38 | """Class containing the diff, match and patch methods.
39 |
40 | Also contains the behaviour settings.
41 | """
42 |
43 | def __init__(self):
44 | """Inits a diff_match_patch object with default settings.
45 | Redefine these in your program to override the defaults.
46 | """
47 |
48 | # Number of seconds to map a diff before giving up (0 for infinity).
49 | self.Diff_Timeout = 1.0
50 | # Cost of an empty edit operation in terms of edit characters.
51 | self.Diff_EditCost = 4
52 | # At what point is no match declared (0.0 = perfection, 1.0 = very loose).
53 | self.Match_Threshold = 0.5
54 | # How far to search for a match (0 = exact location, 1000+ = broad match).
55 | # A match this many characters away from the expected location will add
56 | # 1.0 to the score (0.0 is a perfect match).
57 | self.Match_Distance = 1000
58 | # When deleting a large block of text (over ~64 characters), how close do
59 | # the contents have to be to match the expected contents. (0.0 = perfection,
60 | # 1.0 = very loose). Note that Match_Threshold controls how closely the
61 | # end points of a delete need to match.
62 | self.Patch_DeleteThreshold = 0.5
63 | # Chunk size for context length.
64 | self.Patch_Margin = 4
65 |
66 | # The number of bits in an int.
67 | # Python has no maximum, thus to disable patch splitting set to 0.
68 | # However to avoid long patches in certain pathological cases, use 32.
69 | # Multiple short patches (using native ints) are much faster than long ones.
70 | self.Match_MaxBits = 32
71 |
72 | # DIFF FUNCTIONS
73 |
74 | # The data structure representing a diff is an array of tuples:
75 | # [(DIFF_DELETE, "Hello"), (DIFF_INSERT, "Goodbye"), (DIFF_EQUAL, " world.")]
76 | # which means: delete "Hello", add "Goodbye" and keep " world."
77 | DIFF_DELETE = -1
78 | DIFF_INSERT = 1
79 | DIFF_EQUAL = 0
80 |
81 | def diff_main(self, text1, text2, checklines=True, deadline=None):
82 | """Find the differences between two texts. Simplifies the problem by
83 | stripping any common prefix or suffix off the texts before diffing.
84 |
85 | Args:
86 | text1: Old string to be diffed.
87 | text2: New string to be diffed.
88 | checklines: Optional speedup flag. If present and false, then don't run
89 | a line-level diff first to identify the changed areas.
90 | Defaults to true, which does a faster, slightly less optimal diff.
91 | deadline: Optional time when the diff should be complete by. Used
92 | internally for recursive calls. Users should set DiffTimeout instead.
93 |
94 | Returns:
95 | Array of changes.
96 | """
97 | # Set a deadline by which time the diff must be complete.
98 | if deadline == None:
99 | # Unlike in most languages, Python counts time in seconds.
100 | if self.Diff_Timeout <= 0:
101 | deadline = sys.maxint
102 | else:
103 | deadline = time.time() + self.Diff_Timeout
104 |
105 | # Check for null inputs.
106 | if text1 == None or text2 == None:
107 | raise ValueError("Null inputs. (diff_main)")
108 |
109 | # Check for equality (speedup).
110 | if text1 == text2:
111 | if text1:
112 | return [(self.DIFF_EQUAL, text1)]
113 | return []
114 |
115 | # Trim off common prefix (speedup).
116 | commonlength = self.diff_commonPrefix(text1, text2)
117 | commonprefix = text1[:commonlength]
118 | text1 = text1[commonlength:]
119 | text2 = text2[commonlength:]
120 |
121 | # Trim off common suffix (speedup).
122 | commonlength = self.diff_commonSuffix(text1, text2)
123 | if commonlength == 0:
124 | commonsuffix = ''
125 | else:
126 | commonsuffix = text1[-commonlength:]
127 | text1 = text1[:-commonlength]
128 | text2 = text2[:-commonlength]
129 |
130 | # Compute the diff on the middle block.
131 | diffs = self.diff_compute(text1, text2, checklines, deadline)
132 |
133 | # Restore the prefix and suffix.
134 | if commonprefix:
135 | diffs[:0] = [(self.DIFF_EQUAL, commonprefix)]
136 | if commonsuffix:
137 | diffs.append((self.DIFF_EQUAL, commonsuffix))
138 | self.diff_cleanupMerge(diffs)
139 | return diffs
140 |
141 | def diff_compute(self, text1, text2, checklines, deadline):
142 | """Find the differences between two texts. Assumes that the texts do not
143 | have any common prefix or suffix.
144 |
145 | Args:
146 | text1: Old string to be diffed.
147 | text2: New string to be diffed.
148 | checklines: Speedup flag. If false, then don't run a line-level diff
149 | first to identify the changed areas.
150 | If true, then run a faster, slightly less optimal diff.
151 | deadline: Time when the diff should be complete by.
152 |
153 | Returns:
154 | Array of changes.
155 | """
156 | if not text1:
157 | # Just add some text (speedup).
158 | return [(self.DIFF_INSERT, text2)]
159 |
160 | if not text2:
161 | # Just delete some text (speedup).
162 | return [(self.DIFF_DELETE, text1)]
163 |
164 | if len(text1) > len(text2):
165 | (longtext, shorttext) = (text1, text2)
166 | else:
167 | (shorttext, longtext) = (text1, text2)
168 | i = longtext.find(shorttext)
169 | if i != -1:
170 | # Shorter text is inside the longer text (speedup).
171 | diffs = [(self.DIFF_INSERT, longtext[:i]), (self.DIFF_EQUAL, shorttext),
172 | (self.DIFF_INSERT, longtext[i + len(shorttext):])]
173 | # Swap insertions for deletions if diff is reversed.
174 | if len(text1) > len(text2):
175 | diffs[0] = (self.DIFF_DELETE, diffs[0][1])
176 | diffs[2] = (self.DIFF_DELETE, diffs[2][1])
177 | return diffs
178 |
179 | if len(shorttext) == 1:
180 | # Single character string.
181 | # After the previous speedup, the character can't be an equality.
182 | return [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)]
183 |
184 | # Check to see if the problem can be split in two.
185 | hm = self.diff_halfMatch(text1, text2)
186 | if hm:
187 | # A half-match was found, sort out the return data.
188 | (text1_a, text1_b, text2_a, text2_b, mid_common) = hm
189 | # Send both pairs off for separate processing.
190 | diffs_a = self.diff_main(text1_a, text2_a, checklines, deadline)
191 | diffs_b = self.diff_main(text1_b, text2_b, checklines, deadline)
192 | # Merge the results.
193 | return diffs_a + [(self.DIFF_EQUAL, mid_common)] + diffs_b
194 |
195 | if checklines and len(text1) > 100 and len(text2) > 100:
196 | return self.diff_lineMode(text1, text2, deadline)
197 |
198 | return self.diff_bisect(text1, text2, deadline)
199 |
200 | def diff_lineMode(self, text1, text2, deadline):
201 | """Do a quick line-level diff on both strings, then rediff the parts for
202 | greater accuracy.
203 | This speedup can produce non-minimal diffs.
204 |
205 | Args:
206 | text1: Old string to be diffed.
207 | text2: New string to be diffed.
208 | deadline: Time when the diff should be complete by.
209 |
210 | Returns:
211 | Array of changes.
212 | """
213 |
214 | # Scan the text on a line-by-line basis first.
215 | (text1, text2, linearray) = self.diff_linesToChars(text1, text2)
216 |
217 | diffs = self.diff_main(text1, text2, False, deadline)
218 |
219 | # Convert the diff back to original text.
220 | self.diff_charsToLines(diffs, linearray)
221 | # Eliminate freak matches (e.g. blank lines)
222 | self.diff_cleanupSemantic(diffs)
223 |
224 | # Rediff any replacement blocks, this time character-by-character.
225 | # Add a dummy entry at the end.
226 | diffs.append((self.DIFF_EQUAL, ''))
227 | pointer = 0
228 | count_delete = 0
229 | count_insert = 0
230 | text_delete = ''
231 | text_insert = ''
232 | while pointer < len(diffs):
233 | if diffs[pointer][0] == self.DIFF_INSERT:
234 | count_insert += 1
235 | text_insert += diffs[pointer][1]
236 | elif diffs[pointer][0] == self.DIFF_DELETE:
237 | count_delete += 1
238 | text_delete += diffs[pointer][1]
239 | elif diffs[pointer][0] == self.DIFF_EQUAL:
240 | # Upon reaching an equality, check for prior redundancies.
241 | if count_delete >= 1 and count_insert >= 1:
242 | # Delete the offending records and add the merged ones.
243 | a = self.diff_main(text_delete, text_insert, False, deadline)
244 | diffs[pointer - count_delete - count_insert : pointer] = a
245 | pointer = pointer - count_delete - count_insert + len(a)
246 | count_insert = 0
247 | count_delete = 0
248 | text_delete = ''
249 | text_insert = ''
250 |
251 | pointer += 1
252 |
253 | diffs.pop() # Remove the dummy entry at the end.
254 |
255 | return diffs
256 |
257 | def diff_bisect(self, text1, text2, deadline):
258 | """Find the 'middle snake' of a diff, split the problem in two
259 | and return the recursively constructed diff.
260 | See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
261 |
262 | Args:
263 | text1: Old string to be diffed.
264 | text2: New string to be diffed.
265 | deadline: Time at which to bail if not yet complete.
266 |
267 | Returns:
268 | Array of diff tuples.
269 | """
270 |
271 | # Cache the text lengths to prevent multiple calls.
272 | text1_length = len(text1)
273 | text2_length = len(text2)
274 | max_d = (text1_length + text2_length + 1) // 2
275 | v_offset = max_d
276 | v_length = 2 * max_d
277 | v1 = [-1] * v_length
278 | v1[v_offset + 1] = 0
279 | v2 = v1[:]
280 | delta = text1_length - text2_length
281 | # If the total number of characters is odd, then the front path will
282 | # collide with the reverse path.
283 | front = (delta % 2 != 0)
284 | # Offsets for start and end of k loop.
285 | # Prevents mapping of space beyond the grid.
286 | k1start = 0
287 | k1end = 0
288 | k2start = 0
289 | k2end = 0
290 | for d in xrange(max_d):
291 | # Bail out if deadline is reached.
292 | if time.time() > deadline:
293 | break
294 |
295 | # Walk the front path one step.
296 | for k1 in xrange(-d + k1start, d + 1 - k1end, 2):
297 | k1_offset = v_offset + k1
298 | if k1 == -d or (k1 != d and
299 | v1[k1_offset - 1] < v1[k1_offset + 1]):
300 | x1 = v1[k1_offset + 1]
301 | else:
302 | x1 = v1[k1_offset - 1] + 1
303 | y1 = x1 - k1
304 | while (x1 < text1_length and y1 < text2_length and
305 | text1[x1] == text2[y1]):
306 | x1 += 1
307 | y1 += 1
308 | v1[k1_offset] = x1
309 | if x1 > text1_length:
310 | # Ran off the right of the graph.
311 | k1end += 2
312 | elif y1 > text2_length:
313 | # Ran off the bottom of the graph.
314 | k1start += 2
315 | elif front:
316 | k2_offset = v_offset + delta - k1
317 | if k2_offset >= 0 and k2_offset < v_length and v2[k2_offset] != -1:
318 | # Mirror x2 onto top-left coordinate system.
319 | x2 = text1_length - v2[k2_offset]
320 | if x1 >= x2:
321 | # Overlap detected.
322 | return self.diff_bisectSplit(text1, text2, x1, y1, deadline)
323 |
324 | # Walk the reverse path one step.
325 | for k2 in xrange(-d + k2start, d + 1 - k2end, 2):
326 | k2_offset = v_offset + k2
327 | if k2 == -d or (k2 != d and
328 | v2[k2_offset - 1] < v2[k2_offset + 1]):
329 | x2 = v2[k2_offset + 1]
330 | else:
331 | x2 = v2[k2_offset - 1] + 1
332 | y2 = x2 - k2
333 | while (x2 < text1_length and y2 < text2_length and
334 | text1[-x2 - 1] == text2[-y2 - 1]):
335 | x2 += 1
336 | y2 += 1
337 | v2[k2_offset] = x2
338 | if x2 > text1_length:
339 | # Ran off the left of the graph.
340 | k2end += 2
341 | elif y2 > text2_length:
342 | # Ran off the top of the graph.
343 | k2start += 2
344 | elif not front:
345 | k1_offset = v_offset + delta - k2
346 | if k1_offset >= 0 and k1_offset < v_length and v1[k1_offset] != -1:
347 | x1 = v1[k1_offset]
348 | y1 = v_offset + x1 - k1_offset
349 | # Mirror x2 onto top-left coordinate system.
350 | x2 = text1_length - x2
351 | if x1 >= x2:
352 | # Overlap detected.
353 | return self.diff_bisectSplit(text1, text2, x1, y1, deadline)
354 |
355 | # Diff took too long and hit the deadline or
356 | # number of diffs equals number of characters, no commonality at all.
357 | return [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)]
358 |
359 | def diff_bisectSplit(self, text1, text2, x, y, deadline):
360 | """Given the location of the 'middle snake', split the diff in two parts
361 | and recurse.
362 |
363 | Args:
364 | text1: Old string to be diffed.
365 | text2: New string to be diffed.
366 | x: Index of split point in text1.
367 | y: Index of split point in text2.
368 | deadline: Time at which to bail if not yet complete.
369 |
370 | Returns:
371 | Array of diff tuples.
372 | """
373 | text1a = text1[:x]
374 | text2a = text2[:y]
375 | text1b = text1[x:]
376 | text2b = text2[y:]
377 |
378 | # Compute both diffs serially.
379 | diffs = self.diff_main(text1a, text2a, False, deadline)
380 | diffsb = self.diff_main(text1b, text2b, False, deadline)
381 |
382 | return diffs + diffsb
383 |
384 | def diff_linesToChars(self, text1, text2):
385 | """Split two texts into an array of strings. Reduce the texts to a string
386 | of hashes where each Unicode character represents one line.
387 |
388 | Args:
389 | text1: First string.
390 | text2: Second string.
391 |
392 | Returns:
393 | Three element tuple, containing the encoded text1, the encoded text2 and
394 | the array of unique strings. The zeroth element of the array of unique
395 | strings is intentionally blank.
396 | """
397 | lineArray = [] # e.g. lineArray[4] == "Hello\n"
398 | lineHash = {} # e.g. lineHash["Hello\n"] == 4
399 |
400 | # "\x00" is a valid character, but various debuggers don't like it.
401 | # So we'll insert a junk entry to avoid generating a null character.
402 | lineArray.append('')
403 |
404 | def diff_linesToCharsMunge(text):
405 | """Split a text into an array of strings. Reduce the texts to a string
406 | of hashes where each Unicode character represents one line.
407 | Modifies linearray and linehash through being a closure.
408 |
409 | Args:
410 | text: String to encode.
411 |
412 | Returns:
413 | Encoded string.
414 | """
415 | chars = []
416 | # Walk the text, pulling out a substring for each line.
417 | # text.split('\n') would would temporarily double our memory footprint.
418 | # Modifying text would create many large strings to garbage collect.
419 | lineStart = 0
420 | lineEnd = -1
421 | while lineEnd < len(text) - 1:
422 | lineEnd = text.find('\n', lineStart)
423 | if lineEnd == -1:
424 | lineEnd = len(text) - 1
425 | line = text[lineStart:lineEnd + 1]
426 | lineStart = lineEnd + 1
427 |
428 | if line in lineHash:
429 | chars.append(unichr(lineHash[line]))
430 | else:
431 | lineArray.append(line)
432 | lineHash[line] = len(lineArray) - 1
433 | chars.append(unichr(len(lineArray) - 1))
434 | return "".join(chars)
435 |
436 | chars1 = diff_linesToCharsMunge(text1)
437 | chars2 = diff_linesToCharsMunge(text2)
438 | return (chars1, chars2, lineArray)
439 |
440 | def diff_charsToLines(self, diffs, lineArray):
441 | """Rehydrate the text in a diff from a string of line hashes to real lines
442 | of text.
443 |
444 | Args:
445 | diffs: Array of diff tuples.
446 | lineArray: Array of unique strings.
447 | """
448 | for x in xrange(len(diffs)):
449 | text = []
450 | for char in diffs[x][1]:
451 | text.append(lineArray[ord(char)])
452 | diffs[x] = (diffs[x][0], "".join(text))
453 |
454 | def diff_commonPrefix(self, text1, text2):
455 | """Determine the common prefix of two strings.
456 |
457 | Args:
458 | text1: First string.
459 | text2: Second string.
460 |
461 | Returns:
462 | The number of characters common to the start of each string.
463 | """
464 | # Quick check for common null cases.
465 | if not text1 or not text2 or text1[0] != text2[0]:
466 | return 0
467 | # Binary search.
468 | # Performance analysis: http://neil.fraser.name/news/2007/10/09/
469 | pointermin = 0
470 | pointermax = min(len(text1), len(text2))
471 | pointermid = pointermax
472 | pointerstart = 0
473 | while pointermin < pointermid:
474 | if text1[pointerstart:pointermid] == text2[pointerstart:pointermid]:
475 | pointermin = pointermid
476 | pointerstart = pointermin
477 | else:
478 | pointermax = pointermid
479 | pointermid = (pointermax - pointermin) // 2 + pointermin
480 | return pointermid
481 |
482 | def diff_commonSuffix(self, text1, text2):
483 | """Determine the common suffix of two strings.
484 |
485 | Args:
486 | text1: First string.
487 | text2: Second string.
488 |
489 | Returns:
490 | The number of characters common to the end of each string.
491 | """
492 | # Quick check for common null cases.
493 | if not text1 or not text2 or text1[-1] != text2[-1]:
494 | return 0
495 | # Binary search.
496 | # Performance analysis: http://neil.fraser.name/news/2007/10/09/
497 | pointermin = 0
498 | pointermax = min(len(text1), len(text2))
499 | pointermid = pointermax
500 | pointerend = 0
501 | while pointermin < pointermid:
502 | if (text1[-pointermid:len(text1) - pointerend] ==
503 | text2[-pointermid:len(text2) - pointerend]):
504 | pointermin = pointermid
505 | pointerend = pointermin
506 | else:
507 | pointermax = pointermid
508 | pointermid = (pointermax - pointermin) // 2 + pointermin
509 | return pointermid
510 |
511 | def diff_commonOverlap(self, text1, text2):
512 | """Determine if the suffix of one string is the prefix of another.
513 |
514 | Args:
515 | text1 First string.
516 | text2 Second string.
517 |
518 | Returns:
519 | The number of characters common to the end of the first
520 | string and the start of the second string.
521 | """
522 | # Cache the text lengths to prevent multiple calls.
523 | text1_length = len(text1)
524 | text2_length = len(text2)
525 | # Eliminate the null case.
526 | if text1_length == 0 or text2_length == 0:
527 | return 0
528 | # Truncate the longer string.
529 | if text1_length > text2_length:
530 | text1 = text1[-text2_length:]
531 | elif text1_length < text2_length:
532 | text2 = text2[:text1_length]
533 | text_length = min(text1_length, text2_length)
534 | # Quick check for the worst case.
535 | if text1 == text2:
536 | return text_length
537 |
538 | # Start by looking for a single character match
539 | # and increase length until no match is found.
540 | # Performance analysis: http://neil.fraser.name/news/2010/11/04/
541 | best = 0
542 | length = 1
543 | while True:
544 | pattern = text1[-length:]
545 | found = text2.find(pattern)
546 | if found == -1:
547 | return best
548 | length += found
549 | if found == 0 or text1[-length:] == text2[:length]:
550 | best = length
551 | length += 1
552 |
553 | def diff_halfMatch(self, text1, text2):
554 | """Do the two texts share a substring which is at least half the length of
555 | the longer text?
556 | This speedup can produce non-minimal diffs.
557 |
558 | Args:
559 | text1: First string.
560 | text2: Second string.
561 |
562 | Returns:
563 | Five element Array, containing the prefix of text1, the suffix of text1,
564 | the prefix of text2, the suffix of text2 and the common middle. Or None
565 | if there was no match.
566 | """
567 | if self.Diff_Timeout <= 0:
568 | # Don't risk returning a non-optimal diff if we have unlimited time.
569 | return None
570 | if len(text1) > len(text2):
571 | (longtext, shorttext) = (text1, text2)
572 | else:
573 | (shorttext, longtext) = (text1, text2)
574 | if len(longtext) < 4 or len(shorttext) * 2 < len(longtext):
575 | return None # Pointless.
576 |
577 | def diff_halfMatchI(longtext, shorttext, i):
578 | """Does a substring of shorttext exist within longtext such that the
579 | substring is at least half the length of longtext?
580 | Closure, but does not reference any external variables.
581 |
582 | Args:
583 | longtext: Longer string.
584 | shorttext: Shorter string.
585 | i: Start index of quarter length substring within longtext.
586 |
587 | Returns:
588 | Five element Array, containing the prefix of longtext, the suffix of
589 | longtext, the prefix of shorttext, the suffix of shorttext and the
590 | common middle. Or None if there was no match.
591 | """
592 | seed = longtext[i:i + len(longtext) // 4]
593 | best_common = ''
594 | j = shorttext.find(seed)
595 | while j != -1:
596 | prefixLength = self.diff_commonPrefix(longtext[i:], shorttext[j:])
597 | suffixLength = self.diff_commonSuffix(longtext[:i], shorttext[:j])
598 | if len(best_common) < suffixLength + prefixLength:
599 | best_common = (shorttext[j - suffixLength:j] +
600 | shorttext[j:j + prefixLength])
601 | best_longtext_a = longtext[:i - suffixLength]
602 | best_longtext_b = longtext[i + prefixLength:]
603 | best_shorttext_a = shorttext[:j - suffixLength]
604 | best_shorttext_b = shorttext[j + prefixLength:]
605 | j = shorttext.find(seed, j + 1)
606 |
607 | if len(best_common) * 2 >= len(longtext):
608 | return (best_longtext_a, best_longtext_b,
609 | best_shorttext_a, best_shorttext_b, best_common)
610 | else:
611 | return None
612 |
613 | # First check if the second quarter is the seed for a half-match.
614 | hm1 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 3) // 4)
615 | # Check again based on the third quarter.
616 | hm2 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 1) // 2)
617 | if not hm1 and not hm2:
618 | return None
619 | elif not hm2:
620 | hm = hm1
621 | elif not hm1:
622 | hm = hm2
623 | else:
624 | # Both matched. Select the longest.
625 | if len(hm1[4]) > len(hm2[4]):
626 | hm = hm1
627 | else:
628 | hm = hm2
629 |
630 | # A half-match was found, sort out the return data.
631 | if len(text1) > len(text2):
632 | (text1_a, text1_b, text2_a, text2_b, mid_common) = hm
633 | else:
634 | (text2_a, text2_b, text1_a, text1_b, mid_common) = hm
635 | return (text1_a, text1_b, text2_a, text2_b, mid_common)
636 |
637 | def diff_cleanupSemantic(self, diffs):
638 | """Reduce the number of edits by eliminating semantically trivial
639 | equalities.
640 |
641 | Args:
642 | diffs: Array of diff tuples.
643 | """
644 | changes = False
645 | equalities = [] # Stack of indices where equalities are found.
646 | lastequality = None # Always equal to diffs[equalities[-1]][1]
647 | pointer = 0 # Index of current position.
648 | # Number of chars that changed prior to the equality.
649 | length_insertions1, length_deletions1 = 0, 0
650 | # Number of chars that changed after the equality.
651 | length_insertions2, length_deletions2 = 0, 0
652 | while pointer < len(diffs):
653 | if diffs[pointer][0] == self.DIFF_EQUAL: # Equality found.
654 | equalities.append(pointer)
655 | length_insertions1, length_insertions2 = length_insertions2, 0
656 | length_deletions1, length_deletions2 = length_deletions2, 0
657 | lastequality = diffs[pointer][1]
658 | else: # An insertion or deletion.
659 | if diffs[pointer][0] == self.DIFF_INSERT:
660 | length_insertions2 += len(diffs[pointer][1])
661 | else:
662 | length_deletions2 += len(diffs[pointer][1])
663 | # Eliminate an equality that is smaller or equal to the edits on both
664 | # sides of it.
665 | if (lastequality and (len(lastequality) <=
666 | max(length_insertions1, length_deletions1)) and
667 | (len(lastequality) <= max(length_insertions2, length_deletions2))):
668 | # Duplicate record.
669 | diffs.insert(equalities[-1], (self.DIFF_DELETE, lastequality))
670 | # Change second copy to insert.
671 | diffs[equalities[-1] + 1] = (self.DIFF_INSERT,
672 | diffs[equalities[-1] + 1][1])
673 | # Throw away the equality we just deleted.
674 | equalities.pop()
675 | # Throw away the previous equality (it needs to be reevaluated).
676 | if len(equalities):
677 | equalities.pop()
678 | if len(equalities):
679 | pointer = equalities[-1]
680 | else:
681 | pointer = -1
682 | # Reset the counters.
683 | length_insertions1, length_deletions1 = 0, 0
684 | length_insertions2, length_deletions2 = 0, 0
685 | lastequality = None
686 | changes = True
687 | pointer += 1
688 |
689 | # Normalize the diff.
690 | if changes:
691 | self.diff_cleanupMerge(diffs)
692 | self.diff_cleanupSemanticLossless(diffs)
693 |
694 | # Find any overlaps between deletions and insertions.
695 | # e.g: abcxxxxxxdef
696 | # -> abcxxxdef
697 | # e.g: xxxabcdefxxx
698 | # -> defxxxabc
699 | # Only extract an overlap if it is as big as the edit ahead or behind it.
700 | pointer = 1
701 | while pointer < len(diffs):
702 | if (diffs[pointer - 1][0] == self.DIFF_DELETE and
703 | diffs[pointer][0] == self.DIFF_INSERT):
704 | deletion = diffs[pointer - 1][1]
705 | insertion = diffs[pointer][1]
706 | overlap_length1 = self.diff_commonOverlap(deletion, insertion)
707 | overlap_length2 = self.diff_commonOverlap(insertion, deletion)
708 | if overlap_length1 >= overlap_length2:
709 | if (overlap_length1 >= len(deletion) / 2.0 or
710 | overlap_length1 >= len(insertion) / 2.0):
711 | # Overlap found. Insert an equality and trim the surrounding edits.
712 | diffs.insert(pointer, (self.DIFF_EQUAL,
713 | insertion[:overlap_length1]))
714 | diffs[pointer - 1] = (self.DIFF_DELETE,
715 | deletion[:len(deletion) - overlap_length1])
716 | diffs[pointer + 1] = (self.DIFF_INSERT,
717 | insertion[overlap_length1:])
718 | pointer += 1
719 | else:
720 | if (overlap_length2 >= len(deletion) / 2.0 or
721 | overlap_length2 >= len(insertion) / 2.0):
722 | # Reverse overlap found.
723 | # Insert an equality and swap and trim the surrounding edits.
724 | diffs.insert(pointer, (self.DIFF_EQUAL, deletion[:overlap_length2]))
725 | diffs[pointer - 1] = (self.DIFF_INSERT,
726 | insertion[:len(insertion) - overlap_length2])
727 | diffs[pointer + 1] = (self.DIFF_DELETE, deletion[overlap_length2:])
728 | pointer += 1
729 | pointer += 1
730 | pointer += 1
731 |
732 | def diff_cleanupSemanticLossless(self, diffs):
733 | """Look for single edits surrounded on both sides by equalities
734 | which can be shifted sideways to align the edit to a word boundary.
735 | e.g: The cat came. -> The cat came.
736 |
737 | Args:
738 | diffs: Array of diff tuples.
739 | """
740 |
741 | def diff_cleanupSemanticScore(one, two):
742 | """Given two strings, compute a score representing whether the
743 | internal boundary falls on logical boundaries.
744 | Scores range from 6 (best) to 0 (worst).
745 | Closure, but does not reference any external variables.
746 |
747 | Args:
748 | one: First string.
749 | two: Second string.
750 |
751 | Returns:
752 | The score.
753 | """
754 | if not one or not two:
755 | # Edges are the best.
756 | return 6
757 |
758 | # Each port of this function behaves slightly differently due to
759 | # subtle differences in each language's definition of things like
760 | # 'whitespace'. Since this function's purpose is largely cosmetic,
761 | # the choice has been made to use each language's native features
762 | # rather than force total conformity.
763 | char1 = one[-1]
764 | char2 = two[0]
765 | nonAlphaNumeric1 = not char1.isalnum()
766 | nonAlphaNumeric2 = not char2.isalnum()
767 | whitespace1 = nonAlphaNumeric1 and char1.isspace()
768 | whitespace2 = nonAlphaNumeric2 and char2.isspace()
769 | lineBreak1 = whitespace1 and (char1 == "\r" or char1 == "\n")
770 | lineBreak2 = whitespace2 and (char2 == "\r" or char2 == "\n")
771 | blankLine1 = lineBreak1 and self.BLANKLINEEND.search(one)
772 | blankLine2 = lineBreak2 and self.BLANKLINESTART.match(two)
773 |
774 | if blankLine1 or blankLine2:
775 | # Five points for blank lines.
776 | return 5
777 | elif lineBreak1 or lineBreak2:
778 | # Four points for line breaks.
779 | return 4
780 | elif nonAlphaNumeric1 and not whitespace1 and whitespace2:
781 | # Three points for end of sentences.
782 | return 3
783 | elif whitespace1 or whitespace2:
784 | # Two points for whitespace.
785 | return 2
786 | elif nonAlphaNumeric1 or nonAlphaNumeric2:
787 | # One point for non-alphanumeric.
788 | return 1
789 | return 0
790 |
791 | pointer = 1
792 | # Intentionally ignore the first and last element (don't need checking).
793 | while pointer < len(diffs) - 1:
794 | if (diffs[pointer - 1][0] == self.DIFF_EQUAL and
795 | diffs[pointer + 1][0] == self.DIFF_EQUAL):
796 | # This is a single edit surrounded by equalities.
797 | equality1 = diffs[pointer - 1][1]
798 | edit = diffs[pointer][1]
799 | equality2 = diffs[pointer + 1][1]
800 |
801 | # First, shift the edit as far left as possible.
802 | commonOffset = self.diff_commonSuffix(equality1, edit)
803 | if commonOffset:
804 | commonString = edit[-commonOffset:]
805 | equality1 = equality1[:-commonOffset]
806 | edit = commonString + edit[:-commonOffset]
807 | equality2 = commonString + equality2
808 |
809 | # Second, step character by character right, looking for the best fit.
810 | bestEquality1 = equality1
811 | bestEdit = edit
812 | bestEquality2 = equality2
813 | bestScore = (diff_cleanupSemanticScore(equality1, edit) +
814 | diff_cleanupSemanticScore(edit, equality2))
815 | while edit and equality2 and edit[0] == equality2[0]:
816 | equality1 += edit[0]
817 | edit = edit[1:] + equality2[0]
818 | equality2 = equality2[1:]
819 | score = (diff_cleanupSemanticScore(equality1, edit) +
820 | diff_cleanupSemanticScore(edit, equality2))
821 | # The >= encourages trailing rather than leading whitespace on edits.
822 | if score >= bestScore:
823 | bestScore = score
824 | bestEquality1 = equality1
825 | bestEdit = edit
826 | bestEquality2 = equality2
827 |
828 | if diffs[pointer - 1][1] != bestEquality1:
829 | # We have an improvement, save it back to the diff.
830 | if bestEquality1:
831 | diffs[pointer - 1] = (diffs[pointer - 1][0], bestEquality1)
832 | else:
833 | del diffs[pointer - 1]
834 | pointer -= 1
835 | diffs[pointer] = (diffs[pointer][0], bestEdit)
836 | if bestEquality2:
837 | diffs[pointer + 1] = (diffs[pointer + 1][0], bestEquality2)
838 | else:
839 | del diffs[pointer + 1]
840 | pointer -= 1
841 | pointer += 1
842 |
843 | # Define some regex patterns for matching boundaries.
844 | BLANKLINEEND = re.compile(r"\n\r?\n$");
845 | BLANKLINESTART = re.compile(r"^\r?\n\r?\n");
846 |
847 | def diff_cleanupEfficiency(self, diffs):
848 | """Reduce the number of edits by eliminating operationally trivial
849 | equalities.
850 |
851 | Args:
852 | diffs: Array of diff tuples.
853 | """
854 | changes = False
855 | equalities = [] # Stack of indices where equalities are found.
856 | lastequality = None # Always equal to diffs[equalities[-1]][1]
857 | pointer = 0 # Index of current position.
858 | pre_ins = False # Is there an insertion operation before the last equality.
859 | pre_del = False # Is there a deletion operation before the last equality.
860 | post_ins = False # Is there an insertion operation after the last equality.
861 | post_del = False # Is there a deletion operation after the last equality.
862 | while pointer < len(diffs):
863 | if diffs[pointer][0] == self.DIFF_EQUAL: # Equality found.
864 | if (len(diffs[pointer][1]) < self.Diff_EditCost and
865 | (post_ins or post_del)):
866 | # Candidate found.
867 | equalities.append(pointer)
868 | pre_ins = post_ins
869 | pre_del = post_del
870 | lastequality = diffs[pointer][1]
871 | else:
872 | # Not a candidate, and can never become one.
873 | equalities = []
874 | lastequality = None
875 |
876 | post_ins = post_del = False
877 | else: # An insertion or deletion.
878 | if diffs[pointer][0] == self.DIFF_DELETE:
879 | post_del = True
880 | else:
881 | post_ins = True
882 |
883 | # Five types to be split:
884 | # ABXYCD
885 | # AXCD
886 | # ABXC
887 | # AXCD
888 | # ABXC
889 |
890 | if lastequality and ((pre_ins and pre_del and post_ins and post_del) or
891 | ((len(lastequality) < self.Diff_EditCost / 2) and
892 | (pre_ins + pre_del + post_ins + post_del) == 3)):
893 | # Duplicate record.
894 | diffs.insert(equalities[-1], (self.DIFF_DELETE, lastequality))
895 | # Change second copy to insert.
896 | diffs[equalities[-1] + 1] = (self.DIFF_INSERT,
897 | diffs[equalities[-1] + 1][1])
898 | equalities.pop() # Throw away the equality we just deleted.
899 | lastequality = None
900 | if pre_ins and pre_del:
901 | # No changes made which could affect previous entry, keep going.
902 | post_ins = post_del = True
903 | equalities = []
904 | else:
905 | if len(equalities):
906 | equalities.pop() # Throw away the previous equality.
907 | if len(equalities):
908 | pointer = equalities[-1]
909 | else:
910 | pointer = -1
911 | post_ins = post_del = False
912 | changes = True
913 | pointer += 1
914 |
915 | if changes:
916 | self.diff_cleanupMerge(diffs)
917 |
918 | def diff_cleanupMerge(self, diffs):
919 | """Reorder and merge like edit sections. Merge equalities.
920 | Any edit section can move as long as it doesn't cross an equality.
921 |
922 | Args:
923 | diffs: Array of diff tuples.
924 | """
925 | diffs.append((self.DIFF_EQUAL, '')) # Add a dummy entry at the end.
926 | pointer = 0
927 | count_delete = 0
928 | count_insert = 0
929 | text_delete = ''
930 | text_insert = ''
931 | while pointer < len(diffs):
932 | if diffs[pointer][0] == self.DIFF_INSERT:
933 | count_insert += 1
934 | text_insert += diffs[pointer][1]
935 | pointer += 1
936 | elif diffs[pointer][0] == self.DIFF_DELETE:
937 | count_delete += 1
938 | text_delete += diffs[pointer][1]
939 | pointer += 1
940 | elif diffs[pointer][0] == self.DIFF_EQUAL:
941 | # Upon reaching an equality, check for prior redundancies.
942 | if count_delete + count_insert > 1:
943 | if count_delete != 0 and count_insert != 0:
944 | # Factor out any common prefixies.
945 | commonlength = self.diff_commonPrefix(text_insert, text_delete)
946 | if commonlength != 0:
947 | x = pointer - count_delete - count_insert - 1
948 | if x >= 0 and diffs[x][0] == self.DIFF_EQUAL:
949 | diffs[x] = (diffs[x][0], diffs[x][1] +
950 | text_insert[:commonlength])
951 | else:
952 | diffs.insert(0, (self.DIFF_EQUAL, text_insert[:commonlength]))
953 | pointer += 1
954 | text_insert = text_insert[commonlength:]
955 | text_delete = text_delete[commonlength:]
956 | # Factor out any common suffixies.
957 | commonlength = self.diff_commonSuffix(text_insert, text_delete)
958 | if commonlength != 0:
959 | diffs[pointer] = (diffs[pointer][0], text_insert[-commonlength:] +
960 | diffs[pointer][1])
961 | text_insert = text_insert[:-commonlength]
962 | text_delete = text_delete[:-commonlength]
963 | # Delete the offending records and add the merged ones.
964 | if count_delete == 0:
965 | diffs[pointer - count_insert : pointer] = [
966 | (self.DIFF_INSERT, text_insert)]
967 | elif count_insert == 0:
968 | diffs[pointer - count_delete : pointer] = [
969 | (self.DIFF_DELETE, text_delete)]
970 | else:
971 | diffs[pointer - count_delete - count_insert : pointer] = [
972 | (self.DIFF_DELETE, text_delete),
973 | (self.DIFF_INSERT, text_insert)]
974 | pointer = pointer - count_delete - count_insert + 1
975 | if count_delete != 0:
976 | pointer += 1
977 | if count_insert != 0:
978 | pointer += 1
979 | elif pointer != 0 and diffs[pointer - 1][0] == self.DIFF_EQUAL:
980 | # Merge this equality with the previous one.
981 | diffs[pointer - 1] = (diffs[pointer - 1][0],
982 | diffs[pointer - 1][1] + diffs[pointer][1])
983 | del diffs[pointer]
984 | else:
985 | pointer += 1
986 |
987 | count_insert = 0
988 | count_delete = 0
989 | text_delete = ''
990 | text_insert = ''
991 |
992 | if diffs[-1][1] == '':
993 | diffs.pop() # Remove the dummy entry at the end.
994 |
995 | # Second pass: look for single edits surrounded on both sides by equalities
996 | # which can be shifted sideways to eliminate an equality.
997 | # e.g: ABAC -> ABAC
998 | changes = False
999 | pointer = 1
1000 | # Intentionally ignore the first and last element (don't need checking).
1001 | while pointer < len(diffs) - 1:
1002 | if (diffs[pointer - 1][0] == self.DIFF_EQUAL and
1003 | diffs[pointer + 1][0] == self.DIFF_EQUAL):
1004 | # This is a single edit surrounded by equalities.
1005 | if diffs[pointer][1].endswith(diffs[pointer - 1][1]):
1006 | # Shift the edit over the previous equality.
1007 | diffs[pointer] = (diffs[pointer][0],
1008 | diffs[pointer - 1][1] +
1009 | diffs[pointer][1][:-len(diffs[pointer - 1][1])])
1010 | diffs[pointer + 1] = (diffs[pointer + 1][0],
1011 | diffs[pointer - 1][1] + diffs[pointer + 1][1])
1012 | del diffs[pointer - 1]
1013 | changes = True
1014 | elif diffs[pointer][1].startswith(diffs[pointer + 1][1]):
1015 | # Shift the edit over the next equality.
1016 | diffs[pointer - 1] = (diffs[pointer - 1][0],
1017 | diffs[pointer - 1][1] + diffs[pointer + 1][1])
1018 | diffs[pointer] = (diffs[pointer][0],
1019 | diffs[pointer][1][len(diffs[pointer + 1][1]):] +
1020 | diffs[pointer + 1][1])
1021 | del diffs[pointer + 1]
1022 | changes = True
1023 | pointer += 1
1024 |
1025 | # If shifts were made, the diff needs reordering and another shift sweep.
1026 | if changes:
1027 | self.diff_cleanupMerge(diffs)
1028 |
1029 | def diff_xIndex(self, diffs, loc):
1030 | """loc is a location in text1, compute and return the equivalent location
1031 | in text2. e.g. "The cat" vs "The big cat", 1->1, 5->8
1032 |
1033 | Args:
1034 | diffs: Array of diff tuples.
1035 | loc: Location within text1.
1036 |
1037 | Returns:
1038 | Location within text2.
1039 | """
1040 | chars1 = 0
1041 | chars2 = 0
1042 | last_chars1 = 0
1043 | last_chars2 = 0
1044 | for x in xrange(len(diffs)):
1045 | (op, text) = diffs[x]
1046 | if op != self.DIFF_INSERT: # Equality or deletion.
1047 | chars1 += len(text)
1048 | if op != self.DIFF_DELETE: # Equality or insertion.
1049 | chars2 += len(text)
1050 | if chars1 > loc: # Overshot the location.
1051 | break
1052 | last_chars1 = chars1
1053 | last_chars2 = chars2
1054 |
1055 | if len(diffs) != x and diffs[x][0] == self.DIFF_DELETE:
1056 | # The location was deleted.
1057 | return last_chars2
1058 | # Add the remaining len(character).
1059 | return last_chars2 + (loc - last_chars1)
1060 |
1061 | def diff_prettyHtml(self, diffs):
1062 | """Convert a diff array into a pretty HTML report.
1063 |
1064 | Args:
1065 | diffs: Array of diff tuples.
1066 |
1067 | Returns:
1068 | HTML representation.
1069 | """
1070 | html = []
1071 | for (op, data) in diffs:
1072 | text = (data.replace("&", "&").replace("<", "<")
1073 | .replace(">", ">").replace("\n", "¶
"))
1074 | if op == self.DIFF_INSERT:
1075 | html.append("%s" % text)
1076 | elif op == self.DIFF_DELETE:
1077 | html.append("%s" % text)
1078 | elif op == self.DIFF_EQUAL:
1079 | html.append("%s" % text)
1080 | return "".join(html)
1081 |
1082 | def diff_text1(self, diffs):
1083 | """Compute and return the source text (all equalities and deletions).
1084 |
1085 | Args:
1086 | diffs: Array of diff tuples.
1087 |
1088 | Returns:
1089 | Source text.
1090 | """
1091 | text = []
1092 | for (op, data) in diffs:
1093 | if op != self.DIFF_INSERT:
1094 | text.append(data)
1095 | return "".join(text)
1096 |
1097 | def diff_text2(self, diffs):
1098 | """Compute and return the destination text (all equalities and insertions).
1099 |
1100 | Args:
1101 | diffs: Array of diff tuples.
1102 |
1103 | Returns:
1104 | Destination text.
1105 | """
1106 | text = []
1107 | for (op, data) in diffs:
1108 | if op != self.DIFF_DELETE:
1109 | text.append(data)
1110 | return "".join(text)
1111 |
1112 | def diff_levenshtein(self, diffs):
1113 | """Compute the Levenshtein distance; the number of inserted, deleted or
1114 | substituted characters.
1115 |
1116 | Args:
1117 | diffs: Array of diff tuples.
1118 |
1119 | Returns:
1120 | Number of changes.
1121 | """
1122 | levenshtein = 0
1123 | insertions = 0
1124 | deletions = 0
1125 | for (op, data) in diffs:
1126 | if op == self.DIFF_INSERT:
1127 | insertions += len(data)
1128 | elif op == self.DIFF_DELETE:
1129 | deletions += len(data)
1130 | elif op == self.DIFF_EQUAL:
1131 | # A deletion and an insertion is one substitution.
1132 | levenshtein += max(insertions, deletions)
1133 | insertions = 0
1134 | deletions = 0
1135 | levenshtein += max(insertions, deletions)
1136 | return levenshtein
1137 |
1138 | def diff_toDelta(self, diffs):
1139 | """Crush the diff into an encoded string which describes the operations
1140 | required to transform text1 into text2.
1141 | E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'.
1142 | Operations are tab-separated. Inserted text is escaped using %xx notation.
1143 |
1144 | Args:
1145 | diffs: Array of diff tuples.
1146 |
1147 | Returns:
1148 | Delta text.
1149 | """
1150 | text = []
1151 | for (op, data) in diffs:
1152 | if op == self.DIFF_INSERT:
1153 | # High ascii will raise UnicodeDecodeError. Use Unicode instead.
1154 | data = data.encode("utf-8")
1155 | text.append("+" + urllib.quote(data, "!~*'();/?:@&=+$,# "))
1156 | elif op == self.DIFF_DELETE:
1157 | text.append("-%d" % len(data))
1158 | elif op == self.DIFF_EQUAL:
1159 | text.append("=%d" % len(data))
1160 | return "\t".join(text)
1161 |
1162 | def diff_fromDelta(self, text1, delta):
1163 | """Given the original text1, and an encoded string which describes the
1164 | operations required to transform text1 into text2, compute the full diff.
1165 |
1166 | Args:
1167 | text1: Source string for the diff.
1168 | delta: Delta text.
1169 |
1170 | Returns:
1171 | Array of diff tuples.
1172 |
1173 | Raises:
1174 | ValueError: If invalid input.
1175 | """
1176 | if type(delta) == unicode:
1177 | # Deltas should be composed of a subset of ascii chars, Unicode not
1178 | # required. If this encode raises UnicodeEncodeError, delta is invalid.
1179 | delta = delta.encode("ascii")
1180 | diffs = []
1181 | pointer = 0 # Cursor in text1
1182 | tokens = delta.split("\t")
1183 | for token in tokens:
1184 | if token == "":
1185 | # Blank tokens are ok (from a trailing \t).
1186 | continue
1187 | # Each token begins with a one character parameter which specifies the
1188 | # operation of this token (delete, insert, equality).
1189 | param = token[1:]
1190 | if token[0] == "+":
1191 | param = urllib.unquote(param).decode("utf-8")
1192 | diffs.append((self.DIFF_INSERT, param))
1193 | elif token[0] == "-" or token[0] == "=":
1194 | try:
1195 | n = int(param)
1196 | except ValueError:
1197 | raise ValueError("Invalid number in diff_fromDelta: " + param)
1198 | if n < 0:
1199 | raise ValueError("Negative number in diff_fromDelta: " + param)
1200 | text = text1[pointer : pointer + n]
1201 | pointer += n
1202 | if token[0] == "=":
1203 | diffs.append((self.DIFF_EQUAL, text))
1204 | else:
1205 | diffs.append((self.DIFF_DELETE, text))
1206 | else:
1207 | # Anything else is an error.
1208 | raise ValueError("Invalid diff operation in diff_fromDelta: " +
1209 | token[0])
1210 | if pointer != len(text1):
1211 | raise ValueError(
1212 | "Delta length (%d) does not equal source text length (%d)." %
1213 | (pointer, len(text1)))
1214 | return diffs
1215 |
1216 | # MATCH FUNCTIONS
1217 |
1218 | def match_main(self, text, pattern, loc):
1219 | """Locate the best instance of 'pattern' in 'text' near 'loc'.
1220 |
1221 | Args:
1222 | text: The text to search.
1223 | pattern: The pattern to search for.
1224 | loc: The location to search around.
1225 |
1226 | Returns:
1227 | Best match index or -1.
1228 | """
1229 | # Check for null inputs.
1230 | if text == None or pattern == None:
1231 | raise ValueError("Null inputs. (match_main)")
1232 |
1233 | loc = max(0, min(loc, len(text)))
1234 | if text == pattern:
1235 | # Shortcut (potentially not guaranteed by the algorithm)
1236 | return 0
1237 | elif not text:
1238 | # Nothing to match.
1239 | return -1
1240 | elif text[loc:loc + len(pattern)] == pattern:
1241 | # Perfect match at the perfect spot! (Includes case of null pattern)
1242 | return loc
1243 | else:
1244 | # Do a fuzzy compare.
1245 | match = self.match_bitap(text, pattern, loc)
1246 | return match
1247 |
1248 | def match_bitap(self, text, pattern, loc):
1249 | """Locate the best instance of 'pattern' in 'text' near 'loc' using the
1250 | Bitap algorithm.
1251 |
1252 | Args:
1253 | text: The text to search.
1254 | pattern: The pattern to search for.
1255 | loc: The location to search around.
1256 |
1257 | Returns:
1258 | Best match index or -1.
1259 | """
1260 | # Python doesn't have a maxint limit, so ignore this check.
1261 | #if self.Match_MaxBits != 0 and len(pattern) > self.Match_MaxBits:
1262 | # raise ValueError("Pattern too long for this application.")
1263 |
1264 | # Initialise the alphabet.
1265 | s = self.match_alphabet(pattern)
1266 |
1267 | def match_bitapScore(e, x):
1268 | """Compute and return the score for a match with e errors and x location.
1269 | Accesses loc and pattern through being a closure.
1270 |
1271 | Args:
1272 | e: Number of errors in match.
1273 | x: Location of match.
1274 |
1275 | Returns:
1276 | Overall score for match (0.0 = good, 1.0 = bad).
1277 | """
1278 | accuracy = float(e) / len(pattern)
1279 | proximity = abs(loc - x)
1280 | if not self.Match_Distance:
1281 | # Dodge divide by zero error.
1282 | return proximity and 1.0 or accuracy
1283 | return accuracy + (proximity / float(self.Match_Distance))
1284 |
1285 | # Highest score beyond which we give up.
1286 | score_threshold = self.Match_Threshold
1287 | # Is there a nearby exact match? (speedup)
1288 | best_loc = text.find(pattern, loc)
1289 | if best_loc != -1:
1290 | score_threshold = min(match_bitapScore(0, best_loc), score_threshold)
1291 | # What about in the other direction? (speedup)
1292 | best_loc = text.rfind(pattern, loc + len(pattern))
1293 | if best_loc != -1:
1294 | score_threshold = min(match_bitapScore(0, best_loc), score_threshold)
1295 |
1296 | # Initialise the bit arrays.
1297 | matchmask = 1 << (len(pattern) - 1)
1298 | best_loc = -1
1299 |
1300 | bin_max = len(pattern) + len(text)
1301 | # Empty initialization added to appease pychecker.
1302 | last_rd = None
1303 | for d in xrange(len(pattern)):
1304 | # Scan for the best match each iteration allows for one more error.
1305 | # Run a binary search to determine how far from 'loc' we can stray at
1306 | # this error level.
1307 | bin_min = 0
1308 | bin_mid = bin_max
1309 | while bin_min < bin_mid:
1310 | if match_bitapScore(d, loc + bin_mid) <= score_threshold:
1311 | bin_min = bin_mid
1312 | else:
1313 | bin_max = bin_mid
1314 | bin_mid = (bin_max - bin_min) // 2 + bin_min
1315 |
1316 | # Use the result from this iteration as the maximum for the next.
1317 | bin_max = bin_mid
1318 | start = max(1, loc - bin_mid + 1)
1319 | finish = min(loc + bin_mid, len(text)) + len(pattern)
1320 |
1321 | rd = [0] * (finish + 2)
1322 | rd[finish + 1] = (1 << d) - 1
1323 | for j in xrange(finish, start - 1, -1):
1324 | if len(text) <= j - 1:
1325 | # Out of range.
1326 | charMatch = 0
1327 | else:
1328 | charMatch = s.get(text[j - 1], 0)
1329 | if d == 0: # First pass: exact match.
1330 | rd[j] = ((rd[j + 1] << 1) | 1) & charMatch
1331 | else: # Subsequent passes: fuzzy match.
1332 | rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) | (
1333 | ((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]
1334 | if rd[j] & matchmask:
1335 | score = match_bitapScore(d, j - 1)
1336 | # This match will almost certainly be better than any existing match.
1337 | # But check anyway.
1338 | if score <= score_threshold:
1339 | # Told you so.
1340 | score_threshold = score
1341 | best_loc = j - 1
1342 | if best_loc > loc:
1343 | # When passing loc, don't exceed our current distance from loc.
1344 | start = max(1, 2 * loc - best_loc)
1345 | else:
1346 | # Already passed loc, downhill from here on in.
1347 | break
1348 | # No hope for a (better) match at greater error levels.
1349 | if match_bitapScore(d + 1, loc) > score_threshold:
1350 | break
1351 | last_rd = rd
1352 | return best_loc
1353 |
1354 | def match_alphabet(self, pattern):
1355 | """Initialise the alphabet for the Bitap algorithm.
1356 |
1357 | Args:
1358 | pattern: The text to encode.
1359 |
1360 | Returns:
1361 | Hash of character locations.
1362 | """
1363 | s = {}
1364 | for char in pattern:
1365 | s[char] = 0
1366 | for i in xrange(len(pattern)):
1367 | s[pattern[i]] |= 1 << (len(pattern) - i - 1)
1368 | return s
1369 |
1370 | # PATCH FUNCTIONS
1371 |
1372 | def patch_addContext(self, patch, text):
1373 | """Increase the context until it is unique,
1374 | but don't let the pattern expand beyond Match_MaxBits.
1375 |
1376 | Args:
1377 | patch: The patch to grow.
1378 | text: Source text.
1379 | """
1380 | if len(text) == 0:
1381 | return
1382 | pattern = text[patch.start2 : patch.start2 + patch.length1]
1383 | padding = 0
1384 |
1385 | # Look for the first and last matches of pattern in text. If two different
1386 | # matches are found, increase the pattern length.
1387 | while (text.find(pattern) != text.rfind(pattern) and (self.Match_MaxBits ==
1388 | 0 or len(pattern) < self.Match_MaxBits - self.Patch_Margin -
1389 | self.Patch_Margin)):
1390 | padding += self.Patch_Margin
1391 | pattern = text[max(0, patch.start2 - padding) :
1392 | patch.start2 + patch.length1 + padding]
1393 | # Add one chunk for good luck.
1394 | padding += self.Patch_Margin
1395 |
1396 | # Add the prefix.
1397 | prefix = text[max(0, patch.start2 - padding) : patch.start2]
1398 | if prefix:
1399 | patch.diffs[:0] = [(self.DIFF_EQUAL, prefix)]
1400 | # Add the suffix.
1401 | suffix = text[patch.start2 + patch.length1 :
1402 | patch.start2 + patch.length1 + padding]
1403 | if suffix:
1404 | patch.diffs.append((self.DIFF_EQUAL, suffix))
1405 |
1406 | # Roll back the start points.
1407 | patch.start1 -= len(prefix)
1408 | patch.start2 -= len(prefix)
1409 | # Extend lengths.
1410 | patch.length1 += len(prefix) + len(suffix)
1411 | patch.length2 += len(prefix) + len(suffix)
1412 |
1413 | def patch_make(self, a, b=None, c=None):
1414 | """Compute a list of patches to turn text1 into text2.
1415 | Use diffs if provided, otherwise compute it ourselves.
1416 | There are four ways to call this function, depending on what data is
1417 | available to the caller:
1418 | Method 1:
1419 | a = text1, b = text2
1420 | Method 2:
1421 | a = diffs
1422 | Method 3 (optimal):
1423 | a = text1, b = diffs
1424 | Method 4 (deprecated, use method 3):
1425 | a = text1, b = text2, c = diffs
1426 |
1427 | Args:
1428 | a: text1 (methods 1,3,4) or Array of diff tuples for text1 to
1429 | text2 (method 2).
1430 | b: text2 (methods 1,4) or Array of diff tuples for text1 to
1431 | text2 (method 3) or undefined (method 2).
1432 | c: Array of diff tuples for text1 to text2 (method 4) or
1433 | undefined (methods 1,2,3).
1434 |
1435 | Returns:
1436 | Array of Patch objects.
1437 | """
1438 | text1 = None
1439 | diffs = None
1440 | # Note that texts may arrive as 'str' or 'unicode'.
1441 | if isinstance(a, basestring) and isinstance(b, basestring) and c is None:
1442 | # Method 1: text1, text2
1443 | # Compute diffs from text1 and text2.
1444 | text1 = a
1445 | diffs = self.diff_main(text1, b, True)
1446 | if len(diffs) > 2:
1447 | self.diff_cleanupSemantic(diffs)
1448 | self.diff_cleanupEfficiency(diffs)
1449 | elif isinstance(a, list) and b is None and c is None:
1450 | # Method 2: diffs
1451 | # Compute text1 from diffs.
1452 | diffs = a
1453 | text1 = self.diff_text1(diffs)
1454 | elif isinstance(a, basestring) and isinstance(b, list) and c is None:
1455 | # Method 3: text1, diffs
1456 | text1 = a
1457 | diffs = b
1458 | elif (isinstance(a, basestring) and isinstance(b, basestring) and
1459 | isinstance(c, list)):
1460 | # Method 4: text1, text2, diffs
1461 | # text2 is not used.
1462 | text1 = a
1463 | diffs = c
1464 | else:
1465 | raise ValueError("Unknown call format to patch_make.")
1466 |
1467 | if not diffs:
1468 | return [] # Get rid of the None case.
1469 | patches = []
1470 | patch = patch_obj()
1471 | char_count1 = 0 # Number of characters into the text1 string.
1472 | char_count2 = 0 # Number of characters into the text2 string.
1473 | prepatch_text = text1 # Recreate the patches to determine context info.
1474 | postpatch_text = text1
1475 | for x in xrange(len(diffs)):
1476 | (diff_type, diff_text) = diffs[x]
1477 | if len(patch.diffs) == 0 and diff_type != self.DIFF_EQUAL:
1478 | # A new patch starts here.
1479 | patch.start1 = char_count1
1480 | patch.start2 = char_count2
1481 | if diff_type == self.DIFF_INSERT:
1482 | # Insertion
1483 | patch.diffs.append(diffs[x])
1484 | patch.length2 += len(diff_text)
1485 | postpatch_text = (postpatch_text[:char_count2] + diff_text +
1486 | postpatch_text[char_count2:])
1487 | elif diff_type == self.DIFF_DELETE:
1488 | # Deletion.
1489 | patch.length1 += len(diff_text)
1490 | patch.diffs.append(diffs[x])
1491 | postpatch_text = (postpatch_text[:char_count2] +
1492 | postpatch_text[char_count2 + len(diff_text):])
1493 | elif (diff_type == self.DIFF_EQUAL and
1494 | len(diff_text) <= 2 * self.Patch_Margin and
1495 | len(patch.diffs) != 0 and len(diffs) != x + 1):
1496 | # Small equality inside a patch.
1497 | patch.diffs.append(diffs[x])
1498 | patch.length1 += len(diff_text)
1499 | patch.length2 += len(diff_text)
1500 |
1501 | if (diff_type == self.DIFF_EQUAL and
1502 | len(diff_text) >= 2 * self.Patch_Margin):
1503 | # Time for a new patch.
1504 | if len(patch.diffs) != 0:
1505 | self.patch_addContext(patch, prepatch_text)
1506 | patches.append(patch)
1507 | patch = patch_obj()
1508 | # Unlike Unidiff, our patch lists have a rolling context.
1509 | # http://code.google.com/p/google-diff-match-patch/wiki/Unidiff
1510 | # Update prepatch text & pos to reflect the application of the
1511 | # just completed patch.
1512 | prepatch_text = postpatch_text
1513 | char_count1 = char_count2
1514 |
1515 | # Update the current character count.
1516 | if diff_type != self.DIFF_INSERT:
1517 | char_count1 += len(diff_text)
1518 | if diff_type != self.DIFF_DELETE:
1519 | char_count2 += len(diff_text)
1520 |
1521 | # Pick up the leftover patch if not empty.
1522 | if len(patch.diffs) != 0:
1523 | self.patch_addContext(patch, prepatch_text)
1524 | patches.append(patch)
1525 | return patches
1526 |
1527 | def patch_deepCopy(self, patches):
1528 | """Given an array of patches, return another array that is identical.
1529 |
1530 | Args:
1531 | patches: Array of Patch objects.
1532 |
1533 | Returns:
1534 | Array of Patch objects.
1535 | """
1536 | patchesCopy = []
1537 | for patch in patches:
1538 | patchCopy = patch_obj()
1539 | # No need to deep copy the tuples since they are immutable.
1540 | patchCopy.diffs = patch.diffs[:]
1541 | patchCopy.start1 = patch.start1
1542 | patchCopy.start2 = patch.start2
1543 | patchCopy.length1 = patch.length1
1544 | patchCopy.length2 = patch.length2
1545 | patchesCopy.append(patchCopy)
1546 | return patchesCopy
1547 |
1548 | def patch_apply(self, patches, text):
1549 | """Merge a set of patches onto the text. Return a patched text, as well
1550 | as a list of true/false values indicating which patches were applied.
1551 |
1552 | Args:
1553 | patches: Array of Patch objects.
1554 | text: Old text.
1555 |
1556 | Returns:
1557 | Two element Array, containing the new text and an array of boolean values.
1558 | """
1559 | if not patches:
1560 | return (text, [])
1561 |
1562 | # Deep copy the patches so that no changes are made to originals.
1563 | patches = self.patch_deepCopy(patches)
1564 |
1565 | nullPadding = self.patch_addPadding(patches)
1566 | text = nullPadding + text + nullPadding
1567 | self.patch_splitMax(patches)
1568 |
1569 | # delta keeps track of the offset between the expected and actual location
1570 | # of the previous patch. If there are patches expected at positions 10 and
1571 | # 20, but the first patch was found at 12, delta is 2 and the second patch
1572 | # has an effective expected position of 22.
1573 | delta = 0
1574 | results = []
1575 | for patch in patches:
1576 | expected_loc = patch.start2 + delta
1577 | text1 = self.diff_text1(patch.diffs)
1578 | end_loc = -1
1579 | if len(text1) > self.Match_MaxBits:
1580 | # patch_splitMax will only provide an oversized pattern in the case of
1581 | # a monster delete.
1582 | start_loc = self.match_main(text, text1[:self.Match_MaxBits],
1583 | expected_loc)
1584 | if start_loc != -1:
1585 | end_loc = self.match_main(text, text1[-self.Match_MaxBits:],
1586 | expected_loc + len(text1) - self.Match_MaxBits)
1587 | if end_loc == -1 or start_loc >= end_loc:
1588 | # Can't find valid trailing context. Drop this patch.
1589 | start_loc = -1
1590 | else:
1591 | start_loc = self.match_main(text, text1, expected_loc)
1592 | if start_loc == -1:
1593 | # No match found. :(
1594 | results.append(False)
1595 | # Subtract the delta for this failed patch from subsequent patches.
1596 | delta -= patch.length2 - patch.length1
1597 | else:
1598 | # Found a match. :)
1599 | results.append(True)
1600 | delta = start_loc - expected_loc
1601 | if end_loc == -1:
1602 | text2 = text[start_loc : start_loc + len(text1)]
1603 | else:
1604 | text2 = text[start_loc : end_loc + self.Match_MaxBits]
1605 | if text1 == text2:
1606 | # Perfect match, just shove the replacement text in.
1607 | text = (text[:start_loc] + self.diff_text2(patch.diffs) +
1608 | text[start_loc + len(text1):])
1609 | else:
1610 | # Imperfect match.
1611 | # Run a diff to get a framework of equivalent indices.
1612 | diffs = self.diff_main(text1, text2, False)
1613 | if (len(text1) > self.Match_MaxBits and
1614 | self.diff_levenshtein(diffs) / float(len(text1)) >
1615 | self.Patch_DeleteThreshold):
1616 | # The end points match, but the content is unacceptably bad.
1617 | results[-1] = False
1618 | else:
1619 | self.diff_cleanupSemanticLossless(diffs)
1620 | index1 = 0
1621 | for (op, data) in patch.diffs:
1622 | if op != self.DIFF_EQUAL:
1623 | index2 = self.diff_xIndex(diffs, index1)
1624 | if op == self.DIFF_INSERT: # Insertion
1625 | text = text[:start_loc + index2] + data + text[start_loc +
1626 | index2:]
1627 | elif op == self.DIFF_DELETE: # Deletion
1628 | text = text[:start_loc + index2] + text[start_loc +
1629 | self.diff_xIndex(diffs, index1 + len(data)):]
1630 | if op != self.DIFF_DELETE:
1631 | index1 += len(data)
1632 | # Strip the padding off.
1633 | text = text[len(nullPadding):-len(nullPadding)]
1634 | return (text, results)
1635 |
1636 | def patch_addPadding(self, patches):
1637 | """Add some padding on text start and end so that edges can match
1638 | something. Intended to be called only from within patch_apply.
1639 |
1640 | Args:
1641 | patches: Array of Patch objects.
1642 |
1643 | Returns:
1644 | The padding string added to each side.
1645 | """
1646 | paddingLength = self.Patch_Margin
1647 | nullPadding = ""
1648 | for x in xrange(1, paddingLength + 1):
1649 | nullPadding += chr(x)
1650 |
1651 | # Bump all the patches forward.
1652 | for patch in patches:
1653 | patch.start1 += paddingLength
1654 | patch.start2 += paddingLength
1655 |
1656 | # Add some padding on start of first diff.
1657 | patch = patches[0]
1658 | diffs = patch.diffs
1659 | if not diffs or diffs[0][0] != self.DIFF_EQUAL:
1660 | # Add nullPadding equality.
1661 | diffs.insert(0, (self.DIFF_EQUAL, nullPadding))
1662 | patch.start1 -= paddingLength # Should be 0.
1663 | patch.start2 -= paddingLength # Should be 0.
1664 | patch.length1 += paddingLength
1665 | patch.length2 += paddingLength
1666 | elif paddingLength > len(diffs[0][1]):
1667 | # Grow first equality.
1668 | extraLength = paddingLength - len(diffs[0][1])
1669 | newText = nullPadding[len(diffs[0][1]):] + diffs[0][1]
1670 | diffs[0] = (diffs[0][0], newText)
1671 | patch.start1 -= extraLength
1672 | patch.start2 -= extraLength
1673 | patch.length1 += extraLength
1674 | patch.length2 += extraLength
1675 |
1676 | # Add some padding on end of last diff.
1677 | patch = patches[-1]
1678 | diffs = patch.diffs
1679 | if not diffs or diffs[-1][0] != self.DIFF_EQUAL:
1680 | # Add nullPadding equality.
1681 | diffs.append((self.DIFF_EQUAL, nullPadding))
1682 | patch.length1 += paddingLength
1683 | patch.length2 += paddingLength
1684 | elif paddingLength > len(diffs[-1][1]):
1685 | # Grow last equality.
1686 | extraLength = paddingLength - len(diffs[-1][1])
1687 | newText = diffs[-1][1] + nullPadding[:extraLength]
1688 | diffs[-1] = (diffs[-1][0], newText)
1689 | patch.length1 += extraLength
1690 | patch.length2 += extraLength
1691 |
1692 | return nullPadding
1693 |
1694 | def patch_splitMax(self, patches):
1695 | """Look through the patches and break up any which are longer than the
1696 | maximum limit of the match algorithm.
1697 | Intended to be called only from within patch_apply.
1698 |
1699 | Args:
1700 | patches: Array of Patch objects.
1701 | """
1702 | patch_size = self.Match_MaxBits
1703 | if patch_size == 0:
1704 | # Python has the option of not splitting strings due to its ability
1705 | # to handle integers of arbitrary precision.
1706 | return
1707 | for x in xrange(len(patches)):
1708 | if patches[x].length1 <= patch_size:
1709 | continue
1710 | bigpatch = patches[x]
1711 | # Remove the big old patch.
1712 | del patches[x]
1713 | x -= 1
1714 | start1 = bigpatch.start1
1715 | start2 = bigpatch.start2
1716 | precontext = ''
1717 | while len(bigpatch.diffs) != 0:
1718 | # Create one of several smaller patches.
1719 | patch = patch_obj()
1720 | empty = True
1721 | patch.start1 = start1 - len(precontext)
1722 | patch.start2 = start2 - len(precontext)
1723 | if precontext:
1724 | patch.length1 = patch.length2 = len(precontext)
1725 | patch.diffs.append((self.DIFF_EQUAL, precontext))
1726 |
1727 | while (len(bigpatch.diffs) != 0 and
1728 | patch.length1 < patch_size - self.Patch_Margin):
1729 | (diff_type, diff_text) = bigpatch.diffs[0]
1730 | if diff_type == self.DIFF_INSERT:
1731 | # Insertions are harmless.
1732 | patch.length2 += len(diff_text)
1733 | start2 += len(diff_text)
1734 | patch.diffs.append(bigpatch.diffs.pop(0))
1735 | empty = False
1736 | elif (diff_type == self.DIFF_DELETE and len(patch.diffs) == 1 and
1737 | patch.diffs[0][0] == self.DIFF_EQUAL and
1738 | len(diff_text) > 2 * patch_size):
1739 | # This is a large deletion. Let it pass in one chunk.
1740 | patch.length1 += len(diff_text)
1741 | start1 += len(diff_text)
1742 | empty = False
1743 | patch.diffs.append((diff_type, diff_text))
1744 | del bigpatch.diffs[0]
1745 | else:
1746 | # Deletion or equality. Only take as much as we can stomach.
1747 | diff_text = diff_text[:patch_size - patch.length1 -
1748 | self.Patch_Margin]
1749 | patch.length1 += len(diff_text)
1750 | start1 += len(diff_text)
1751 | if diff_type == self.DIFF_EQUAL:
1752 | patch.length2 += len(diff_text)
1753 | start2 += len(diff_text)
1754 | else:
1755 | empty = False
1756 |
1757 | patch.diffs.append((diff_type, diff_text))
1758 | if diff_text == bigpatch.diffs[0][1]:
1759 | del bigpatch.diffs[0]
1760 | else:
1761 | bigpatch.diffs[0] = (bigpatch.diffs[0][0],
1762 | bigpatch.diffs[0][1][len(diff_text):])
1763 |
1764 | # Compute the head context for the next patch.
1765 | precontext = self.diff_text2(patch.diffs)
1766 | precontext = precontext[-self.Patch_Margin:]
1767 | # Append the end context for this patch.
1768 | postcontext = self.diff_text1(bigpatch.diffs)[:self.Patch_Margin]
1769 | if postcontext:
1770 | patch.length1 += len(postcontext)
1771 | patch.length2 += len(postcontext)
1772 | if len(patch.diffs) != 0 and patch.diffs[-1][0] == self.DIFF_EQUAL:
1773 | patch.diffs[-1] = (self.DIFF_EQUAL, patch.diffs[-1][1] +
1774 | postcontext)
1775 | else:
1776 | patch.diffs.append((self.DIFF_EQUAL, postcontext))
1777 |
1778 | if not empty:
1779 | x += 1
1780 | patches.insert(x, patch)
1781 |
1782 | def patch_toText(self, patches):
1783 | """Take a list of patches and return a textual representation.
1784 |
1785 | Args:
1786 | patches: Array of Patch objects.
1787 |
1788 | Returns:
1789 | Text representation of patches.
1790 | """
1791 | text = []
1792 | for patch in patches:
1793 | text.append(str(patch))
1794 | return "".join(text)
1795 |
1796 | def patch_fromText(self, textline):
1797 | """Parse a textual representation of patches and return a list of patch
1798 | objects.
1799 |
1800 | Args:
1801 | textline: Text representation of patches.
1802 |
1803 | Returns:
1804 | Array of Patch objects.
1805 |
1806 | Raises:
1807 | ValueError: If invalid input.
1808 | """
1809 | if type(textline) == unicode:
1810 | # Patches should be composed of a subset of ascii chars, Unicode not
1811 | # required. If this encode raises UnicodeEncodeError, patch is invalid.
1812 | textline = textline.encode("ascii")
1813 | patches = []
1814 | if not textline:
1815 | return patches
1816 | text = textline.split('\n')
1817 | while len(text) != 0:
1818 | m = re.match("^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$", text[0])
1819 | if not m:
1820 | raise ValueError("Invalid patch string: " + text[0])
1821 | patch = patch_obj()
1822 | patches.append(patch)
1823 | patch.start1 = int(m.group(1))
1824 | if m.group(2) == '':
1825 | patch.start1 -= 1
1826 | patch.length1 = 1
1827 | elif m.group(2) == '0':
1828 | patch.length1 = 0
1829 | else:
1830 | patch.start1 -= 1
1831 | patch.length1 = int(m.group(2))
1832 |
1833 | patch.start2 = int(m.group(3))
1834 | if m.group(4) == '':
1835 | patch.start2 -= 1
1836 | patch.length2 = 1
1837 | elif m.group(4) == '0':
1838 | patch.length2 = 0
1839 | else:
1840 | patch.start2 -= 1
1841 | patch.length2 = int(m.group(4))
1842 |
1843 | del text[0]
1844 |
1845 | while len(text) != 0:
1846 | if text[0]:
1847 | sign = text[0][0]
1848 | else:
1849 | sign = ''
1850 | line = urllib.unquote(text[0][1:])
1851 | line = line.decode("utf-8")
1852 | if sign == '+':
1853 | # Insertion.
1854 | patch.diffs.append((self.DIFF_INSERT, line))
1855 | elif sign == '-':
1856 | # Deletion.
1857 | patch.diffs.append((self.DIFF_DELETE, line))
1858 | elif sign == ' ':
1859 | # Minor equality.
1860 | patch.diffs.append((self.DIFF_EQUAL, line))
1861 | elif sign == '@':
1862 | # Start of next patch.
1863 | break
1864 | elif sign == '':
1865 | # Blank line? Whatever.
1866 | pass
1867 | else:
1868 | # WTF?
1869 | raise ValueError("Invalid patch mode: '%s'\n%s" % (sign, line))
1870 | del text[0]
1871 | return patches
1872 |
1873 |
1874 | class patch_obj:
1875 | """Class representing one patch operation.
1876 | """
1877 |
1878 | def __init__(self):
1879 | """Initializes with an empty list of diffs.
1880 | """
1881 | self.diffs = []
1882 | self.start1 = None
1883 | self.start2 = None
1884 | self.length1 = 0
1885 | self.length2 = 0
1886 |
1887 | def __str__(self):
1888 | """Emmulate GNU diff's format.
1889 | Header: @@ -382,8 +481,9 @@
1890 | Indicies are printed as 1-based, not 0-based.
1891 |
1892 | Returns:
1893 | The GNU diff string.
1894 | """
1895 | if self.length1 == 0:
1896 | coords1 = str(self.start1) + ",0"
1897 | elif self.length1 == 1:
1898 | coords1 = str(self.start1 + 1)
1899 | else:
1900 | coords1 = str(self.start1 + 1) + "," + str(self.length1)
1901 | if self.length2 == 0:
1902 | coords2 = str(self.start2) + ",0"
1903 | elif self.length2 == 1:
1904 | coords2 = str(self.start2 + 1)
1905 | else:
1906 | coords2 = str(self.start2 + 1) + "," + str(self.length2)
1907 | text = ["@@ -", coords1, " +", coords2, " @@\n"]
1908 | # Escape the body of the patch with %xx notation.
1909 | for (op, data) in self.diffs:
1910 | if op == diff_match_patch.DIFF_INSERT:
1911 | text.append("+")
1912 | elif op == diff_match_patch.DIFF_DELETE:
1913 | text.append("-")
1914 | elif op == diff_match_patch.DIFF_EQUAL:
1915 | text.append(" ")
1916 | # High ascii will raise UnicodeDecodeError. Use Unicode instead.
1917 | data = data.encode("utf-8")
1918 | text.append(urllib.quote(data, "!~*'();/?:@&=+$,# ") + "\n")
1919 | return "".join(text)
1920 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | "12.2.0": "messages/12.2.0.txt",
36 | "14.0.0": "messages/14.0.0.txt"
37 | }
--------------------------------------------------------------------------------
/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 | Thanks for installing this plugin.
2 |
3 | Please, before posting an issue that this plugin is not formatting your code, answer the following questions:
4 |
5 | - Have you installed PHP in the computer which is running Sublime Text?
6 |
7 | - Is PHP configured in the default $PATH/%PATH% variable?
8 |
9 | - 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?
10 |
11 | - Are you running at least PHP 7.0?
12 |
13 | - Have you bought fmt.phar from phpfmt org? Or have you downloaded php-cs-fixer into the plugin's directory?
14 |
15 | If you have answered more than one "no", then double check your environment.
16 |
17 | XDebug makes this plugin to work much slower than normal. Consider disabling it before actually using phpfmt.
18 |
--------------------------------------------------------------------------------
/php.tools.ini:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nanch/sublime-phpfmt/cae557c18698dc6d906a80c9677fa77e1ab09997/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 | import json
11 | import urllib.request
12 | from os.path import dirname, realpath
13 |
14 | dist_dir = os.path.dirname(os.path.abspath(__file__))
15 | sys.path.insert(0, dist_dir)
16 | from diff_match_patch.python3.diff_match_patch import diff_match_patch
17 |
18 | def print_debug(*msg):
19 | if getSetting(sublime.active_window().active_view(), sublime.load_settings('phpfmt.sublime-settings'), "debug", False):
20 | print(msg)
21 |
22 | def getSetting( view, settings, key, default ):
23 | local = 'phpfmt.' + key
24 | return view.settings().get( local, settings.get( key, default ) )
25 |
26 | def dofmt(eself, eview, sgter = None, src = None, force = False):
27 | if int(sublime.version()) < 3000:
28 | print_debug("phpfmt: ST2 not supported")
29 | return False
30 |
31 | self = eself
32 | view = eview
33 | s = sublime.load_settings('phpfmt.sublime-settings')
34 | additional_extensions = s.get("additional_extensions", [])
35 |
36 | uri = view.file_name()
37 | dirnm, sfn = os.path.split(uri)
38 | ext = os.path.splitext(uri)[1][1:]
39 | if force is False and "php" != ext and not ext in additional_extensions:
40 | print_debug("phpfmt: not a PHP file")
41 | return False
42 |
43 | php_bin = getSetting( view, s, "php_bin", "php")
44 | formatter_path = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "fmt.phar")
45 |
46 | if not os.path.isfile(formatter_path):
47 | sublime.message_dialog("engine file is missing: "+formatter_path)
48 | return
49 |
50 | indent_with_space = getSetting( view, s, "indent_with_space", False)
51 | debug = getSetting( view, s, "debug", False)
52 | ignore_list = getSetting( view, s, "ignore_list", "")
53 | passes = getSetting( view, s, "passes", [])
54 | excludes = getSetting( view, s, "excludes", [])
55 |
56 |
57 | config_file = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "php.tools.ini")
58 |
59 | if force is False and "php" != ext and not ext in additional_extensions:
60 | print_debug("phpfmt: not a PHP file")
61 | return False
62 |
63 | if "" != ignore_list:
64 | if type(ignore_list) is not list:
65 | ignore_list = ignore_list.split(" ")
66 | for v in ignore_list:
67 | pos = uri.find(v)
68 | if -1 != pos and v != "":
69 | print_debug("phpfmt: skipping file")
70 | return False
71 |
72 | if not os.path.isfile(php_bin) and not php_bin == "php":
73 | print_debug("Can't find PHP binary file at "+php_bin)
74 | sublime.error_message("Can't find PHP binary file at "+php_bin)
75 |
76 | cmd_ver = [php_bin, '-v'];
77 | p = subprocess.Popen(cmd_ver, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
78 | res, err = p.communicate()
79 | print_debug("phpfmt (php_ver) cmd:\n", cmd_ver)
80 | print_debug("phpfmt (php_ver) out:\n", res.decode('utf-8'))
81 | print_debug("phpfmt (php_ver) err:\n", err.decode('utf-8'))
82 | 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')):
83 | s = debugEnvironment(php_bin, formatter_path)
84 | sublime.message_dialog('Warning.\nPHP 7.0 or newer is required.\nPlease, upgrade your local PHP installation.\nDebug information:'+s)
85 | return False
86 |
87 | s = debugEnvironment(php_bin, formatter_path)
88 | print_debug(s)
89 |
90 | lintret = 1
91 | if "AutoSemicolon" in passes:
92 | lintret = 0
93 | else:
94 | cmd_lint = [php_bin,"-ddisplay_errors=1","-l"];
95 | if src is None:
96 | cmd_lint.append(uri)
97 | p = subprocess.Popen(cmd_lint, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False)
98 | else:
99 | p = subprocess.Popen(cmd_lint, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
100 | p.stdin.write(src.encode('utf-8'))
101 |
102 | lint_out, lint_err = p.communicate()
103 | lintret = p.returncode
104 |
105 | if(lintret==0):
106 | cmd_fmt = [php_bin]
107 |
108 | if not debug:
109 | cmd_fmt.append("-ddisplay_errors=stderr")
110 |
111 | cmd_fmt.append(formatter_path)
112 | cmd_fmt.append("--config="+config_file)
113 |
114 | if indent_with_space is True:
115 | cmd_fmt.append("--indent_with_space")
116 | elif indent_with_space > 0:
117 | cmd_fmt.append("--indent_with_space="+str(indent_with_space))
118 |
119 | if len(passes) > 0:
120 | cmd_fmt.append("--passes="+','.join(passes))
121 |
122 | if len(excludes) > 0:
123 | cmd_fmt.append("--exclude="+','.join(excludes))
124 |
125 | if debug:
126 | cmd_fmt.append("-v")
127 |
128 | if src is None:
129 | cmd_fmt.append(uri)
130 | else:
131 | cmd_fmt.append("-")
132 |
133 | print_debug("cmd_fmt: ", cmd_fmt)
134 |
135 | if src is None:
136 | p = subprocess.Popen(cmd_fmt, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False)
137 | else:
138 | p = subprocess.Popen(cmd_fmt, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
139 |
140 | if src is not None:
141 | p.stdin.write(src.encode('utf-8'))
142 |
143 | res, err = p.communicate()
144 |
145 | print_debug("p:\n", p.returncode)
146 | print_debug("err:\n", err.decode('utf-8'))
147 |
148 | if p.returncode != 0:
149 | return ''
150 |
151 | return res.decode('utf-8')
152 | else:
153 | sublime.status_message("phpfmt: format failed - syntax errors found")
154 | print_debug("lint error: ", lint_out)
155 |
156 | def doreordermethod(eself, eview):
157 | self = eself
158 | view = eview
159 | s = sublime.load_settings('phpfmt.sublime-settings')
160 |
161 | additional_extensions = s.get("additional_extensions", [])
162 | autoimport = s.get("autoimport", True)
163 | debug = s.get("debug", False)
164 | enable_auto_align = s.get("enable_auto_align", False)
165 | ignore_list = s.get("ignore_list", "")
166 | indent_with_space = s.get("indent_with_space", False)
167 | psr1 = s.get("psr1", False)
168 | psr1_naming = s.get("psr1_naming", psr1)
169 | psr2 = s.get("psr2", False)
170 | smart_linebreak_after_curly = s.get("smart_linebreak_after_curly", True)
171 | visibility_order = s.get("visibility_order", False)
172 | yoda = s.get("yoda", False)
173 |
174 | passes = s.get("passes", [])
175 |
176 | php_bin = s.get("php_bin", "php")
177 | formatter_path = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "fmt.phar")
178 |
179 | config_file = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "php.tools.ini")
180 |
181 | uri = view.file_name()
182 | dirnm, sfn = os.path.split(uri)
183 | ext = os.path.splitext(uri)[1][1:]
184 |
185 | if "php" != ext and not ext in additional_extensions:
186 | print_debug("phpfmt: not a PHP file")
187 | sublime.status_message("phpfmt: not a PHP file")
188 | return False
189 |
190 | if not os.path.isfile(php_bin) and not php_bin == "php":
191 | print_debug("Can't find PHP binary file at "+php_bin)
192 | sublime.error_message("Can't find PHP binary file at "+php_bin)
193 |
194 |
195 | print_debug("phpfmt:", uri)
196 | if enable_auto_align:
197 | print_debug("auto align: enabled")
198 | else:
199 | print_debug("auto align: disabled")
200 |
201 |
202 |
203 | cmd_lint = [php_bin,"-l",uri];
204 | p = subprocess.Popen(cmd_lint, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False)
205 | lint_out, lint_err = p.communicate()
206 |
207 | if(p.returncode==0):
208 | cmd_fmt = [php_bin]
209 |
210 | if not debug:
211 | cmd_fmt.append("-ddisplay_errors=stderr")
212 |
213 | cmd_fmt.append(formatter_path)
214 | cmd_fmt.append("--config="+config_file)
215 |
216 | if psr1:
217 | cmd_fmt.append("--psr1")
218 |
219 | if psr1_naming:
220 | cmd_fmt.append("--psr1-naming")
221 |
222 | if psr2:
223 | cmd_fmt.append("--psr2")
224 |
225 | if indent_with_space:
226 | cmd_fmt.append("--indent_with_space")
227 | elif indent_with_space > 0:
228 | cmd_fmt.append("--indent_with_space="+str(indent_with_space))
229 |
230 | if enable_auto_align:
231 | cmd_fmt.append("--enable_auto_align")
232 |
233 | if visibility_order:
234 | cmd_fmt.append("--visibility_order")
235 |
236 | passes.append("OrganizeClass")
237 | if len(passes) > 0:
238 | cmd_fmt.append("--passes="+','.join(passes))
239 |
240 | cmd_fmt.append(uri)
241 |
242 | uri_tmp = uri + "~"
243 |
244 | print_debug("cmd_fmt: ", cmd_fmt)
245 |
246 | p = subprocess.Popen(cmd_fmt, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dirnm, shell=False)
247 | res, err = p.communicate()
248 | print_debug("err:\n", err.decode('utf-8'))
249 | sublime.set_timeout(revert_active_window, 50)
250 | else:
251 | print_debug("lint error: ", lint_out)
252 |
253 | def debugEnvironment(php_bin, formatter_path):
254 | ret = ""
255 | cmd_ver = [php_bin,"-v"];
256 | p = subprocess.Popen(cmd_ver, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
257 | res, err = p.communicate()
258 | ret += ("phpfmt (php version):\n"+res.decode('utf-8'))
259 | if err.decode('utf-8'):
260 | ret += ("phpfmt (php version) err:\n"+err.decode('utf-8'))
261 | ret += "\n"
262 |
263 | cmd_ver = [php_bin,"-m"];
264 | p = subprocess.Popen(cmd_ver, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
265 | res, err = p.communicate()
266 | if res.decode('utf-8').find("tokenizer") != -1:
267 | ret += ("phpfmt (php tokenizer) found\n")
268 | else:
269 | ret += ("phpfmt (php tokenizer):\n"+res.decode('utf-8'))
270 | if err.decode('utf-8'):
271 | ret += ("phpfmt (php tokenizer) err:\n"+err.decode('utf-8'))
272 | ret += "\n"
273 |
274 | cmd_ver = [php_bin,formatter_path,"--version"];
275 | p = subprocess.Popen(cmd_ver, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
276 | res, err = p.communicate()
277 | ret += ("phpfmt (fmt.phar version):\n"+res.decode('utf-8'))
278 | if err.decode('utf-8'):
279 | ret += ("phpfmt (fmt.phar version) err:\n"+err.decode('utf-8'))
280 | ret += "\n"
281 |
282 | return ret
283 |
284 | def revert_active_window():
285 | sublime.active_window().active_view().run_command("revert")
286 | sublime.active_window().active_view().run_command("phpcs_sniff_this_file")
287 |
288 | class phpfmt(sublime_plugin.EventListener):
289 | def on_pre_save(self, view):
290 | s = sublime.load_settings('phpfmt.sublime-settings')
291 | format_on_save = s.get("format_on_save", True)
292 |
293 | if format_on_save:
294 | view.run_command('php_fmt')
295 |
296 | class DebugEnvCommand(sublime_plugin.TextCommand):
297 | def run(self, edit):
298 | s = sublime.load_settings('phpfmt.sublime-settings')
299 |
300 | php_bin = s.get("php_bin", "php")
301 | formatter_path = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "fmt.phar")
302 |
303 | s = debugEnvironment(php_bin, formatter_path)
304 | sublime.message_dialog(s)
305 |
306 | class FmtNowCommand(sublime_plugin.TextCommand):
307 | def run(self, edit):
308 | vsize = self.view.size()
309 | src = self.view.substr(sublime.Region(0, vsize))
310 | if not src.strip():
311 | return
312 |
313 | src = dofmt(self, self.view, None, src, True)
314 | if src is False or src == "":
315 | return False
316 |
317 | _, err = merge(self.view, vsize, src, edit)
318 | print_debug(err)
319 |
320 | class TogglePassMenuCommand(sublime_plugin.TextCommand):
321 | def run(self, edit):
322 | s = sublime.load_settings('phpfmt.sublime-settings')
323 |
324 | php_bin = s.get("php_bin", "php")
325 | formatter_path = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "fmt.phar")
326 |
327 | cmd_passes = [php_bin,formatter_path,'--list-simple'];
328 | print_debug(cmd_passes)
329 |
330 | p = subprocess.Popen(cmd_passes, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
331 |
332 | out, err = p.communicate()
333 |
334 | descriptions = out.decode("utf-8").strip().split(os.linesep)
335 |
336 | def on_done(i):
337 | if i >= 0 :
338 | s = sublime.load_settings('phpfmt.sublime-settings')
339 | passes = s.get('passes', [])
340 | chosenPass = descriptions[i].split(' ')
341 | option = chosenPass[0]
342 |
343 | passDesc = option
344 |
345 | if option in passes:
346 | passes.remove(option)
347 | msg = "phpfmt: "+passDesc+" disabled"
348 | print_debug(msg)
349 | sublime.status_message(msg)
350 | else:
351 | passes.append(option)
352 | msg = "phpfmt: "+passDesc+" enabled"
353 | print_debug(msg)
354 | sublime.status_message(msg)
355 |
356 | s.set('passes', passes)
357 | sublime.save_settings('phpfmt.sublime-settings')
358 |
359 | self.view.window().show_quick_panel(descriptions, on_done, sublime.MONOSPACE_FONT)
360 |
361 | class ToggleExcludeMenuCommand(sublime_plugin.TextCommand):
362 | def run(self, edit):
363 | s = sublime.load_settings('phpfmt.sublime-settings')
364 |
365 | php_bin = s.get("php_bin", "php")
366 | formatter_path = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "fmt.phar")
367 |
368 | cmd_passes = [php_bin,formatter_path,'--list-simple'];
369 | print_debug(cmd_passes)
370 |
371 | p = subprocess.Popen(cmd_passes, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
372 |
373 | out, err = p.communicate()
374 |
375 | descriptions = out.decode("utf-8").strip().split(os.linesep)
376 |
377 | def on_done(i):
378 | if i >= 0 :
379 | s = sublime.load_settings('phpfmt.sublime-settings')
380 | excludes = s.get('excludes', [])
381 | chosenPass = descriptions[i].split(' ')
382 | option = chosenPass[0]
383 |
384 | passDesc = option
385 |
386 | if option in excludes:
387 | excludes.remove(option)
388 | msg = "phpfmt: "+passDesc+" disabled"
389 | print_debug(msg)
390 | sublime.status_message(msg)
391 | else:
392 | excludes.append(option)
393 | msg = "phpfmt: "+passDesc+" enabled"
394 | print_debug(msg)
395 | sublime.status_message(msg)
396 |
397 | s.set('excludes', excludes)
398 | sublime.save_settings('phpfmt.sublime-settings')
399 |
400 | self.view.window().show_quick_panel(descriptions, on_done, sublime.MONOSPACE_FONT)
401 |
402 | class ToggleCommand(sublime_plugin.TextCommand):
403 | def run(self, edit, option):
404 | s = sublime.load_settings('phpfmt.sublime-settings')
405 | options = {"format_on_save":"format on save"}
406 | s = sublime.load_settings('phpfmt.sublime-settings')
407 | value = s.get(option, False)
408 |
409 | if value:
410 | s.set(option, False)
411 | msg = "phpfmt: "+options[option]+" disabled"
412 | print_debug(msg)
413 | sublime.status_message(msg)
414 | else:
415 | s.set(option, True)
416 | msg = "phpfmt: "+options[option]+" enabled"
417 | print_debug(msg)
418 | sublime.status_message(msg)
419 |
420 | sublime.save_settings('phpfmt.sublime-settings')
421 |
422 | class UpdatePhpBinCommand(sublime_plugin.TextCommand):
423 | def run(self, edit):
424 | def execute(text):
425 | s = sublime.load_settings('phpfmt.sublime-settings')
426 | s.set("php_bin", text)
427 |
428 | s = sublime.load_settings('phpfmt.sublime-settings')
429 | self.view.window().show_input_panel('php binary path:', s.get("php_bin", ""), execute, None, None)
430 |
431 | class OrderMethodCommand(sublime_plugin.TextCommand):
432 | def run(self, edit):
433 | doreordermethod(self, self.view)
434 |
435 | class IndentWithSpacesCommand(sublime_plugin.TextCommand):
436 | def run(self, edit):
437 | s = sublime.load_settings('phpfmt.sublime-settings')
438 |
439 | def setIndentWithSpace(text):
440 | s = sublime.load_settings('phpfmt.sublime-settings')
441 | v = text.strip()
442 | if not v:
443 | v = False
444 | else:
445 | v = int(v)
446 | s.set("indent_with_space", v)
447 | sublime.save_settings('phpfmt.sublime-settings')
448 | sublime.status_message("phpfmt (indentation): done")
449 | sublime.active_window().active_view().run_command("fmt_now")
450 |
451 | s = sublime.load_settings('phpfmt.sublime-settings')
452 | spaces = s.get("indent_with_space", 4)
453 | if not spaces:
454 | spaces = ""
455 | spaces = str(spaces)
456 | self.view.window().show_input_panel('how many spaces? (leave it empty to return to tabs)', spaces, setIndentWithSpace, None, None)
457 |
458 | s = sublime.load_settings('phpfmt.sublime-settings')
459 | version = s.get('version', 1)
460 | s.set('version', version)
461 | sublime.save_settings('phpfmt.sublime-settings')
462 |
463 | if version == 2:
464 | print_debug("Convert to version 3")
465 | s.set('version', 3)
466 | sublime.save_settings('phpfmt.sublime-settings')
467 |
468 | if version == 3:
469 | print_debug("Convert to version 4")
470 | s.set('version', 4)
471 | passes = s.get('passes', [])
472 | passes.append("ReindentSwitchBlocks")
473 | s.set('passes', passes)
474 | sublime.save_settings('phpfmt.sublime-settings')
475 |
476 |
477 | def selfupdate():
478 | s = sublime.load_settings('phpfmt.sublime-settings')
479 |
480 | php_bin = s.get("php_bin", "php")
481 | formatter_path = os.path.join(dirname(realpath(sublime.packages_path())), "Packages", "phpfmt", "fmt.phar")
482 |
483 | channel = s.get("engine_channel", "alpha")
484 | version = s.get("engine_version", "")
485 |
486 | if channel == "alpha":
487 | sublime.message_dialog("fmt.phar is a commercial product.\nAlthough fmt.phar alpha is widely available, you are restricted to use for strictly personal and educational purposes.\nConsider buying a commercial license at: https://github.com/phpfmt/issues/issues/17")
488 |
489 | if version == "":
490 | releaseJSON = urllib.request.urlopen("https://raw.githubusercontent.com/phpfmt/releases/master/releases.json").read()
491 | releases = json.loads(releaseJSON.decode('utf-8'))
492 | version = releases[channel]
493 |
494 | downloadURL = "https://github.com/phpfmt/releases/raw/master/releases/"+channel+"/"+version+"/fmt.phar"
495 | urllib.request.urlretrieve (downloadURL, formatter_path)
496 |
497 | sublime.set_timeout_async(selfupdate, 3000)
498 |
499 |
500 | class PhpFmtCommand(sublime_plugin.TextCommand):
501 | def run(self, edit):
502 | vsize = self.view.size()
503 | src = self.view.substr(sublime.Region(0, vsize))
504 | if not src.strip():
505 | return
506 |
507 | src = dofmt(self, self.view, None, src)
508 | if src is False or src == "":
509 | return False
510 |
511 | _, err = merge(self.view, vsize, src, edit)
512 | print_debug(err)
513 |
514 | class MergeException(Exception):
515 | pass
516 |
517 | def _merge(view, size, text, edit):
518 | def ss(start, end):
519 | return view.substr(sublime.Region(start, end))
520 | dmp = diff_match_patch()
521 | diffs = dmp.diff_main(ss(0, size), text, False)
522 | dmp.diff_cleanupEfficiency(diffs)
523 | i = 0
524 | dirty = False
525 | for d in diffs:
526 | k, s = d
527 | l = len(s)
528 | if k == 0:
529 | # match
530 | l = len(s)
531 | if ss(i, i+l) != s:
532 | raise MergeException('mismatch', dirty)
533 | i += l
534 | else:
535 | dirty = True
536 | if k > 0:
537 | # insert
538 | view.insert(edit, i, s)
539 | i += l
540 | else:
541 | # delete
542 | if ss(i, i+l) != s:
543 | raise MergeException('mismatch', dirty)
544 | view.erase(edit, sublime.Region(i, i+l))
545 | return dirty
546 |
547 | def merge(view, size, text, edit):
548 | vs = view.settings()
549 | ttts = vs.get("translate_tabs_to_spaces")
550 | vs.set("translate_tabs_to_spaces", False)
551 | origin_src = view.substr(sublime.Region(0, view.size()))
552 | if not origin_src.strip():
553 | vs.set("translate_tabs_to_spaces", ttts)
554 | return (False, '')
555 |
556 | try:
557 | dirty = False
558 | err = ''
559 | if size < 0:
560 | size = view.size()
561 | dirty = _merge(view, size, text, edit)
562 | except MergeException as ex:
563 | dirty = True
564 | err = "Could not merge changes into the buffer, edit aborted: %s" % ex[0]
565 | view.replace(edit, sublime.Region(0, view.size()), origin_src)
566 | except Exception as ex:
567 | err = "error: %s" % ex
568 | finally:
569 | vs.set("translate_tabs_to_spaces", ttts)
570 | return (dirty, err)
571 |
--------------------------------------------------------------------------------
/phpfmt.sublime-settings:
--------------------------------------------------------------------------------
1 | {
2 | "version": 4,
3 | "engine_channel": "alpha",
4 | // "php_bin":"/usr/local/bin/php",
5 | // "format_on_save":true,
6 | "option": "value"
7 | }
8 |
--------------------------------------------------------------------------------