├── .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 | --------------------------------------------------------------------------------