From 0ae308daa77aeddb089cd6b7b0a443fca026266e Mon Sep 17 00:00:00 2001 From: Neil Fraser Date: Tue, 23 Jan 2018 13:41:02 -0800 Subject: [PATCH] Initial port from code.google.com MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plus header changes required by Google’s open source office. --- AUTHORS | 10 + CONTRIBUTING.md | 23 + LICENSE | 202 ++ README.txt | 43 + cpp/diff_match_patch.cpp | 2105 ++++++++++++++ cpp/diff_match_patch.h | 625 ++++ cpp/diff_match_patch.pro | 19 + cpp/diff_match_patch_test.cpp | 1197 ++++++++ cpp/diff_match_patch_test.h | 89 + csharp/DiffMatchPatch.cs | 2299 +++++++++++++++ csharp/DiffMatchPatchTest.cs | 1179 ++++++++ dart/DMPClass.dart | 2117 ++++++++++++++ dart/DiffClass.dart | 57 + dart/DiffMatchPatch.dart | 26 + dart/DiffMatchPatchTest.dart | 871 ++++++ dart/PatchClass.dart | 78 + dart/README.txt | 13 + demos/demo_diff.html | 87 + demos/demo_match.html | 92 + demos/demo_patch.html | 121 + .../neil/plaintext/diff_match_patch.java | 2470 ++++++++++++++++ .../neil/plaintext/diff_match_patch_test.java | 910 ++++++ javascript/diff_match_patch.js | 49 + javascript/diff_match_patch_test.html | 146 + javascript/diff_match_patch_test.js | 937 ++++++ javascript/diff_match_patch_uncompressed.js | 2192 ++++++++++++++ lua/diff_match_patch.lua | 2196 ++++++++++++++ lua/diff_match_patch_test.lua | 1201 ++++++++ .../current/diff_match_patch-current-src.jar | Bin 0 -> 134290 bytes .../current/diff_match_patch-current.jar | Bin 0 -> 24454 bytes .../current/diff_match_patch-current.pom | 1 + .../diff_match_patch/maven-metadata.xml | 1 + .../Configurations/Base+SnowLeopard.xcconfig | 3 + objectivec/Configurations/Base.xcconfig | 35 + objectivec/Configurations/Version.xcconfig | 2 + objectivec/DiffMatchPatch.h | 174 ++ objectivec/DiffMatchPatch.m | 2559 +++++++++++++++++ .../DiffMatchPatch.xcodeproj/project.pbxproj | 771 +++++ objectivec/DiffMatchPatchCFUtilities.c | 586 ++++ objectivec/DiffMatchPatchCFUtilities.h | 48 + objectivec/DiffMatchPatch_Prefix.pch | 7 + objectivec/English.lproj/InfoPlist.strings | 2 + objectivec/Info.plist | 28 + objectivec/MinMaxMacros.h | 40 + .../NSMutableDictionary+DMPExtensions.h | 46 + .../NSMutableDictionary+DMPExtensions.m | 108 + objectivec/NSString+JavaSubstring.h | 29 + objectivec/NSString+JavaSubstring.m | 35 + objectivec/NSString+UnicharUtilities.h | 30 + objectivec/NSString+UnicharUtilities.m | 39 + objectivec/NSString+UriCompatibility.h | 30 + objectivec/NSString+UriCompatibility.m | 62 + objectivec/Speedtest1.txt | 230 ++ objectivec/Speedtest2.txt | 188 ++ .../Tests/DiffMatchPatchTest-Info.plist | 22 + objectivec/Tests/DiffMatchPatchTest.h | 31 + objectivec/Tests/DiffMatchPatchTest.m | 1315 +++++++++ objectivec/Tests/speedtest.m | 50 + objectivec/speedtest_Prefix.pch | 7 + python2/__init__.py | 2 + python2/diff_match_patch.py | 1918 ++++++++++++ python2/diff_match_patch_test.py | 868 ++++++ python3/__init__.py | 2 + python3/diff_match_patch.py | 1906 ++++++++++++ python3/diff_match_patch_test.py | 870 ++++++ 65 files changed, 33399 insertions(+) create mode 100644 AUTHORS create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.txt create mode 100644 cpp/diff_match_patch.cpp create mode 100644 cpp/diff_match_patch.h create mode 100644 cpp/diff_match_patch.pro create mode 100644 cpp/diff_match_patch_test.cpp create mode 100644 cpp/diff_match_patch_test.h create mode 100644 csharp/DiffMatchPatch.cs create mode 100644 csharp/DiffMatchPatchTest.cs create mode 100644 dart/DMPClass.dart create mode 100644 dart/DiffClass.dart create mode 100644 dart/DiffMatchPatch.dart create mode 100644 dart/DiffMatchPatchTest.dart create mode 100644 dart/PatchClass.dart create mode 100644 dart/README.txt create mode 100644 demos/demo_diff.html create mode 100644 demos/demo_match.html create mode 100644 demos/demo_patch.html create mode 100644 java/name/fraser/neil/plaintext/diff_match_patch.java create mode 100644 java/name/fraser/neil/plaintext/diff_match_patch_test.java create mode 100644 javascript/diff_match_patch.js create mode 100644 javascript/diff_match_patch_test.html create mode 100644 javascript/diff_match_patch_test.js create mode 100644 javascript/diff_match_patch_uncompressed.js create mode 100644 lua/diff_match_patch.lua create mode 100644 lua/diff_match_patch_test.lua create mode 100644 maven/diff_match_patch/diff_match_patch/current/diff_match_patch-current-src.jar create mode 100755 maven/diff_match_patch/diff_match_patch/current/diff_match_patch-current.jar create mode 100644 maven/diff_match_patch/diff_match_patch/current/diff_match_patch-current.pom create mode 100644 maven/diff_match_patch/diff_match_patch/maven-metadata.xml create mode 100755 objectivec/Configurations/Base+SnowLeopard.xcconfig create mode 100755 objectivec/Configurations/Base.xcconfig create mode 100755 objectivec/Configurations/Version.xcconfig create mode 100755 objectivec/DiffMatchPatch.h create mode 100755 objectivec/DiffMatchPatch.m create mode 100755 objectivec/DiffMatchPatch.xcodeproj/project.pbxproj create mode 100755 objectivec/DiffMatchPatchCFUtilities.c create mode 100755 objectivec/DiffMatchPatchCFUtilities.h create mode 100755 objectivec/DiffMatchPatch_Prefix.pch create mode 100755 objectivec/English.lproj/InfoPlist.strings create mode 100755 objectivec/Info.plist create mode 100755 objectivec/MinMaxMacros.h create mode 100755 objectivec/NSMutableDictionary+DMPExtensions.h create mode 100755 objectivec/NSMutableDictionary+DMPExtensions.m create mode 100755 objectivec/NSString+JavaSubstring.h create mode 100755 objectivec/NSString+JavaSubstring.m create mode 100755 objectivec/NSString+UnicharUtilities.h create mode 100755 objectivec/NSString+UnicharUtilities.m create mode 100755 objectivec/NSString+UriCompatibility.h create mode 100755 objectivec/NSString+UriCompatibility.m create mode 100644 objectivec/Speedtest1.txt create mode 100644 objectivec/Speedtest2.txt create mode 100755 objectivec/Tests/DiffMatchPatchTest-Info.plist create mode 100755 objectivec/Tests/DiffMatchPatchTest.h create mode 100755 objectivec/Tests/DiffMatchPatchTest.m create mode 100755 objectivec/Tests/speedtest.m create mode 100755 objectivec/speedtest_Prefix.pch create mode 100644 python2/__init__.py create mode 100644 python2/diff_match_patch.py create mode 100644 python2/diff_match_patch_test.py create mode 100644 python3/__init__.py create mode 100644 python3/diff_match_patch.py create mode 100644 python3/diff_match_patch_test.py diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..c82809e --- /dev/null +++ b/AUTHORS @@ -0,0 +1,10 @@ +# Below is a list of people and organizations that have contributed +# to the Diff Match Patch project. + +Google Inc. + +Duncan Cross (Lua port) +Jan Weiß (Objective C port) +Matthaeus G. Chajdas (C# port) +Mike Slemmer (C++ port) + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ae319c7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution, +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..4ccbe63 --- /dev/null +++ b/README.txt @@ -0,0 +1,43 @@ +Diff, Match and Patch Library +https://github.com/google/diff-match-patch +Neil Fraser + +This library is currently available in seven different ports, all using the same API. +Every version includes a full set of unit tests. + +C++: +* Ported by Mike Slemmer. +* Currently requires the Qt library. + +C#: +* Ported by Matthaeus G. Chajdas. + +Dart: +* The Dart language is still growing and evolving, so this port is only as + stable as the underlying language. + +Java: +* Included is both the source and a Maven package. + +JavaScript: +* diff_match_patch_uncompressed.js is the human-readable version. + Users of node.js should 'require' this uncompressed version since the + compressed version is not guaranteed to work outside of a web browser. +* diff_match_patch.js has been compressed using Google's internal JavaScript compressor. + Non-Google hackers who wish to recompress the source can use: + http://dean.edwards.name/packer/ + +Lua: +* Ported by Duncan Cross. +* Does not support line-mode speedup. + +Objective C: +* Ported by Jan Weiss. +* Includes speed test (this is a separate bundle for other languages). + +Python: +* Two versions, one for Python 2.x, the other for Python 3.x. +* Runs 10x faster under PyPy than CPython. + +Demos: +* Separate demos for Diff, Match and Patch in JavaScript. diff --git a/cpp/diff_match_patch.cpp b/cpp/diff_match_patch.cpp new file mode 100644 index 0000000..4574f25 --- /dev/null +++ b/cpp/diff_match_patch.cpp @@ -0,0 +1,2105 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +// Code known to compile and run with Qt 4.3 through Qt 4.7. +#include +#include +#include "diff_match_patch.h" + + +////////////////////////// +// +// Diff Class +// +////////////////////////// + + +/** + * Constructor. Initializes the diff with the provided values. + * @param operation One of INSERT, DELETE or EQUAL + * @param text The text being applied + */ +Diff::Diff(Operation _operation, const QString &_text) : + operation(_operation), text(_text) { + // Construct a diff with the specified operation and text. +} + +Diff::Diff() { +} + + +QString Diff::strOperation(Operation op) { + switch (op) { + case INSERT: + return "INSERT"; + case DELETE: + return "DELETE"; + case EQUAL: + return "EQUAL"; + } + throw "Invalid operation."; +} + +/** + * Display a human-readable version of this Diff. + * @return text version + */ +QString Diff::toString() const { + QString prettyText = text; + // Replace linebreaks with Pilcrow signs. + prettyText.replace('\n', L'\u00b6'); + return QString("Diff(") + strOperation(operation) + QString(",\"") + + prettyText + QString("\")"); +} + +/** + * Is this Diff equivalent to another Diff? + * @param d Another Diff to compare against + * @return true or false + */ +bool Diff::operator==(const Diff &d) const { + return (d.operation == this->operation) && (d.text == this->text); +} + +bool Diff::operator!=(const Diff &d) const { + return !(operator == (d)); +} + + +///////////////////////////////////////////// +// +// Patch Class +// +///////////////////////////////////////////// + + +/** + * Constructor. Initializes with an empty list of diffs. + */ +Patch::Patch() : + start1(0), start2(0), + length1(0), length2(0) { +} + +bool Patch::isNull() const { + if (start1 == 0 && start2 == 0 && length1 == 0 && length2 == 0 + && diffs.size() == 0) { + return true; + } + return false; +} + + +/** + * Emmulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indicies are printed as 1-based, not 0-based. + * @return The GNU diff string + */ +QString Patch::toString() { + QString coords1, coords2; + if (length1 == 0) { + coords1 = QString::number(start1) + QString(",0"); + } else if (length1 == 1) { + coords1 = QString::number(start1 + 1); + } else { + coords1 = QString::number(start1 + 1) + QString(",") + + QString::number(length1); + } + if (length2 == 0) { + coords2 = QString::number(start2) + QString(",0"); + } else if (length2 == 1) { + coords2 = QString::number(start2 + 1); + } else { + coords2 = QString::number(start2 + 1) + QString(",") + + QString::number(length2); + } + QString text; + text = QString("@@ -") + coords1 + QString(" +") + coords2 + + QString(" @@\n"); + // Escape the body of the patch with %xx notation. + foreach (Diff aDiff, diffs) { + switch (aDiff.operation) { + case INSERT: + text += QString('+'); + break; + case DELETE: + text += QString('-'); + break; + case EQUAL: + text += QString(' '); + break; + } + text += QString(QUrl::toPercentEncoding(aDiff.text, " !~*'();/?:@&=+$,#")) + + QString("\n"); + } + + return text; +} + + +///////////////////////////////////////////// +// +// diff_match_patch Class +// +///////////////////////////////////////////// + +diff_match_patch::diff_match_patch() : + Diff_Timeout(1.0f), + Diff_EditCost(4), + Match_Threshold(0.5f), + Match_Distance(1000), + Patch_DeleteThreshold(0.5f), + Patch_Margin(4), + Match_MaxBits(32) { +} + + +QList diff_match_patch::diff_main(const QString &text1, + const QString &text2) { + return diff_main(text1, text2, true); +} + +QList diff_match_patch::diff_main(const QString &text1, + const QString &text2, bool checklines) { + // Set a deadline by which time the diff must be complete. + clock_t deadline; + if (Diff_Timeout <= 0) { + deadline = std::numeric_limits::max(); + } else { + deadline = clock() + (clock_t)(Diff_Timeout * CLOCKS_PER_SEC); + } + return diff_main(text1, text2, checklines, deadline); +} + +QList diff_match_patch::diff_main(const QString &text1, + const QString &text2, bool checklines, clock_t deadline) { + // Check for null inputs. + if (text1.isNull() || text2.isNull()) { + throw "Null inputs. (diff_main)"; + } + + // Check for equality (speedup). + QList diffs; + if (text1 == text2) { + if (!text1.isEmpty()) { + diffs.append(Diff(EQUAL, text1)); + } + return diffs; + } + + // Trim off common prefix (speedup). + int commonlength = diff_commonPrefix(text1, text2); + const QString &commonprefix = text1.left(commonlength); + QString textChopped1 = text1.mid(commonlength); + QString textChopped2 = text2.mid(commonlength); + + // Trim off common suffix (speedup). + commonlength = diff_commonSuffix(textChopped1, textChopped2); + const QString &commonsuffix = textChopped1.right(commonlength); + textChopped1 = textChopped1.left(textChopped1.length() - commonlength); + textChopped2 = textChopped2.left(textChopped2.length() - commonlength); + + // Compute the diff on the middle block. + diffs = diff_compute(textChopped1, textChopped2, checklines, deadline); + + // Restore the prefix and suffix. + if (!commonprefix.isEmpty()) { + diffs.prepend(Diff(EQUAL, commonprefix)); + } + if (!commonsuffix.isEmpty()) { + diffs.append(Diff(EQUAL, commonsuffix)); + } + + diff_cleanupMerge(diffs); + + return diffs; +} + + +QList diff_match_patch::diff_compute(QString text1, QString text2, + bool checklines, clock_t deadline) { + QList diffs; + + if (text1.isEmpty()) { + // Just add some text (speedup). + diffs.append(Diff(INSERT, text2)); + return diffs; + } + + if (text2.isEmpty()) { + // Just delete some text (speedup). + diffs.append(Diff(DELETE, text1)); + return diffs; + } + + { + const QString longtext = text1.length() > text2.length() ? text1 : text2; + const QString shorttext = text1.length() > text2.length() ? text2 : text1; + const int i = longtext.indexOf(shorttext); + if (i != -1) { + // Shorter text is inside the longer text (speedup). + const Operation op = (text1.length() > text2.length()) ? DELETE : INSERT; + diffs.append(Diff(op, longtext.left(i))); + diffs.append(Diff(EQUAL, shorttext)); + diffs.append(Diff(op, safeMid(longtext, i + shorttext.length()))); + return diffs; + } + + if (shorttext.length() == 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + diffs.append(Diff(DELETE, text1)); + diffs.append(Diff(INSERT, text2)); + return diffs; + } + // Garbage collect longtext and shorttext by scoping out. + } + + // Check to see if the problem can be split in two. + const QStringList hm = diff_halfMatch(text1, text2); + if (hm.count() > 0) { + // A half-match was found, sort out the return data. + const QString text1_a = hm[0]; + const QString text1_b = hm[1]; + const QString text2_a = hm[2]; + const QString text2_b = hm[3]; + const QString mid_common = hm[4]; + // Send both pairs off for separate processing. + const QList diffs_a = diff_main(text1_a, text2_a, + checklines, deadline); + const QList diffs_b = diff_main(text1_b, text2_b, + checklines, deadline); + // Merge the results. + diffs = diffs_a; + diffs.append(Diff(EQUAL, mid_common)); + diffs += diffs_b; + return diffs; + } + + // Perform a real diff. + if (checklines && text1.length() > 100 && text2.length() > 100) { + return diff_lineMode(text1, text2, deadline); + } + + return diff_bisect(text1, text2, deadline); +} + + +QList diff_match_patch::diff_lineMode(QString text1, QString text2, + clock_t deadline) { + // Scan the text on a line-by-line basis first. + const QList b = diff_linesToChars(text1, text2); + text1 = b[0].toString(); + text2 = b[1].toString(); + QStringList linearray = b[2].toStringList(); + + QList diffs = diff_main(text1, text2, false, deadline); + + // Convert the diff back to original text. + diff_charsToLines(diffs, linearray); + // Eliminate freak matches (e.g. blank lines) + diff_cleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.append(Diff(EQUAL, "")); + int count_delete = 0; + int count_insert = 0; + QString text_delete = ""; + QString text_insert = ""; + + QMutableListIterator pointer(diffs); + Diff *thisDiff = pointer.hasNext() ? &pointer.next() : NULL; + while (thisDiff != NULL) { + switch (thisDiff->operation) { + case INSERT: + count_insert++; + text_insert += thisDiff->text; + break; + case DELETE: + count_delete++; + text_delete += thisDiff->text; + break; + case EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + pointer.previous(); + for (int j = 0; j < count_delete + count_insert; j++) { + pointer.previous(); + pointer.remove(); + } + foreach(Diff newDiff, + diff_main(text_delete, text_insert, false, deadline)) { + pointer.insert(newDiff); + } + } + count_insert = 0; + count_delete = 0; + text_delete = ""; + text_insert = ""; + break; + } + thisDiff = pointer.hasNext() ? &pointer.next() : NULL; + } + diffs.removeLast(); // Remove the dummy entry at the end. + + return diffs; +} + + +QList diff_match_patch::diff_bisect(const QString &text1, + const QString &text2, clock_t deadline) { + // Cache the text lengths to prevent multiple calls. + const int text1_length = text1.length(); + const int text2_length = text2.length(); + const int max_d = (text1_length + text2_length + 1) / 2; + const int v_offset = max_d; + const int v_length = 2 * max_d; + int *v1 = new int[v_length]; + int *v2 = new int[v_length]; + for (int x = 0; x < v_length; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[v_offset + 1] = 0; + v2[v_offset + 1] = 0; + const int delta = text1_length - text2_length; + // If the total number of characters is odd, then the front path will + // collide with the reverse path. + const bool front = (delta % 2 != 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + int k1start = 0; + int k1end = 0; + int k2start = 0; + int k2end = 0; + for (int d = 0; d < max_d; d++) { + // Bail out if deadline is reached. + if (clock() > deadline) { + break; + } + + // Walk the front path one step. + for (int k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + const int k1_offset = v_offset + k1; + int x1; + if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { + x1 = v1[k1_offset + 1]; + } else { + x1 = v1[k1_offset - 1] + 1; + } + int y1 = x1 - k1; + while (x1 < text1_length && y1 < text2_length + && text1[x1] == text2[y1]) { + x1++; + y1++; + } + v1[k1_offset] = x1; + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + int k2_offset = v_offset + delta - k1; + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { + // Mirror x2 onto top-left coordinate system. + int x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + delete [] v1; + delete [] v2; + return diff_bisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + + // Walk the reverse path one step. + for (int k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + const int k2_offset = v_offset + k2; + int x2; + if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { + x2 = v2[k2_offset + 1]; + } else { + x2 = v2[k2_offset - 1] + 1; + } + int y2 = x2 - k2; + while (x2 < text1_length && y2 < text2_length + && text1[text1_length - x2 - 1] == text2[text2_length - y2 - 1]) { + x2++; + y2++; + } + v2[k2_offset] = x2; + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + int k1_offset = v_offset + delta - k2; + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { + int x1 = v1[k1_offset]; + int y1 = v_offset + x1 - k1_offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2; + if (x1 >= x2) { + // Overlap detected. + delete [] v1; + delete [] v2; + return diff_bisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + } + delete [] v1; + delete [] v2; + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + QList diffs; + diffs.append(Diff(DELETE, text1)); + diffs.append(Diff(INSERT, text2)); + return diffs; +} + +QList diff_match_patch::diff_bisectSplit(const QString &text1, + const QString &text2, int x, int y, clock_t deadline) { + QString text1a = text1.left(x); + QString text2a = text2.left(y); + QString text1b = safeMid(text1, x); + QString text2b = safeMid(text2, y); + + // Compute both diffs serially. + QList diffs = diff_main(text1a, text2a, false, deadline); + QList diffsb = diff_main(text1b, text2b, false, deadline); + + return diffs + diffsb; +} + +QList diff_match_patch::diff_linesToChars(const QString &text1, + const QString &text2) { + QStringList lineArray; + QMap lineHash; + // e.g. linearray[4] == "Hello\n" + // e.g. linehash.get("Hello\n") == 4 + + // "\x00" is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray.append(""); + + const QString chars1 = diff_linesToCharsMunge(text1, lineArray, lineHash); + const QString chars2 = diff_linesToCharsMunge(text2, lineArray, lineHash); + + QList listRet; + listRet.append(QVariant::fromValue(chars1)); + listRet.append(QVariant::fromValue(chars2)); + listRet.append(QVariant::fromValue(lineArray)); + return listRet; +} + + +QString diff_match_patch::diff_linesToCharsMunge(const QString &text, + QStringList &lineArray, + QMap &lineHash) { + int lineStart = 0; + int lineEnd = -1; + QString line; + QString chars; + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + while (lineEnd < text.length() - 1) { + lineEnd = text.indexOf('\n', lineStart); + if (lineEnd == -1) { + lineEnd = text.length() - 1; + } + line = safeMid(text, lineStart, lineEnd + 1 - lineStart); + lineStart = lineEnd + 1; + + if (lineHash.contains(line)) { + chars += QChar(static_cast(lineHash.value(line))); + } else { + lineArray.append(line); + lineHash.insert(line, lineArray.size() - 1); + chars += QChar(static_cast(lineArray.size() - 1)); + } + } + return chars; +} + + + +void diff_match_patch::diff_charsToLines(QList &diffs, + const QStringList &lineArray) { + // Qt has no mutable foreach construct. + QMutableListIterator i(diffs); + while (i.hasNext()) { + Diff &diff = i.next(); + QString text; + for (int y = 0; y < diff.text.length(); y++) { + text += lineArray.value(static_cast(diff.text[y].unicode())); + } + diff.text = text; + } +} + + +int diff_match_patch::diff_commonPrefix(const QString &text1, + const QString &text2) { + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + const int n = std::min(text1.length(), text2.length()); + for (int i = 0; i < n; i++) { + if (text1[i] != text2[i]) { + return i; + } + } + return n; +} + + +int diff_match_patch::diff_commonSuffix(const QString &text1, + const QString &text2) { + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + const int text1_length = text1.length(); + const int text2_length = text2.length(); + const int n = std::min(text1_length, text2_length); + for (int i = 1; i <= n; i++) { + if (text1[text1_length - i] != text2[text2_length - i]) { + return i - 1; + } + } + return n; +} + +int diff_match_patch::diff_commonOverlap(const QString &text1, + const QString &text2) { + // Cache the text lengths to prevent multiple calls. + const int text1_length = text1.length(); + const int text2_length = text2.length(); + // Eliminate the null case. + if (text1_length == 0 || text2_length == 0) { + return 0; + } + // Truncate the longer string. + QString text1_trunc = text1; + QString text2_trunc = text2; + if (text1_length > text2_length) { + text1_trunc = text1.right(text2_length); + } else if (text1_length < text2_length) { + text2_trunc = text2.left(text1_length); + } + const int text_length = std::min(text1_length, text2_length); + // Quick check for the worst case. + if (text1_trunc == text2_trunc) { + return text_length; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: http://neil.fraser.name/news/2010/11/04/ + int best = 0; + int length = 1; + while (true) { + QString pattern = text1_trunc.right(length); + int found = text2_trunc.indexOf(pattern); + if (found == -1) { + return best; + } + length += found; + if (found == 0 || text1_trunc.right(length) == text2_trunc.left(length)) { + best = length; + length++; + } + } +} + +QStringList diff_match_patch::diff_halfMatch(const QString &text1, + const QString &text2) { + if (Diff_Timeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return QStringList(); + } + const QString longtext = text1.length() > text2.length() ? text1 : text2; + const QString shorttext = text1.length() > text2.length() ? text2 : text1; + if (longtext.length() < 4 || shorttext.length() * 2 < longtext.length()) { + return QStringList(); // Pointless. + } + + // First check if the second quarter is the seed for a half-match. + const QStringList hm1 = diff_halfMatchI(longtext, shorttext, + (longtext.length() + 3) / 4); + // Check again based on the third quarter. + const QStringList hm2 = diff_halfMatchI(longtext, shorttext, + (longtext.length() + 1) / 2); + QStringList hm; + if (hm1.isEmpty() && hm2.isEmpty()) { + return QStringList(); + } else if (hm2.isEmpty()) { + hm = hm1; + } else if (hm1.isEmpty()) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].length() > hm2[4].length() ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + if (text1.length() > text2.length()) { + return hm; + } else { + QStringList listRet; + listRet << hm[2] << hm[3] << hm[0] << hm[1] << hm[4]; + return listRet; + } +} + + +QStringList diff_match_patch::diff_halfMatchI(const QString &longtext, + const QString &shorttext, + int i) { + // Start with a 1/4 length substring at position i as a seed. + const QString seed = safeMid(longtext, i, longtext.length() / 4); + int j = -1; + QString best_common; + QString best_longtext_a, best_longtext_b; + QString best_shorttext_a, best_shorttext_b; + while ((j = shorttext.indexOf(seed, j + 1)) != -1) { + const int prefixLength = diff_commonPrefix(safeMid(longtext, i), + safeMid(shorttext, j)); + const int suffixLength = diff_commonSuffix(longtext.left(i), + shorttext.left(j)); + if (best_common.length() < suffixLength + prefixLength) { + best_common = safeMid(shorttext, j - suffixLength, suffixLength) + + safeMid(shorttext, j, prefixLength); + best_longtext_a = longtext.left(i - suffixLength); + best_longtext_b = safeMid(longtext, i + prefixLength); + best_shorttext_a = shorttext.left(j - suffixLength); + best_shorttext_b = safeMid(shorttext, j + prefixLength); + } + } + if (best_common.length() * 2 >= longtext.length()) { + QStringList listRet; + listRet << best_longtext_a << best_longtext_b << best_shorttext_a + << best_shorttext_b << best_common; + return listRet; + } else { + return QStringList(); + } +} + + +void diff_match_patch::diff_cleanupSemantic(QList &diffs) { + if (diffs.isEmpty()) { + return; + } + bool changes = false; + QStack equalities; // Stack of equalities. + QString lastequality; // Always equal to equalities.lastElement().text + QMutableListIterator pointer(diffs); + // Number of characters that changed prior to the equality. + int length_insertions1 = 0; + int length_deletions1 = 0; + // Number of characters that changed after the equality. + int length_insertions2 = 0; + int length_deletions2 = 0; + Diff *thisDiff = pointer.hasNext() ? &pointer.next() : NULL; + while (thisDiff != NULL) { + if (thisDiff->operation == EQUAL) { + // Equality found. + equalities.push(*thisDiff); + length_insertions1 = length_insertions2; + length_deletions1 = length_deletions2; + length_insertions2 = 0; + length_deletions2 = 0; + lastequality = thisDiff->text; + } else { + // An insertion or deletion. + if (thisDiff->operation == INSERT) { + length_insertions2 += thisDiff->text.length(); + } else { + length_deletions2 += thisDiff->text.length(); + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (!lastequality.isNull() + && (lastequality.length() + <= std::max(length_insertions1, length_deletions1)) + && (lastequality.length() + <= std::max(length_insertions2, length_deletions2))) { + // printf("Splitting: '%s'\n", qPrintable(lastequality)); + // Walk back to offending equality. + while (*thisDiff != equalities.top()) { + thisDiff = &pointer.previous(); + } + pointer.next(); + + // Replace equality with a delete. + pointer.setValue(Diff(DELETE, lastequality)); + // Insert a corresponding an insert. + pointer.insert(Diff(INSERT, lastequality)); + + equalities.pop(); // Throw away the equality we just deleted. + if (!equalities.isEmpty()) { + // Throw away the previous equality (it needs to be reevaluated). + equalities.pop(); + } + if (equalities.isEmpty()) { + // There are no previous equalities, walk back to the start. + while (pointer.hasPrevious()) { + pointer.previous(); + } + } else { + // There is a safe equality we can fall back to. + thisDiff = &equalities.top(); + while (*thisDiff != pointer.previous()) { + // Intentionally empty loop. + } + } + + length_insertions1 = 0; // Reset the counters. + length_deletions1 = 0; + length_insertions2 = 0; + length_deletions2 = 0; + lastequality = QString(); + changes = true; + } + } + thisDiff = pointer.hasNext() ? &pointer.next() : NULL; + } + + // Normalize the diff. + if (changes) { + diff_cleanupMerge(diffs); + } + diff_cleanupSemanticLossless(diffs); + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer.toFront(); + Diff *prevDiff = NULL; + thisDiff = NULL; + if (pointer.hasNext()) { + prevDiff = &pointer.next(); + if (pointer.hasNext()) { + thisDiff = &pointer.next(); + } + } + while (thisDiff != NULL) { + if (prevDiff->operation == DELETE && + thisDiff->operation == INSERT) { + QString deletion = prevDiff->text; + QString insertion = thisDiff->text; + int overlap_length1 = diff_commonOverlap(deletion, insertion); + int overlap_length2 = diff_commonOverlap(insertion, deletion); + if (overlap_length1 >= overlap_length2) { + if (overlap_length1 >= deletion.length() / 2.0 || + overlap_length1 >= insertion.length() / 2.0) { + // Overlap found. Insert an equality and trim the surrounding edits. + pointer.previous(); + pointer.insert(Diff(EQUAL, insertion.left(overlap_length1))); + prevDiff->text = + deletion.left(deletion.length() - overlap_length1); + thisDiff->text = safeMid(insertion, overlap_length1); + // pointer.insert inserts the element before the cursor, so there is + // no need to step past the new element. + } + } else { + if (overlap_length2 >= deletion.length() / 2.0 || + overlap_length2 >= insertion.length() / 2.0) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + pointer.previous(); + pointer.insert(Diff(EQUAL, deletion.left(overlap_length2))); + prevDiff->operation = INSERT; + prevDiff->text = + insertion.left(insertion.length() - overlap_length2); + thisDiff->operation = DELETE; + thisDiff->text = safeMid(deletion, overlap_length2); + // pointer.insert inserts the element before the cursor, so there is + // no need to step past the new element. + } + } + thisDiff = pointer.hasNext() ? &pointer.next() : NULL; + } + prevDiff = thisDiff; + thisDiff = pointer.hasNext() ? &pointer.next() : NULL; + } +} + + +void diff_match_patch::diff_cleanupSemanticLossless(QList &diffs) { + QString equality1, edit, equality2; + QString commonString; + int commonOffset; + int score, bestScore; + QString bestEquality1, bestEdit, bestEquality2; + // Create a new iterator at the start. + QMutableListIterator pointer(diffs); + Diff *prevDiff = pointer.hasNext() ? &pointer.next() : NULL; + Diff *thisDiff = pointer.hasNext() ? &pointer.next() : NULL; + Diff *nextDiff = pointer.hasNext() ? &pointer.next() : NULL; + + // Intentionally ignore the first and last element (don't need checking). + while (nextDiff != NULL) { + if (prevDiff->operation == EQUAL && + nextDiff->operation == EQUAL) { + // This is a single edit surrounded by equalities. + equality1 = prevDiff->text; + edit = thisDiff->text; + equality2 = nextDiff->text; + + // First, shift the edit as far left as possible. + commonOffset = diff_commonSuffix(equality1, edit); + if (commonOffset != 0) { + commonString = safeMid(edit, edit.length() - commonOffset); + equality1 = equality1.left(equality1.length() - commonOffset); + edit = commonString + edit.left(edit.length() - commonOffset); + equality2 = commonString + equality2; + } + + // Second, step character by character right, looking for the best fit. + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + bestScore = diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2); + while (!edit.isEmpty() && !equality2.isEmpty() + && edit[0] == equality2[0]) { + equality1 += edit[0]; + edit = safeMid(edit, 1) + equality2[0]; + equality2 = safeMid(equality2, 1); + score = diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2); + // The >= encourages trailing rather than leading whitespace on edits. + if (score >= bestScore) { + bestScore = score; + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + } + } + + if (prevDiff->text != bestEquality1) { + // We have an improvement, save it back to the diff. + if (!bestEquality1.isEmpty()) { + prevDiff->text = bestEquality1; + } else { + pointer.previous(); // Walk past nextDiff. + pointer.previous(); // Walk past thisDiff. + pointer.previous(); // Walk past prevDiff. + pointer.remove(); // Delete prevDiff. + pointer.next(); // Walk past thisDiff. + pointer.next(); // Walk past nextDiff. + } + thisDiff->text = bestEdit; + if (!bestEquality2.isEmpty()) { + nextDiff->text = bestEquality2; + } else { + pointer.remove(); // Delete nextDiff. + nextDiff = thisDiff; + thisDiff = prevDiff; + } + } + } + prevDiff = thisDiff; + thisDiff = nextDiff; + nextDiff = pointer.hasNext() ? &pointer.next() : NULL; + } +} + + +int diff_match_patch::diff_cleanupSemanticScore(const QString &one, + const QString &two) { + if (one.isEmpty() || two.isEmpty()) { + // Edges are the best. + return 6; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + QChar char1 = one[one.length() - 1]; + QChar char2 = two[0]; + bool nonAlphaNumeric1 = !char1.isLetterOrNumber(); + bool nonAlphaNumeric2 = !char2.isLetterOrNumber(); + bool whitespace1 = nonAlphaNumeric1 && char1.isSpace(); + bool whitespace2 = nonAlphaNumeric2 && char2.isSpace(); + bool lineBreak1 = whitespace1 && char1.category() == QChar::Other_Control; + bool lineBreak2 = whitespace2 && char2.category() == QChar::Other_Control; + bool blankLine1 = lineBreak1 && BLANKLINEEND.indexIn(one) != -1; + bool blankLine2 = lineBreak2 && BLANKLINESTART.indexIn(two) != -1; + + if (blankLine1 || blankLine2) { + // Five points for blank lines. + return 5; + } else if (lineBreak1 || lineBreak2) { + // Four points for line breaks. + return 4; + } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { + // Three points for end of sentences. + return 3; + } else if (whitespace1 || whitespace2) { + // Two points for whitespace. + return 2; + } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { + // One point for non-alphanumeric. + return 1; + } + return 0; +} + + +// Define some regex patterns for matching boundaries. +QRegExp diff_match_patch::BLANKLINEEND = QRegExp("\\n\\r?\\n$"); +QRegExp diff_match_patch::BLANKLINESTART = QRegExp("^\\r?\\n\\r?\\n"); + + +void diff_match_patch::diff_cleanupEfficiency(QList &diffs) { + if (diffs.isEmpty()) { + return; + } + bool changes = false; + QStack equalities; // Stack of equalities. + QString lastequality; // Always equal to equalities.lastElement().text + QMutableListIterator pointer(diffs); + // Is there an insertion operation before the last equality. + bool pre_ins = false; + // Is there a deletion operation before the last equality. + bool pre_del = false; + // Is there an insertion operation after the last equality. + bool post_ins = false; + // Is there a deletion operation after the last equality. + bool post_del = false; + + Diff *thisDiff = pointer.hasNext() ? &pointer.next() : NULL; + Diff *safeDiff = thisDiff; + + while (thisDiff != NULL) { + if (thisDiff->operation == EQUAL) { + // Equality found. + if (thisDiff->text.length() < Diff_EditCost && (post_ins || post_del)) { + // Candidate found. + equalities.push(*thisDiff); + pre_ins = post_ins; + pre_del = post_del; + lastequality = thisDiff->text; + } else { + // Not a candidate, and can never become one. + equalities.clear(); + lastequality = QString(); + safeDiff = thisDiff; + } + post_ins = post_del = false; + } else { + // An insertion or deletion. + if (thisDiff->operation == DELETE) { + post_del = true; + } else { + post_ins = true; + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if (!lastequality.isNull() + && ((pre_ins && pre_del && post_ins && post_del) + || ((lastequality.length() < Diff_EditCost / 2) + && ((pre_ins ? 1 : 0) + (pre_del ? 1 : 0) + + (post_ins ? 1 : 0) + (post_del ? 1 : 0)) == 3))) { + // printf("Splitting: '%s'\n", qPrintable(lastequality)); + // Walk back to offending equality. + while (*thisDiff != equalities.top()) { + thisDiff = &pointer.previous(); + } + pointer.next(); + + // Replace equality with a delete. + pointer.setValue(Diff(DELETE, lastequality)); + // Insert a corresponding an insert. + pointer.insert(Diff(INSERT, lastequality)); + thisDiff = &pointer.previous(); + pointer.next(); + + equalities.pop(); // Throw away the equality we just deleted. + lastequality = QString(); + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_ins = post_del = true; + equalities.clear(); + safeDiff = thisDiff; + } else { + if (!equalities.isEmpty()) { + // Throw away the previous equality (it needs to be reevaluated). + equalities.pop(); + } + if (equalities.isEmpty()) { + // There are no previous questionable equalities, + // walk back to the last known safe diff. + thisDiff = safeDiff; + } else { + // There is an equality we can fall back to. + thisDiff = &equalities.top(); + } + while (*thisDiff != pointer.previous()) { + // Intentionally empty loop. + } + post_ins = post_del = false; + } + + changes = true; + } + } + thisDiff = pointer.hasNext() ? &pointer.next() : NULL; + } + + if (changes) { + diff_cleanupMerge(diffs); + } +} + + +void diff_match_patch::diff_cleanupMerge(QList &diffs) { + diffs.append(Diff(EQUAL, "")); // Add a dummy entry at the end. + QMutableListIterator pointer(diffs); + int count_delete = 0; + int count_insert = 0; + QString text_delete = ""; + QString text_insert = ""; + Diff *thisDiff = pointer.hasNext() ? &pointer.next() : NULL; + Diff *prevEqual = NULL; + int commonlength; + while (thisDiff != NULL) { + switch (thisDiff->operation) { + case INSERT: + count_insert++; + text_insert += thisDiff->text; + prevEqual = NULL; + break; + case DELETE: + count_delete++; + text_delete += thisDiff->text; + prevEqual = NULL; + break; + case EQUAL: + if (count_delete + count_insert > 1) { + bool both_types = count_delete != 0 && count_insert != 0; + // Delete the offending records. + pointer.previous(); // Reverse direction. + while (count_delete-- > 0) { + pointer.previous(); + pointer.remove(); + } + while (count_insert-- > 0) { + pointer.previous(); + pointer.remove(); + } + if (both_types) { + // Factor out any common prefixies. + commonlength = diff_commonPrefix(text_insert, text_delete); + if (commonlength != 0) { + if (pointer.hasPrevious()) { + thisDiff = &pointer.previous(); + if (thisDiff->operation != EQUAL) { + throw "Previous diff should have been an equality."; + } + thisDiff->text += text_insert.left(commonlength); + pointer.next(); + } else { + pointer.insert(Diff(EQUAL, text_insert.left(commonlength))); + } + text_insert = safeMid(text_insert, commonlength); + text_delete = safeMid(text_delete, commonlength); + } + // Factor out any common suffixies. + commonlength = diff_commonSuffix(text_insert, text_delete); + if (commonlength != 0) { + thisDiff = &pointer.next(); + thisDiff->text = safeMid(text_insert, text_insert.length() + - commonlength) + thisDiff->text; + text_insert = text_insert.left(text_insert.length() + - commonlength); + text_delete = text_delete.left(text_delete.length() + - commonlength); + pointer.previous(); + } + } + // Insert the merged records. + if (!text_delete.isEmpty()) { + pointer.insert(Diff(DELETE, text_delete)); + } + if (!text_insert.isEmpty()) { + pointer.insert(Diff(INSERT, text_insert)); + } + // Step forward to the equality. + thisDiff = pointer.hasNext() ? &pointer.next() : NULL; + + } else if (prevEqual != NULL) { + // Merge this equality with the previous one. + prevEqual->text += thisDiff->text; + pointer.remove(); + thisDiff = &pointer.previous(); + pointer.next(); // Forward direction + } + count_insert = 0; + count_delete = 0; + text_delete = ""; + text_insert = ""; + prevEqual = thisDiff; + break; + } + thisDiff = pointer.hasNext() ? &pointer.next() : NULL; + } + if (diffs.back().text.isEmpty()) { + diffs.removeLast(); // Remove the dummy entry at the end. + } + + /* + * Second pass: look for single edits surrounded on both sides by equalities + * which can be shifted sideways to eliminate an equality. + * e.g: ABAC -> ABAC + */ + bool changes = false; + // Create a new iterator at the start. + // (As opposed to walking the current one back.) + pointer.toFront(); + Diff *prevDiff = pointer.hasNext() ? &pointer.next() : NULL; + thisDiff = pointer.hasNext() ? &pointer.next() : NULL; + Diff *nextDiff = pointer.hasNext() ? &pointer.next() : NULL; + + // Intentionally ignore the first and last element (don't need checking). + while (nextDiff != NULL) { + if (prevDiff->operation == EQUAL && + nextDiff->operation == EQUAL) { + // This is a single edit surrounded by equalities. + if (thisDiff->text.endsWith(prevDiff->text)) { + // Shift the edit over the previous equality. + thisDiff->text = prevDiff->text + + thisDiff->text.left(thisDiff->text.length() + - prevDiff->text.length()); + nextDiff->text = prevDiff->text + nextDiff->text; + pointer.previous(); // Walk past nextDiff. + pointer.previous(); // Walk past thisDiff. + pointer.previous(); // Walk past prevDiff. + pointer.remove(); // Delete prevDiff. + pointer.next(); // Walk past thisDiff. + thisDiff = &pointer.next(); // Walk past nextDiff. + nextDiff = pointer.hasNext() ? &pointer.next() : NULL; + changes = true; + } else if (thisDiff->text.startsWith(nextDiff->text)) { + // Shift the edit over the next equality. + prevDiff->text += nextDiff->text; + thisDiff->text = safeMid(thisDiff->text, nextDiff->text.length()) + + nextDiff->text; + pointer.remove(); // Delete nextDiff. + nextDiff = pointer.hasNext() ? &pointer.next() : NULL; + changes = true; + } + } + prevDiff = thisDiff; + thisDiff = nextDiff; + nextDiff = pointer.hasNext() ? &pointer.next() : NULL; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + diff_cleanupMerge(diffs); + } +} + + +int diff_match_patch::diff_xIndex(const QList &diffs, int loc) { + int chars1 = 0; + int chars2 = 0; + int last_chars1 = 0; + int last_chars2 = 0; + Diff lastDiff; + foreach(Diff aDiff, diffs) { + if (aDiff.operation != INSERT) { + // Equality or deletion. + chars1 += aDiff.text.length(); + } + if (aDiff.operation != DELETE) { + // Equality or insertion. + chars2 += aDiff.text.length(); + } + if (chars1 > loc) { + // Overshot the location. + lastDiff = aDiff; + break; + } + last_chars1 = chars1; + last_chars2 = chars2; + } + if (lastDiff.operation == DELETE) { + // The location was deleted. + return last_chars2; + } + // Add the remaining character length. + return last_chars2 + (loc - last_chars1); +} + + +QString diff_match_patch::diff_prettyHtml(const QList &diffs) { + QString html; + QString text; + foreach(Diff aDiff, diffs) { + text = aDiff.text; + text.replace("&", "&").replace("<", "<") + .replace(">", ">").replace("\n", "¶
"); + switch (aDiff.operation) { + case INSERT: + html += QString("") + text + + QString(""); + break; + case DELETE: + html += QString("") + text + + QString(""); + break; + case EQUAL: + html += QString("") + text + QString(""); + break; + } + } + return html; +} + + +QString diff_match_patch::diff_text1(const QList &diffs) { + QString text; + foreach(Diff aDiff, diffs) { + if (aDiff.operation != INSERT) { + text += aDiff.text; + } + } + return text; +} + + +QString diff_match_patch::diff_text2(const QList &diffs) { + QString text; + foreach(Diff aDiff, diffs) { + if (aDiff.operation != DELETE) { + text += aDiff.text; + } + } + return text; +} + + +int diff_match_patch::diff_levenshtein(const QList &diffs) { + int levenshtein = 0; + int insertions = 0; + int deletions = 0; + foreach(Diff aDiff, diffs) { + switch (aDiff.operation) { + case INSERT: + insertions += aDiff.text.length(); + break; + case DELETE: + deletions += aDiff.text.length(); + break; + case EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += std::max(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += std::max(insertions, deletions); + return levenshtein; +} + + +QString diff_match_patch::diff_toDelta(const QList &diffs) { + QString text; + foreach(Diff aDiff, diffs) { + switch (aDiff.operation) { + case INSERT: { + QString encoded = QString(QUrl::toPercentEncoding(aDiff.text, + " !~*'();/?:@&=+$,#")); + text += QString("+") + encoded + QString("\t"); + break; + } + case DELETE: + text += QString("-") + QString::number(aDiff.text.length()) + + QString("\t"); + break; + case EQUAL: + text += QString("=") + QString::number(aDiff.text.length()) + + QString("\t"); + break; + } + } + if (!text.isEmpty()) { + // Strip off trailing tab character. + text = text.left(text.length() - 1); + } + return text; +} + + +QList diff_match_patch::diff_fromDelta(const QString &text1, + const QString &delta) { + QList diffs; + int pointer = 0; // Cursor in text1 + QStringList tokens = delta.split("\t"); + foreach(QString token, tokens) { + if (token.isEmpty()) { + // Blank tokens are ok (from a trailing \t). + continue; + } + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + QString param = safeMid(token, 1); + switch (token[0].toAscii()) { + case '+': + param = QUrl::fromPercentEncoding(qPrintable(param)); + diffs.append(Diff(INSERT, param)); + break; + case '-': + // Fall through. + case '=': { + int n; + n = param.toInt(); + if (n < 0) { + throw QString("Negative number in diff_fromDelta: %1").arg(param); + } + QString text; + text = safeMid(text1, pointer, n); + pointer += n; + if (token[0] == QChar('=')) { + diffs.append(Diff(EQUAL, text)); + } else { + diffs.append(Diff(DELETE, text)); + } + break; + } + default: + throw QString("Invalid diff operation in diff_fromDelta: %1") + .arg(token[0]); + } + } + if (pointer != text1.length()) { + throw QString("Delta length (%1) smaller than source text length (%2)") + .arg(pointer).arg(text1.length()); + } + return diffs; +} + + + // MATCH FUNCTIONS + + +int diff_match_patch::match_main(const QString &text, const QString &pattern, + int loc) { + // Check for null inputs. + if (text.isNull() || pattern.isNull()) { + throw "Null inputs. (match_main)"; + } + + loc = std::max(0, std::min(loc, text.length())); + if (text == pattern) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (text.isEmpty()) { + // Nothing to match. + return -1; + } else if (loc + pattern.length() <= text.length() + && safeMid(text, loc, pattern.length()) == pattern) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return loc; + } else { + // Do a fuzzy compare. + return match_bitap(text, pattern, loc); + } +} + + +int diff_match_patch::match_bitap(const QString &text, const QString &pattern, + int loc) { + if (!(Match_MaxBits == 0 || pattern.length() <= Match_MaxBits)) { + throw "Pattern too long for this application."; + } + + // Initialise the alphabet. + QMap s = match_alphabet(pattern); + + // Highest score beyond which we give up. + double score_threshold = Match_Threshold; + // Is there a nearby exact match? (speedup) + int best_loc = text.indexOf(pattern, loc); + if (best_loc != -1) { + score_threshold = std::min(match_bitapScore(0, best_loc, loc, pattern), + score_threshold); + // What about in the other direction? (speedup) + best_loc = text.lastIndexOf(pattern, loc + pattern.length()); + if (best_loc != -1) { + score_threshold = std::min(match_bitapScore(0, best_loc, loc, pattern), + score_threshold); + } + } + + // Initialise the bit arrays. + int matchmask = 1 << (pattern.length() - 1); + best_loc = -1; + + int bin_min, bin_mid; + int bin_max = pattern.length() + text.length(); + int *rd; + int *last_rd = NULL; + for (int d = 0; d < pattern.length(); d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at + // this error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (match_bitapScore(d, loc + bin_mid, loc, pattern) + <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = (bin_max - bin_min) / 2 + bin_min; + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + int start = std::max(1, loc - bin_mid + 1); + int finish = std::min(loc + bin_mid, text.length()) + pattern.length(); + + rd = new int[finish + 2]; + rd[finish + 1] = (1 << d) - 1; + for (int j = finish; j >= start; j--) { + int charMatch; + if (text.length() <= j - 1) { + // Out of range. + charMatch = 0; + } else { + charMatch = s.value(text[j - 1], 0); + } + if (d == 0) { + // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { + // Subsequent passes: fuzzy match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch + | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) + | last_rd[j + 1]; + } + if ((rd[j] & matchmask) != 0) { + double score = match_bitapScore(d, j - 1, loc, pattern); + // This match will almost certainly be better than any existing + // match. But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = std::max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + if (match_bitapScore(d + 1, loc, loc, pattern) > score_threshold) { + // No hope for a (better) match at greater error levels. + break; + } + delete [] last_rd; + last_rd = rd; + } + delete [] last_rd; + delete [] rd; + return best_loc; +} + + +double diff_match_patch::match_bitapScore(int e, int x, int loc, + const QString &pattern) { + const float accuracy = static_cast (e) / pattern.length(); + const int proximity = qAbs(loc - x); + if (Match_Distance == 0) { + // Dodge divide by zero error. + return proximity == 0 ? accuracy : 1.0; + } + return accuracy + (proximity / static_cast (Match_Distance)); +} + + +QMap diff_match_patch::match_alphabet(const QString &pattern) { + QMap s; + int i; + for (i = 0; i < pattern.length(); i++) { + QChar c = pattern[i]; + s.insert(c, 0); + } + for (i = 0; i < pattern.length(); i++) { + QChar c = pattern[i]; + s.insert(c, s.value(c) | (1 << (pattern.length() - i - 1))); + } + return s; +} + + +// PATCH FUNCTIONS + + +void diff_match_patch::patch_addContext(Patch &patch, const QString &text) { + if (text.isEmpty()) { + return; + } + QString pattern = safeMid(text, patch.start2, patch.length1); + int padding = 0; + + // Look for the first and last matches of pattern in text. If two different + // matches are found, increase the pattern length. + while (text.indexOf(pattern) != text.lastIndexOf(pattern) + && pattern.length() < Match_MaxBits - Patch_Margin - Patch_Margin) { + padding += Patch_Margin; + pattern = safeMid(text, std::max(0, patch.start2 - padding), + std::min(text.length(), patch.start2 + patch.length1 + padding) + - std::max(0, patch.start2 - padding)); + } + // Add one chunk for good luck. + padding += Patch_Margin; + + // Add the prefix. + QString prefix = safeMid(text, std::max(0, patch.start2 - padding), + patch.start2 - std::max(0, patch.start2 - padding)); + if (!prefix.isEmpty()) { + patch.diffs.prepend(Diff(EQUAL, prefix)); + } + // Add the suffix. + QString suffix = safeMid(text, patch.start2 + patch.length1, + std::min(text.length(), patch.start2 + patch.length1 + padding) + - (patch.start2 + patch.length1)); + if (!suffix.isEmpty()) { + patch.diffs.append(Diff(EQUAL, suffix)); + } + + // Roll back the start points. + patch.start1 -= prefix.length(); + patch.start2 -= prefix.length(); + // Extend the lengths. + patch.length1 += prefix.length() + suffix.length(); + patch.length2 += prefix.length() + suffix.length(); +} + + +QList diff_match_patch::patch_make(const QString &text1, + const QString &text2) { + // Check for null inputs. + if (text1.isNull() || text2.isNull()) { + throw "Null inputs. (patch_make)"; + } + + // No diffs provided, compute our own. + QList diffs = diff_main(text1, text2, true); + if (diffs.size() > 2) { + diff_cleanupSemantic(diffs); + diff_cleanupEfficiency(diffs); + } + + return patch_make(text1, diffs); +} + + +QList diff_match_patch::patch_make(const QList &diffs) { + // No origin string provided, compute our own. + const QString text1 = diff_text1(diffs); + return patch_make(text1, diffs); +} + + +QList diff_match_patch::patch_make(const QString &text1, + const QString &text2, + const QList &diffs) { + // text2 is entirely unused. + return patch_make(text1, diffs); + + Q_UNUSED(text2) +} + + +QList diff_match_patch::patch_make(const QString &text1, + const QList &diffs) { + // Check for null inputs. + if (text1.isNull()) { + throw "Null inputs. (patch_make)"; + } + + QList patches; + if (diffs.isEmpty()) { + return patches; // Get rid of the null case. + } + Patch patch; + int char_count1 = 0; // Number of characters into the text1 string. + int char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + QString prepatch_text = text1; + QString postpatch_text = text1; + foreach(Diff aDiff, diffs) { + if (patch.diffs.isEmpty() && aDiff.operation != EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (aDiff.operation) { + case INSERT: + patch.diffs.append(aDiff); + patch.length2 += aDiff.text.length(); + postpatch_text = postpatch_text.left(char_count2) + + aDiff.text + safeMid(postpatch_text, char_count2); + break; + case DELETE: + patch.length1 += aDiff.text.length(); + patch.diffs.append(aDiff); + postpatch_text = postpatch_text.left(char_count2) + + safeMid(postpatch_text, char_count2 + aDiff.text.length()); + break; + case EQUAL: + if (aDiff.text.length() <= 2 * Patch_Margin + && !patch.diffs.isEmpty() && !(aDiff == diffs.back())) { + // Small equality inside a patch. + patch.diffs.append(aDiff); + patch.length1 += aDiff.text.length(); + patch.length2 += aDiff.text.length(); + } + + if (aDiff.text.length() >= 2 * Patch_Margin) { + // Time for a new patch. + if (!patch.diffs.isEmpty()) { + patch_addContext(patch, prepatch_text); + patches.append(patch); + patch = Patch(); + // Unlike Unidiff, our patch lists have a rolling context. + // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (aDiff.operation != INSERT) { + char_count1 += aDiff.text.length(); + } + if (aDiff.operation != DELETE) { + char_count2 += aDiff.text.length(); + } + } + // Pick up the leftover patch if not empty. + if (!patch.diffs.isEmpty()) { + patch_addContext(patch, prepatch_text); + patches.append(patch); + } + + return patches; +} + + +QList diff_match_patch::patch_deepCopy(QList &patches) { + QList patchesCopy; + foreach(Patch aPatch, patches) { + Patch patchCopy = Patch(); + foreach(Diff aDiff, aPatch.diffs) { + Diff diffCopy = Diff(aDiff.operation, aDiff.text); + patchCopy.diffs.append(diffCopy); + } + patchCopy.start1 = aPatch.start1; + patchCopy.start2 = aPatch.start2; + patchCopy.length1 = aPatch.length1; + patchCopy.length2 = aPatch.length2; + patchesCopy.append(patchCopy); + } + return patchesCopy; +} + + +QPair > diff_match_patch::patch_apply( + QList &patches, const QString &sourceText) { + QString text = sourceText; // Copy to preserve original. + if (patches.isEmpty()) { + return QPair >(text, QVector(0)); + } + + // Deep copy the patches so that no changes are made to originals. + QList patchesCopy = patch_deepCopy(patches); + + QString nullPadding = patch_addPadding(patchesCopy); + text = nullPadding + text + nullPadding; + patch_splitMax(patchesCopy); + + int x = 0; + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + int delta = 0; + QVector results(patchesCopy.size()); + foreach(Patch aPatch, patchesCopy) { + int expected_loc = aPatch.start2 + delta; + QString text1 = diff_text1(aPatch.diffs); + int start_loc; + int end_loc = -1; + if (text1.length() > Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = match_main(text, text1.left(Match_MaxBits), expected_loc); + if (start_loc != -1) { + end_loc = match_main(text, text1.right(Match_MaxBits), + expected_loc + text1.length() - Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= aPatch.length2 - aPatch.length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + QString text2; + if (end_loc == -1) { + text2 = safeMid(text, start_loc, text1.length()); + } else { + text2 = safeMid(text, start_loc, end_loc + Match_MaxBits - start_loc); + } + if (text1 == text2) { + // Perfect match, just shove the replacement text in. + text = text.left(start_loc) + diff_text2(aPatch.diffs) + + safeMid(text, start_loc + text1.length()); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + QList diffs = diff_main(text1, text2, false); + if (text1.length() > Match_MaxBits + && diff_levenshtein(diffs) / static_cast (text1.length()) + > Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + diff_cleanupSemanticLossless(diffs); + int index1 = 0; + foreach(Diff aDiff, aPatch.diffs) { + if (aDiff.operation != EQUAL) { + int index2 = diff_xIndex(diffs, index1); + if (aDiff.operation == INSERT) { + // Insertion + text = text.left(start_loc + index2) + aDiff.text + + safeMid(text, start_loc + index2); + } else if (aDiff.operation == DELETE) { + // Deletion + text = text.left(start_loc + index2) + + safeMid(text, start_loc + diff_xIndex(diffs, + index1 + aDiff.text.length())); + } + } + if (aDiff.operation != DELETE) { + index1 += aDiff.text.length(); + } + } + } + } + } + x++; + } + // Strip the padding off. + text = safeMid(text, nullPadding.length(), text.length() + - 2 * nullPadding.length()); + return QPair >(text, results); +} + + +QString diff_match_patch::patch_addPadding(QList &patches) { + short paddingLength = Patch_Margin; + QString nullPadding = ""; + for (short x = 1; x <= paddingLength; x++) { + nullPadding += QChar((ushort)x); + } + + // Bump all the patches forward. + QMutableListIterator pointer(patches); + while (pointer.hasNext()) { + Patch &aPatch = pointer.next(); + aPatch.start1 += paddingLength; + aPatch.start2 += paddingLength; + } + + // Add some padding on start of first diff. + Patch &firstPatch = patches.first(); + QList &firstPatchDiffs = firstPatch.diffs; + if (firstPatchDiffs.empty() || firstPatchDiffs.first().operation != EQUAL) { + // Add nullPadding equality. + firstPatchDiffs.prepend(Diff(EQUAL, nullPadding)); + firstPatch.start1 -= paddingLength; // Should be 0. + firstPatch.start2 -= paddingLength; // Should be 0. + firstPatch.length1 += paddingLength; + firstPatch.length2 += paddingLength; + } else if (paddingLength > firstPatchDiffs.first().text.length()) { + // Grow first equality. + Diff &firstDiff = firstPatchDiffs.first(); + int extraLength = paddingLength - firstDiff.text.length(); + firstDiff.text = safeMid(nullPadding, firstDiff.text.length(), + paddingLength - firstDiff.text.length()) + firstDiff.text; + firstPatch.start1 -= extraLength; + firstPatch.start2 -= extraLength; + firstPatch.length1 += extraLength; + firstPatch.length2 += extraLength; + } + + // Add some padding on end of last diff. + Patch &lastPatch = patches.first(); + QList &lastPatchDiffs = lastPatch.diffs; + if (lastPatchDiffs.empty() || lastPatchDiffs.last().operation != EQUAL) { + // Add nullPadding equality. + lastPatchDiffs.append(Diff(EQUAL, nullPadding)); + lastPatch.length1 += paddingLength; + lastPatch.length2 += paddingLength; + } else if (paddingLength > lastPatchDiffs.last().text.length()) { + // Grow last equality. + Diff &lastDiff = lastPatchDiffs.last(); + int extraLength = paddingLength - lastDiff.text.length(); + lastDiff.text += nullPadding.left(extraLength); + lastPatch.length1 += extraLength; + lastPatch.length2 += extraLength; + } + + return nullPadding; +} + + +void diff_match_patch::patch_splitMax(QList &patches) { + short patch_size = Match_MaxBits; + QString precontext, postcontext; + Patch patch; + int start1, start2; + bool empty; + Operation diff_type; + QString diff_text; + QMutableListIterator pointer(patches); + Patch bigpatch; + + if (pointer.hasNext()) { + bigpatch = pointer.next(); + } + + while (!bigpatch.isNull()) { + if (bigpatch.length1 <= patch_size) { + bigpatch = pointer.hasNext() ? pointer.next() : Patch(); + continue; + } + // Remove the big old patch. + pointer.remove(); + start1 = bigpatch.start1; + start2 = bigpatch.start2; + precontext = ""; + while (!bigpatch.diffs.isEmpty()) { + // Create one of several smaller patches. + patch = Patch(); + empty = true; + patch.start1 = start1 - precontext.length(); + patch.start2 = start2 - precontext.length(); + if (!precontext.isEmpty()) { + patch.length1 = patch.length2 = precontext.length(); + patch.diffs.append(Diff(EQUAL, precontext)); + } + while (!bigpatch.diffs.isEmpty() + && patch.length1 < patch_size - Patch_Margin) { + diff_type = bigpatch.diffs.front().operation; + diff_text = bigpatch.diffs.front().text; + if (diff_type == INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.length(); + start2 += diff_text.length(); + patch.diffs.append(bigpatch.diffs.front()); + bigpatch.diffs.removeFirst(); + empty = false; + } else if (diff_type == DELETE && patch.diffs.size() == 1 + && patch.diffs.front().operation == EQUAL + && diff_text.length() > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.length(); + start1 += diff_text.length(); + empty = false; + patch.diffs.append(Diff(diff_type, diff_text)); + bigpatch.diffs.removeFirst(); + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text.left(std::min(diff_text.length(), + patch_size - patch.length1 - Patch_Margin)); + patch.length1 += diff_text.length(); + start1 += diff_text.length(); + if (diff_type == EQUAL) { + patch.length2 += diff_text.length(); + start2 += diff_text.length(); + } else { + empty = false; + } + patch.diffs.append(Diff(diff_type, diff_text)); + if (diff_text == bigpatch.diffs.front().text) { + bigpatch.diffs.removeFirst(); + } else { + bigpatch.diffs.front().text = safeMid(bigpatch.diffs.front().text, + diff_text.length()); + } + } + } + // Compute the head context for the next patch. + precontext = diff_text2(patch.diffs); + precontext = safeMid(precontext, precontext.length() - Patch_Margin); + // Append the end context for this patch. + if (diff_text1(bigpatch.diffs).length() > Patch_Margin) { + postcontext = diff_text1(bigpatch.diffs).left(Patch_Margin); + } else { + postcontext = diff_text1(bigpatch.diffs); + } + if (!postcontext.isEmpty()) { + patch.length1 += postcontext.length(); + patch.length2 += postcontext.length(); + if (!patch.diffs.isEmpty() + && patch.diffs.back().operation == EQUAL) { + patch.diffs.back().text += postcontext; + } else { + patch.diffs.append(Diff(EQUAL, postcontext)); + } + } + if (!empty) { + pointer.insert(patch); + } + } + bigpatch = pointer.hasNext() ? pointer.next() : Patch(); + } +} + + +QString diff_match_patch::patch_toText(const QList &patches) { + QString text; + foreach(Patch aPatch, patches) { + text.append(aPatch.toString()); + } + return text; +} + + +QList diff_match_patch::patch_fromText(const QString &textline) { + QList patches; + if (textline.isEmpty()) { + return patches; + } + QStringList text = textline.split("\n", QString::SkipEmptyParts); + Patch patch; + QRegExp patchHeader("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$"); + char sign; + QString line; + while (!text.isEmpty()) { + if (!patchHeader.exactMatch(text.front())) { + throw QString("Invalid patch string: %1").arg(text.front()); + } + + patch = Patch(); + patch.start1 = patchHeader.cap(1).toInt(); + if (patchHeader.cap(2).isEmpty()) { + patch.start1--; + patch.length1 = 1; + } else if (patchHeader.cap(2) == "0") { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = patchHeader.cap(2).toInt(); + } + + patch.start2 = patchHeader.cap(3).toInt(); + if (patchHeader.cap(4).isEmpty()) { + patch.start2--; + patch.length2 = 1; + } else if (patchHeader.cap(4) == "0") { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = patchHeader.cap(4).toInt(); + } + text.removeFirst(); + + while (!text.isEmpty()) { + if (text.front().isEmpty()) { + text.removeFirst(); + continue; + } + sign = text.front()[0].toAscii(); + line = safeMid(text.front(), 1); + line = line.replace("+", "%2B"); // decode would change all "+" to " " + line = QUrl::fromPercentEncoding(qPrintable(line)); + if (sign == '-') { + // Deletion. + patch.diffs.append(Diff(DELETE, line)); + } else if (sign == '+') { + // Insertion. + patch.diffs.append(Diff(INSERT, line)); + } else if (sign == ' ') { + // Minor equality. + patch.diffs.append(Diff(EQUAL, line)); + } else if (sign == '@') { + // Start of next patch. + break; + } else { + // WTF? + throw QString("Invalid patch mode '%1' in: %2").arg(sign).arg(line); + return QList(); + } + text.removeFirst(); + } + + patches.append(patch); + + } + return patches; +} diff --git a/cpp/diff_match_patch.h b/cpp/diff_match_patch.h new file mode 100644 index 0000000..82d3283 --- /dev/null +++ b/cpp/diff_match_patch.h @@ -0,0 +1,625 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef DIFF_MATCH_PATCH_H +#define DIFF_MATCH_PATCH_H + +/* + * Functions for diff, match and patch. + * Computes the difference between two texts to create a patch. + * Applies the patch onto another text, allowing for errors. + * + * @author fraser@google.com (Neil Fraser) + * + * Qt/C++ port by mikeslemmer@gmail.com (Mike Slemmer): + * + * Code known to compile and run with Qt 4.3 through Qt 4.7. + * + * Here is a trivial sample program which works properly when linked with this + * library: + * + + #include + #include + #include + #include + #include + #include "diff_match_patch.h" + int main(int argc, char **argv) { + diff_match_patch dmp; + QString str1 = QString("First string in diff"); + QString str2 = QString("Second string in diff"); + + QString strPatch = dmp.patch_toText(dmp.patch_make(str1, str2)); + QPair > out + = dmp.patch_apply(dmp.patch_fromText(strPatch), str1); + QString strResult = out.first; + + // here, strResult will equal str2 above. + return 0; + } + + */ + + +/**- +* The data structure representing a diff is a Linked list of Diff objects: +* {Diff(Operation.DELETE, "Hello"), Diff(Operation.INSERT, "Goodbye"), +* Diff(Operation.EQUAL, " world.")} +* which means: delete "Hello", add "Goodbye" and keep " world." +*/ +enum Operation { + DELETE, INSERT, EQUAL +}; + + +/** +* Class representing one diff operation. +*/ +class Diff { + public: + Operation operation; + // One of: INSERT, DELETE or EQUAL. + QString text; + // The text associated with this diff operation. + + /** + * Constructor. Initializes the diff with the provided values. + * @param operation One of INSERT, DELETE or EQUAL. + * @param text The text being applied. + */ + Diff(Operation _operation, const QString &_text); + Diff(); + inline bool isNull() const; + QString toString() const; + bool operator==(const Diff &d) const; + bool operator!=(const Diff &d) const; + + static QString strOperation(Operation op); +}; + + +/** +* Class representing one patch operation. +*/ +class Patch { + public: + QList diffs; + int start1; + int start2; + int length1; + int length2; + + /** + * Constructor. Initializes with an empty list of diffs. + */ + Patch(); + bool isNull() const; + QString toString(); +}; + + +/** + * Class containing the diff, match and patch methods. + * Also contains the behaviour settings. + */ +class diff_match_patch { + + friend class diff_match_patch_test; + + public: + // Defaults. + // Set these on your diff_match_patch instance to override the defaults. + + // Number of seconds to map a diff before giving up (0 for infinity). + float Diff_Timeout; + // Cost of an empty edit operation in terms of edit characters. + short Diff_EditCost; + // At what point is no match declared (0.0 = perfection, 1.0 = very loose). + float Match_Threshold; + // How far to search for a match (0 = exact location, 1000+ = broad match). + // A match this many characters away from the expected location will add + // 1.0 to the score (0.0 is a perfect match). + int Match_Distance; + // When deleting a large block of text (over ~64 characters), how close does + // the contents have to match the expected contents. (0.0 = perfection, + // 1.0 = very loose). Note that Match_Threshold controls how closely the + // end points of a delete need to match. + float Patch_DeleteThreshold; + // Chunk size for context length. + short Patch_Margin; + + // The number of bits in an int. + short Match_MaxBits; + + private: + // Define some regex patterns for matching boundaries. + static QRegExp BLANKLINEEND; + static QRegExp BLANKLINESTART; + + + public: + + diff_match_patch(); + + // DIFF FUNCTIONS + + + /** + * Find the differences between two texts. + * Run a faster slightly less optimal diff. + * This method allows the 'checklines' of diff_main() to be optional. + * Most of the time checklines is wanted, so default to true. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @return Linked List of Diff objects. + */ + QList diff_main(const QString &text1, const QString &text2); + + /** + * Find the differences between two texts. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @return Linked List of Diff objects. + */ + QList diff_main(const QString &text1, const QString &text2, bool checklines); + + /** + * Find the differences between two texts. Simplifies the problem by + * stripping any common prefix or suffix off the texts before diffing. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. Used + * internally for recursive calls. Users should set DiffTimeout instead. + * @return Linked List of Diff objects. + */ + private: + QList diff_main(const QString &text1, const QString &text2, bool checklines, clock_t deadline); + + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. + * @return Linked List of Diff objects. + */ + private: + QList diff_compute(QString text1, QString text2, bool checklines, clock_t deadline); + + /** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time when the diff should be complete by. + * @return Linked List of Diff objects. + */ + private: + QList diff_lineMode(QString text1, QString text2, clock_t deadline); + + /** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @return Linked List of Diff objects. + */ + protected: + QList diff_bisect(const QString &text1, const QString &text2, clock_t deadline); + + /** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param x Index of split point in text1. + * @param y Index of split point in text2. + * @param deadline Time at which to bail if not yet complete. + * @return LinkedList of Diff objects. + */ + private: + QList diff_bisectSplit(const QString &text1, const QString &text2, int x, int y, clock_t deadline); + + /** + * Split two texts into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text1 First string. + * @param text2 Second string. + * @return Three element Object array, containing the encoded text1, the + * encoded text2 and the List of unique strings. The zeroth element + * of the List of unique strings is intentionally blank. + */ + protected: + QList diff_linesToChars(const QString &text1, const QString &text2); // return elems 0 and 1 are QString, elem 2 is QStringList + + /** + * Split a text into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text String to encode. + * @param lineArray List of unique strings. + * @param lineHash Map of strings to indices. + * @return Encoded string. + */ + private: + QString diff_linesToCharsMunge(const QString &text, QStringList &lineArray, + QMap &lineHash); + + /** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * @param diffs LinkedList of Diff objects. + * @param lineArray List of unique strings. + */ + private: + void diff_charsToLines(QList &diffs, const QStringList &lineArray); + + /** + * Determine the common prefix of two strings. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the start of each string. + */ + public: + int diff_commonPrefix(const QString &text1, const QString &text2); + + /** + * Determine the common suffix of two strings. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of each string. + */ + public: + int diff_commonSuffix(const QString &text1, const QString &text2); + + /** + * Determine if the suffix of one string is the prefix of another. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of the first + * string and the start of the second string. + */ + protected: + int diff_commonOverlap(const QString &text1, const QString &text2); + + /** + * Do the two texts share a substring which is at least half the length of + * the longer text? + * This speedup can produce non-minimal diffs. + * @param text1 First string. + * @param text2 Second string. + * @return Five element String array, containing the prefix of text1, the + * suffix of text1, the prefix of text2, the suffix of text2 and the + * common middle. Or null if there was no match. + */ + protected: + QStringList diff_halfMatch(const QString &text1, const QString &text2); + + /** + * Does a substring of shorttext exist within longtext such that the + * substring is at least half the length of longtext? + * @param longtext Longer string. + * @param shorttext Shorter string. + * @param i Start index of quarter length substring within longtext. + * @return Five element String array, containing the prefix of longtext, the + * suffix of longtext, the prefix of shorttext, the suffix of shorttext + * and the common middle. Or null if there was no match. + */ + private: + QStringList diff_halfMatchI(const QString &longtext, const QString &shorttext, int i); + + /** + * Reduce the number of edits by eliminating semantically trivial equalities. + * @param diffs LinkedList of Diff objects. + */ + public: + void diff_cleanupSemantic(QList &diffs); + + /** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The cat came. -> The cat came. + * @param diffs LinkedList of Diff objects. + */ + public: + void diff_cleanupSemanticLossless(QList &diffs); + + /** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 6 (best) to 0 (worst). + * @param one First string. + * @param two Second string. + * @return The score. + */ + private: + int diff_cleanupSemanticScore(const QString &one, const QString &two); + + /** + * Reduce the number of edits by eliminating operationally trivial equalities. + * @param diffs LinkedList of Diff objects. + */ + public: + void diff_cleanupEfficiency(QList &diffs); + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param diffs LinkedList of Diff objects. + */ + public: + void diff_cleanupMerge(QList &diffs); + + /** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. "The cat" vs "The big cat", 1->1, 5->8 + * @param diffs LinkedList of Diff objects. + * @param loc Location within text1. + * @return Location within text2. + */ + public: + int diff_xIndex(const QList &diffs, int loc); + + /** + * Convert a Diff list into a pretty HTML report. + * @param diffs LinkedList of Diff objects. + * @return HTML representation. + */ + public: + QString diff_prettyHtml(const QList &diffs); + + /** + * Compute and return the source text (all equalities and deletions). + * @param diffs LinkedList of Diff objects. + * @return Source text. + */ + public: + QString diff_text1(const QList &diffs); + + /** + * Compute and return the destination text (all equalities and insertions). + * @param diffs LinkedList of Diff objects. + * @return Destination text. + */ + public: + QString diff_text2(const QList &diffs); + + /** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * @param diffs LinkedList of Diff objects. + * @return Number of changes. + */ + public: + int diff_levenshtein(const QList &diffs); + + /** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx notation. + * @param diffs Array of diff tuples. + * @return Delta text. + */ + public: + QString diff_toDelta(const QList &diffs); + + /** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, compute the full diff. + * @param text1 Source string for the diff. + * @param delta Delta text. + * @return Array of diff tuples or null if invalid. + * @throws QString If invalid input. + */ + public: + QList diff_fromDelta(const QString &text1, const QString &delta); + + + // MATCH FUNCTIONS + + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + public: + int match_main(const QString &text, const QString &pattern, int loc); + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + protected: + int match_bitap(const QString &text, const QString &pattern, int loc); + + /** + * Compute and return the score for a match with e errors and x location. + * @param e Number of errors in match. + * @param x Location of match. + * @param loc Expected location of match. + * @param pattern Pattern being sought. + * @return Overall score for match (0.0 = good, 1.0 = bad). + */ + private: + double match_bitapScore(int e, int x, int loc, const QString &pattern); + + /** + * Initialise the alphabet for the Bitap algorithm. + * @param pattern The text to encode. + * @return Hash of character locations. + */ + protected: + QMap match_alphabet(const QString &pattern); + + + // PATCH FUNCTIONS + + + /** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * @param patch The patch to grow. + * @param text Source text. + */ + protected: + void patch_addContext(Patch &patch, const QString &text); + + /** + * Compute a list of patches to turn text1 into text2. + * A set of diffs will be computed. + * @param text1 Old text. + * @param text2 New text. + * @return LinkedList of Patch objects. + */ + public: + QList patch_make(const QString &text1, const QString &text2); + + /** + * Compute a list of patches to turn text1 into text2. + * text1 will be derived from the provided diffs. + * @param diffs Array of diff tuples for text1 to text2. + * @return LinkedList of Patch objects. + */ + public: + QList patch_make(const QList &diffs); + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is ignored, diffs are the delta between text1 and text2. + * @param text1 Old text. + * @param text2 Ignored. + * @param diffs Array of diff tuples for text1 to text2. + * @return LinkedList of Patch objects. + * @deprecated Prefer patch_make(const QString &text1, const QList &diffs). + */ + public: + QList patch_make(const QString &text1, const QString &text2, const QList &diffs); + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is not provided, diffs are the delta between text1 and text2. + * @param text1 Old text. + * @param diffs Array of diff tuples for text1 to text2. + * @return LinkedList of Patch objects. + */ + public: + QList patch_make(const QString &text1, const QList &diffs); + + /** + * Given an array of patches, return another array that is identical. + * @param patches Array of patch objects. + * @return Array of patch objects. + */ + public: + QList patch_deepCopy(QList &patches); + + /** + * Merge a set of patches onto the text. Return a patched text, as well + * as an array of true/false values indicating which patches were applied. + * @param patches Array of patch objects. + * @param text Old text. + * @return Two element Object array, containing the new text and an array of + * boolean values. + */ + public: + QPair > patch_apply(QList &patches, const QString &text); + + /** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * @param patches Array of patch objects. + * @return The padding string added to each side. + */ + public: + QString patch_addPadding(QList &patches); + + /** + * Look through the patches and break up any which are longer than the + * maximum limit of the match algorithm. + * Intended to be called only from within patch_apply. + * @param patches LinkedList of Patch objects. + */ + public: + void patch_splitMax(QList &patches); + + /** + * Take a list of patches and return a textual representation. + * @param patches List of Patch objects. + * @return Text representation of patches. + */ + public: + QString patch_toText(const QList &patches); + + /** + * Parse a textual representation of patches and return a List of Patch + * objects. + * @param textline Text representation of patches. + * @return List of Patch objects. + * @throws QString If invalid input. + */ + public: + QList patch_fromText(const QString &textline); + + /** + * A safer version of QString.mid(pos). This one returns "" instead of + * null when the postion equals the string length. + * @param str String to take a substring from. + * @param pos Position to start the substring from. + * @return Substring. + */ + private: + static inline QString safeMid(const QString &str, int pos) { + return (pos == str.length()) ? QString("") : str.mid(pos); + } + + /** + * A safer version of QString.mid(pos, len). This one returns "" instead of + * null when the postion equals the string length. + * @param str String to take a substring from. + * @param pos Position to start the substring from. + * @param len Length of substring. + * @return Substring. + */ + private: + static inline QString safeMid(const QString &str, int pos, int len) { + return (pos == str.length()) ? QString("") : str.mid(pos, len); + } +}; + +#endif // DIFF_MATCH_PATCH_H diff --git a/cpp/diff_match_patch.pro b/cpp/diff_match_patch.pro new file mode 100644 index 0000000..8052797 --- /dev/null +++ b/cpp/diff_match_patch.pro @@ -0,0 +1,19 @@ +#QT += sql xml network +TEMPLATE = app +CONFIG += qt debug_and_release + +mac { + CONFIG -= app_bundle +} + +# don't embed the manifest for now (doesn't work :( ) +#CONFIG -= embed_manifest_exe + +FORMS = + +HEADERS = diff_match_patch.h diff_match_patch_test.h + +SOURCES = diff_match_patch.cpp diff_match_patch_test.cpp + +RESOURCES = + diff --git a/cpp/diff_match_patch_test.cpp b/cpp/diff_match_patch_test.cpp new file mode 100644 index 0000000..f75b1cd --- /dev/null +++ b/cpp/diff_match_patch_test.cpp @@ -0,0 +1,1197 @@ +/* + * Diff Match and Patch -- Test Harness + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code known to compile and run with Qt 4.3 through Qt 4.7. +#include +#include "diff_match_patch.h" +#include "diff_match_patch_test.h" + +int main(int argc, char **argv) { + diff_match_patch_test dmp_test; + qDebug("Starting diff_match_patch unit tests."); + dmp_test.run_all_tests(); + qDebug("Done."); + return 0; + Q_UNUSED(argc) + Q_UNUSED(argv) +} + + +diff_match_patch_test::diff_match_patch_test() { +} + +void diff_match_patch_test::run_all_tests() { + QTime t; + t.start(); + try { + testDiffCommonPrefix(); + testDiffCommonSuffix(); + testDiffCommonOverlap(); + testDiffHalfmatch(); + testDiffLinesToChars(); + testDiffCharsToLines(); + testDiffCleanupMerge(); + testDiffCleanupSemanticLossless(); + testDiffCleanupSemantic(); + testDiffCleanupEfficiency(); + testDiffPrettyHtml(); + testDiffText(); + testDiffDelta(); + testDiffXIndex(); + testDiffLevenshtein(); + testDiffBisect(); + testDiffMain(); + + testMatchAlphabet(); + testMatchBitap(); + testMatchMain(); + + testPatchObj(); + testPatchFromText(); + testPatchToText(); + testPatchAddContext(); + testPatchMake(); + testPatchSplitMax(); + testPatchAddPadding(); + testPatchApply(); + qDebug("All tests passed."); + } catch (QString strCase) { + qDebug("Test failed: %s", qPrintable(strCase)); + } + qDebug("Total time: %d ms", t.elapsed()); +} + +// DIFF TEST FUNCTIONS + +void diff_match_patch_test::testDiffCommonPrefix() { + // Detect any common prefix. + assertEquals("diff_commonPrefix: Null case.", 0, dmp.diff_commonPrefix("abc", "xyz")); + + assertEquals("diff_commonPrefix: Non-null case.", 4, dmp.diff_commonPrefix("1234abcdef", "1234xyz")); + + assertEquals("diff_commonPrefix: Whole case.", 4, dmp.diff_commonPrefix("1234", "1234xyz")); +} + +void diff_match_patch_test::testDiffCommonSuffix() { + // Detect any common suffix. + assertEquals("diff_commonSuffix: Null case.", 0, dmp.diff_commonSuffix("abc", "xyz")); + + assertEquals("diff_commonSuffix: Non-null case.", 4, dmp.diff_commonSuffix("abcdef1234", "xyz1234")); + + assertEquals("diff_commonSuffix: Whole case.", 4, dmp.diff_commonSuffix("1234", "xyz1234")); +} + +void diff_match_patch_test::testDiffCommonOverlap() { + // Detect any suffix/prefix overlap. + assertEquals("diff_commonOverlap: Null case.", 0, dmp.diff_commonOverlap("", "abcd")); + + assertEquals("diff_commonOverlap: Whole case.", 3, dmp.diff_commonOverlap("abc", "abcd")); + + assertEquals("diff_commonOverlap: No overlap.", 0, dmp.diff_commonOverlap("123456", "abcd")); + + assertEquals("diff_commonOverlap: Overlap.", 3, dmp.diff_commonOverlap("123456xxx", "xxxabcd")); + + // Some overly clever languages (C#) may treat ligatures as equal to their + // component letters. E.g. U+FB01 == 'fi' + assertEquals("diff_commonOverlap: Unicode.", 0, dmp.diff_commonOverlap("fi", QString::fromWCharArray((const wchar_t*) L"\ufb01i", 2))); +} + +void diff_match_patch_test::testDiffHalfmatch() { + // Detect a halfmatch. + dmp.Diff_Timeout = 1; + assertEmpty("diff_halfMatch: No match #1.", dmp.diff_halfMatch("1234567890", "abcdef")); + + assertEmpty("diff_halfMatch: No match #2.", dmp.diff_halfMatch("12345", "23")); + + assertEquals("diff_halfMatch: Single Match #1.", QString("12,90,a,z,345678").split(","), dmp.diff_halfMatch("1234567890", "a345678z")); + + assertEquals("diff_halfMatch: Single Match #2.", QString("a,z,12,90,345678").split(","), dmp.diff_halfMatch("a345678z", "1234567890")); + + assertEquals("diff_halfMatch: Single Match #3.", QString("abc,z,1234,0,56789").split(","), dmp.diff_halfMatch("abc56789z", "1234567890")); + + assertEquals("diff_halfMatch: Single Match #4.", QString("a,xyz,1,7890,23456").split(","), dmp.diff_halfMatch("a23456xyz", "1234567890")); + + assertEquals("diff_halfMatch: Multiple Matches #1.", QString("12123,123121,a,z,1234123451234").split(","), dmp.diff_halfMatch("121231234123451234123121", "a1234123451234z")); + + assertEquals("diff_halfMatch: Multiple Matches #2.", QString(",-=-=-=-=-=,x,,x-=-=-=-=-=-=-=").split(","), dmp.diff_halfMatch("x-=-=-=-=-=-=-=-=-=-=-=-=", "xx-=-=-=-=-=-=-=")); + + assertEquals("diff_halfMatch: Multiple Matches #3.", QString("-=-=-=-=-=,,,y,-=-=-=-=-=-=-=y").split(","), dmp.diff_halfMatch("-=-=-=-=-=-=-=-=-=-=-=-=y", "-=-=-=-=-=-=-=yy")); + + // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy + assertEquals("diff_halfMatch: Non-optimal halfmatch.", QString("qHillo,w,x,Hulloy,HelloHe").split(","), dmp.diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy")); + + dmp.Diff_Timeout = 0; + assertEmpty("diff_halfMatch: Optimal no halfmatch.", dmp.diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy")); +} + +void diff_match_patch_test::testDiffLinesToChars() { + // Convert lines down to characters. + QStringList tmpVector; + QList tmpVarList; + tmpVector.append(""); + tmpVector.append("alpha\n"); + tmpVector.append("beta\n"); + tmpVarList << QVariant::fromValue(QString() + QChar((ushort)1) + QChar((ushort)2) + QChar((ushort)1)); //(("\u0001\u0002\u0001")); + tmpVarList << QVariant::fromValue(QString() + QChar((ushort)2) + QChar((ushort)1) + QChar((ushort)2)); // (("\u0002\u0001\u0002")); + tmpVarList << QVariant::fromValue(tmpVector); + assertEquals("diff_linesToChars:", tmpVarList, dmp.diff_linesToChars("alpha\nbeta\nalpha\n", "beta\nalpha\nbeta\n")); + + tmpVector.clear(); + tmpVarList.clear(); + tmpVector.append(""); + tmpVector.append("alpha\r\n"); + tmpVector.append("beta\r\n"); + tmpVector.append("\r\n"); + tmpVarList << QVariant::fromValue(QString("")); + tmpVarList << QVariant::fromValue(QString() + QChar((ushort)1) + QChar((ushort)2) + QChar((ushort)3) + QChar((ushort)3)); // (("\u0001\u0002\u0003\u0003")); + tmpVarList << QVariant::fromValue(tmpVector); + assertEquals("diff_linesToChars:", tmpVarList, dmp.diff_linesToChars("", "alpha\r\nbeta\r\n\r\n\r\n")); + + tmpVector.clear(); + tmpVarList.clear(); + tmpVector.append(""); + tmpVector.append("a"); + tmpVector.append("b"); + tmpVarList << QVariant::fromValue(QString() + QChar((ushort)1)); // (("\u0001")); + tmpVarList << QVariant::fromValue(QString() + QChar((ushort)2)); // (("\u0002")); + tmpVarList << QVariant::fromValue(tmpVector); + assertEquals("diff_linesToChars:", tmpVarList, dmp.diff_linesToChars("a", "b")); + + // More than 256 to reveal any 8-bit limitations. + int n = 300; + tmpVector.clear(); + tmpVarList.clear(); + QString lines; + QString chars; + for (int x = 1; x < n + 1; x++) { + tmpVector.append(QString::number(x) + "\n"); + lines += QString::number(x) + "\n"; + chars += QChar(static_cast(x)); + } + assertEquals("diff_linesToChars: More than 256 (setup).", n, tmpVector.size()); + assertEquals("diff_linesToChars: More than 256 (setup).", n, chars.length()); + tmpVector.prepend(""); + tmpVarList << QVariant::fromValue(chars); + tmpVarList << QVariant::fromValue(QString("")); + tmpVarList << QVariant::fromValue(tmpVector); + assertEquals("diff_linesToChars: More than 256.", tmpVarList, dmp.diff_linesToChars(lines, "")); +} + +void diff_match_patch_test::testDiffCharsToLines() { + // First check that Diff equality works. + assertTrue("diff_charsToLines:", Diff(EQUAL, "a") == Diff(EQUAL, "a")); + + assertEquals("diff_charsToLines:", Diff(EQUAL, "a"), Diff(EQUAL, "a")); + + // Convert chars up to lines. + QList diffs; + diffs << Diff(EQUAL, QString() + QChar((ushort)1) + QChar((ushort)2) + QChar((ushort)1)); // ("\u0001\u0002\u0001"); + diffs << Diff(INSERT, QString() + QChar((ushort)2) + QChar((ushort)1) + QChar((ushort)2)); // ("\u0002\u0001\u0002"); + QStringList tmpVector; + tmpVector.append(""); + tmpVector.append("alpha\n"); + tmpVector.append("beta\n"); + dmp.diff_charsToLines(diffs, tmpVector); + assertEquals("diff_charsToLines:", diffList(Diff(EQUAL, "alpha\nbeta\nalpha\n"), Diff(INSERT, "beta\nalpha\nbeta\n")), diffs); + + // More than 256 to reveal any 8-bit limitations. + int n = 300; + tmpVector.clear(); + QList tmpVarList; + QString lines; + QString chars; + for (int x = 1; x < n + 1; x++) { + tmpVector.append(QString::number(x) + "\n"); + lines += QString::number(x) + "\n"; + chars += QChar(static_cast(x)); + } + assertEquals("diff_linesToChars: More than 256 (setup).", n, tmpVector.size()); + assertEquals("diff_linesToChars: More than 256 (setup).", n, chars.length()); + tmpVector.prepend(""); + diffs = diffList(Diff(DELETE, chars)); + dmp.diff_charsToLines(diffs, tmpVector); + assertEquals("diff_charsToLines: More than 256.", diffList(Diff(DELETE, lines)), diffs); +} + +void diff_match_patch_test::testDiffCleanupMerge() { + // Cleanup a messy diff. + QList diffs; + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Null case.", diffList(), diffs); + + diffs = diffList(Diff(EQUAL, "a"), Diff(DELETE, "b"), Diff(INSERT, "c")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: No change case.", diffList(Diff(EQUAL, "a"), Diff(DELETE, "b"), Diff(INSERT, "c")), diffs); + + diffs = diffList(Diff(EQUAL, "a"), Diff(EQUAL, "b"), Diff(EQUAL, "c")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Merge equalities.", diffList(Diff(EQUAL, "abc")), diffs); + + diffs = diffList(Diff(DELETE, "a"), Diff(DELETE, "b"), Diff(DELETE, "c")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Merge deletions.", diffList(Diff(DELETE, "abc")), diffs); + + diffs = diffList(Diff(INSERT, "a"), Diff(INSERT, "b"), Diff(INSERT, "c")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Merge insertions.", diffList(Diff(INSERT, "abc")), diffs); + + diffs = diffList(Diff(DELETE, "a"), Diff(INSERT, "b"), Diff(DELETE, "c"), Diff(INSERT, "d"), Diff(EQUAL, "e"), Diff(EQUAL, "f")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Merge interweave.", diffList(Diff(DELETE, "ac"), Diff(INSERT, "bd"), Diff(EQUAL, "ef")), diffs); + + diffs = diffList(Diff(DELETE, "a"), Diff(INSERT, "abc"), Diff(DELETE, "dc")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Prefix and suffix detection.", diffList(Diff(EQUAL, "a"), Diff(DELETE, "d"), Diff(INSERT, "b"), Diff(EQUAL, "c")), diffs); + + diffs = diffList(Diff(EQUAL, "x"), Diff(DELETE, "a"), Diff(INSERT, "abc"), Diff(DELETE, "dc"), Diff(EQUAL, "y")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Prefix and suffix detection with equalities.", diffList(Diff(EQUAL, "xa"), Diff(DELETE, "d"), Diff(INSERT, "b"), Diff(EQUAL, "cy")), diffs); + + diffs = diffList(Diff(EQUAL, "a"), Diff(INSERT, "ba"), Diff(EQUAL, "c")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Slide edit left.", diffList(Diff(INSERT, "ab"), Diff(EQUAL, "ac")), diffs); + + diffs = diffList(Diff(EQUAL, "c"), Diff(INSERT, "ab"), Diff(EQUAL, "a")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Slide edit right.", diffList(Diff(EQUAL, "ca"), Diff(INSERT, "ba")), diffs); + + diffs = diffList(Diff(EQUAL, "a"), Diff(DELETE, "b"), Diff(EQUAL, "c"), Diff(DELETE, "ac"), Diff(EQUAL, "x")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Slide edit left recursive.", diffList(Diff(DELETE, "abc"), Diff(EQUAL, "acx")), diffs); + + diffs = diffList(Diff(EQUAL, "x"), Diff(DELETE, "ca"), Diff(EQUAL, "c"), Diff(DELETE, "b"), Diff(EQUAL, "a")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Slide edit right recursive.", diffList(Diff(EQUAL, "xca"), Diff(DELETE, "cba")), diffs); +} + +void diff_match_patch_test::testDiffCleanupSemanticLossless() { + // Slide diffs to match logical boundaries. + QList diffs = diffList(); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemantic: Null case.", diffList(), diffs); + + diffs = diffList(Diff(EQUAL, "AAA\r\n\r\nBBB"), Diff(INSERT, "\r\nDDD\r\n\r\nBBB"), Diff(EQUAL, "\r\nEEE")); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemanticLossless: Blank lines.", diffList(Diff(EQUAL, "AAA\r\n\r\n"), Diff(INSERT, "BBB\r\nDDD\r\n\r\n"), Diff(EQUAL, "BBB\r\nEEE")), diffs); + + diffs = diffList(Diff(EQUAL, "AAA\r\nBBB"), Diff(INSERT, " DDD\r\nBBB"), Diff(EQUAL, " EEE")); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemanticLossless: Line boundaries.", diffList(Diff(EQUAL, "AAA\r\n"), Diff(INSERT, "BBB DDD\r\n"), Diff(EQUAL, "BBB EEE")), diffs); + + diffs = diffList(Diff(EQUAL, "The c"), Diff(INSERT, "ow and the c"), Diff(EQUAL, "at.")); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemantic: Word boundaries.", diffList(Diff(EQUAL, "The "), Diff(INSERT, "cow and the "), Diff(EQUAL, "cat.")), diffs); + + diffs = diffList(Diff(EQUAL, "The-c"), Diff(INSERT, "ow-and-the-c"), Diff(EQUAL, "at.")); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemantic: Alphanumeric boundaries.", diffList(Diff(EQUAL, "The-"), Diff(INSERT, "cow-and-the-"), Diff(EQUAL, "cat.")), diffs); + + diffs = diffList(Diff(EQUAL, "a"), Diff(DELETE, "a"), Diff(EQUAL, "ax")); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemantic: Hitting the start.", diffList(Diff(DELETE, "a"), Diff(EQUAL, "aax")), diffs); + + diffs = diffList(Diff(EQUAL, "xa"), Diff(DELETE, "a"), Diff(EQUAL, "a")); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemantic: Hitting the end.", diffList(Diff(EQUAL, "xaa"), Diff(DELETE, "a")), diffs); + + diffs = diffList(Diff(EQUAL, "The xxx. The "), Diff(INSERT, "zzz. The "), Diff(EQUAL, "yyy.")); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemantic: Sentence boundaries.", diffList(Diff(EQUAL, "The xxx."), Diff(INSERT, " The zzz."), Diff(EQUAL, " The yyy.")), diffs); +} + +void diff_match_patch_test::testDiffCleanupSemantic() { + // Cleanup semantically trivial equalities. + QList diffs = diffList(); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Null case.", diffList(), diffs); + + diffs = diffList(Diff(DELETE, "ab"), Diff(INSERT, "cd"), Diff(EQUAL, "12"), Diff(DELETE, "e")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: No elimination #1.", diffList(Diff(DELETE, "ab"), Diff(INSERT, "cd"), Diff(EQUAL, "12"), Diff(DELETE, "e")), diffs); + + diffs = diffList(Diff(DELETE, "abc"), Diff(INSERT, "ABC"), Diff(EQUAL, "1234"), Diff(DELETE, "wxyz")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: No elimination #2.", diffList(Diff(DELETE, "abc"), Diff(INSERT, "ABC"), Diff(EQUAL, "1234"), Diff(DELETE, "wxyz")), diffs); + + diffs = diffList(Diff(DELETE, "a"), Diff(EQUAL, "b"), Diff(DELETE, "c")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Simple elimination.", diffList(Diff(DELETE, "abc"), Diff(INSERT, "b")), diffs); + + diffs = diffList(Diff(DELETE, "ab"), Diff(EQUAL, "cd"), Diff(DELETE, "e"), Diff(EQUAL, "f"), Diff(INSERT, "g")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Backpass elimination.", diffList(Diff(DELETE, "abcdef"), Diff(INSERT, "cdfg")), diffs); + + diffs = diffList(Diff(INSERT, "1"), Diff(EQUAL, "A"), Diff(DELETE, "B"), Diff(INSERT, "2"), Diff(EQUAL, "_"), Diff(INSERT, "1"), Diff(EQUAL, "A"), Diff(DELETE, "B"), Diff(INSERT, "2")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Multiple elimination.", diffList(Diff(DELETE, "AB_AB"), Diff(INSERT, "1A2_1A2")), diffs); + + diffs = diffList(Diff(EQUAL, "The c"), Diff(DELETE, "ow and the c"), Diff(EQUAL, "at.")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Word boundaries.", diffList(Diff(EQUAL, "The "), Diff(DELETE, "cow and the "), Diff(EQUAL, "cat.")), diffs); + + diffs = diffList(Diff(DELETE, "abcxx"), Diff(INSERT, "xxdef")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: No overlap elimination.", diffList(Diff(DELETE, "abcxx"), Diff(INSERT, "xxdef")), diffs); + + diffs = diffList(Diff(DELETE, "abcxxx"), Diff(INSERT, "xxxdef")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Overlap elimination.", diffList(Diff(DELETE, "abc"), Diff(EQUAL, "xxx"), Diff(INSERT, "def")), diffs); + + diffs = diffList(Diff(DELETE, "xxxabc"), Diff(INSERT, "defxxx")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Reverse overlap elimination.", diffList(Diff(INSERT, "def"), Diff(EQUAL, "xxx"), Diff(DELETE, "abc")), diffs); + + diffs = diffList(Diff(DELETE, "abcd1212"), Diff(INSERT, "1212efghi"), Diff(EQUAL, "----"), Diff(DELETE, "A3"), Diff(INSERT, "3BC")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Two overlap eliminations.", diffList(Diff(DELETE, "abcd"), Diff(EQUAL, "1212"), Diff(INSERT, "efghi"), Diff(EQUAL, "----"), Diff(DELETE, "A"), Diff(EQUAL, "3"), Diff(INSERT, "BC")), diffs); +} + +void diff_match_patch_test::testDiffCleanupEfficiency() { + // Cleanup operationally trivial equalities. + dmp.Diff_EditCost = 4; + QList diffs = diffList(); + dmp.diff_cleanupEfficiency(diffs); + assertEquals("diff_cleanupEfficiency: Null case.", diffList(), diffs); + + diffs = diffList(Diff(DELETE, "ab"), Diff(INSERT, "12"), Diff(EQUAL, "wxyz"), Diff(DELETE, "cd"), Diff(INSERT, "34")); + dmp.diff_cleanupEfficiency(diffs); + assertEquals("diff_cleanupEfficiency: No elimination.", diffList(Diff(DELETE, "ab"), Diff(INSERT, "12"), Diff(EQUAL, "wxyz"), Diff(DELETE, "cd"), Diff(INSERT, "34")), diffs); + + diffs = diffList(Diff(DELETE, "ab"), Diff(INSERT, "12"), Diff(EQUAL, "xyz"), Diff(DELETE, "cd"), Diff(INSERT, "34")); + dmp.diff_cleanupEfficiency(diffs); + assertEquals("diff_cleanupEfficiency: Four-edit elimination.", diffList(Diff(DELETE, "abxyzcd"), Diff(INSERT, "12xyz34")), diffs); + + diffs = diffList(Diff(INSERT, "12"), Diff(EQUAL, "x"), Diff(DELETE, "cd"), Diff(INSERT, "34")); + dmp.diff_cleanupEfficiency(diffs); + assertEquals("diff_cleanupEfficiency: Three-edit elimination.", diffList(Diff(DELETE, "xcd"), Diff(INSERT, "12x34")), diffs); + + diffs = diffList(Diff(DELETE, "ab"), Diff(INSERT, "12"), Diff(EQUAL, "xy"), Diff(INSERT, "34"), Diff(EQUAL, "z"), Diff(DELETE, "cd"), Diff(INSERT, "56")); + dmp.diff_cleanupEfficiency(diffs); + assertEquals("diff_cleanupEfficiency: Backpass elimination.", diffList(Diff(DELETE, "abxyzcd"), Diff(INSERT, "12xy34z56")), diffs); + + dmp.Diff_EditCost = 5; + diffs = diffList(Diff(DELETE, "ab"), Diff(INSERT, "12"), Diff(EQUAL, "wxyz"), Diff(DELETE, "cd"), Diff(INSERT, "34")); + dmp.diff_cleanupEfficiency(diffs); + assertEquals("diff_cleanupEfficiency: High cost elimination.", diffList(Diff(DELETE, "abwxyzcd"), Diff(INSERT, "12wxyz34")), diffs); + dmp.Diff_EditCost = 4; +} + +void diff_match_patch_test::testDiffPrettyHtml() { + // Pretty print. + QList diffs = diffList(Diff(EQUAL, "a\n"), Diff(DELETE, "b"), Diff(INSERT, "c&d")); + assertEquals("diff_prettyHtml:", "
<B>b</B>c&d", dmp.diff_prettyHtml(diffs)); +} + +void diff_match_patch_test::testDiffText() { + // Compute the source and destination texts. + QList diffs = diffList(Diff(EQUAL, "jump"), Diff(DELETE, "s"), Diff(INSERT, "ed"), Diff(EQUAL, " over "), Diff(DELETE, "the"), Diff(INSERT, "a"), Diff(EQUAL, " lazy")); + assertEquals("diff_text1:", "jumps over the lazy", dmp.diff_text1(diffs)); + assertEquals("diff_text2:", "jumped over a lazy", dmp.diff_text2(diffs)); +} + +void diff_match_patch_test::testDiffDelta() { + // Convert a diff into delta string. + QList diffs = diffList(Diff(EQUAL, "jump"), Diff(DELETE, "s"), Diff(INSERT, "ed"), Diff(EQUAL, " over "), Diff(DELETE, "the"), Diff(INSERT, "a"), Diff(EQUAL, " lazy"), Diff(INSERT, "old dog")); + QString text1 = dmp.diff_text1(diffs); + assertEquals("diff_text1: Base text.", "jumps over the lazy", text1); + + QString delta = dmp.diff_toDelta(diffs); + assertEquals("diff_toDelta:", "=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog", delta); + + // Convert delta string into a diff. + assertEquals("diff_fromDelta: Normal.", diffs, dmp.diff_fromDelta(text1, delta)); + + // Generates error (19 < 20). + try { + dmp.diff_fromDelta(text1 + "x", delta); + assertFalse("diff_fromDelta: Too long.", true); + } catch (QString ex) { + // Exception expected. + } + + // Generates error (19 > 18). + try { + dmp.diff_fromDelta(text1.mid(1), delta); + assertFalse("diff_fromDelta: Too short.", true); + } catch (QString ex) { + // Exception expected. + } + + // Generates error (%c3%xy invalid Unicode). + /* This test does not work because QUrl::fromPercentEncoding("%xy") -> "?" + try { + dmp.diff_fromDelta("", "+%c3%xy"); + assertFalse("diff_fromDelta: Invalid character.", true); + } catch (QString ex) { + // Exception expected. + } + */ + + // Test deltas with special characters. + diffs = diffList(Diff(EQUAL, QString::fromWCharArray((const wchar_t*) L"\u0680 \000 \t %", 7)), Diff(DELETE, QString::fromWCharArray((const wchar_t*) L"\u0681 \001 \n ^", 7)), Diff(INSERT, QString::fromWCharArray((const wchar_t*) L"\u0682 \002 \\ |", 7))); + text1 = dmp.diff_text1(diffs); + assertEquals("diff_text1: Unicode text.", QString::fromWCharArray((const wchar_t*) L"\u0680 \000 \t %\u0681 \001 \n ^", 14), text1); + + delta = dmp.diff_toDelta(diffs); + assertEquals("diff_toDelta: Unicode.", "=7\t-7\t+%DA%82 %02 %5C %7C", delta); + + assertEquals("diff_fromDelta: Unicode.", diffs, dmp.diff_fromDelta(text1, delta)); + + // Verify pool of unchanged characters. + diffs = diffList(Diff(INSERT, "A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ")); + QString text2 = dmp.diff_text2(diffs); + assertEquals("diff_text2: Unchanged characters.", "A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", text2); + + delta = dmp.diff_toDelta(diffs); + assertEquals("diff_toDelta: Unchanged characters.", "+A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", delta); + + // Convert delta string into a diff. + assertEquals("diff_fromDelta: Unchanged characters.", diffs, dmp.diff_fromDelta("", delta)); +} + +void diff_match_patch_test::testDiffXIndex() { + // Translate a location in text1 to text2. + QList diffs = diffList(Diff(DELETE, "a"), Diff(INSERT, "1234"), Diff(EQUAL, "xyz")); + assertEquals("diff_xIndex: Translation on equality.", 5, dmp.diff_xIndex(diffs, 2)); + + diffs = diffList(Diff(EQUAL, "a"), Diff(DELETE, "1234"), Diff(EQUAL, "xyz")); + assertEquals("diff_xIndex: Translation on deletion.", 1, dmp.diff_xIndex(diffs, 3)); +} + +void diff_match_patch_test::testDiffLevenshtein() { + QList diffs = diffList(Diff(DELETE, "abc"), Diff(INSERT, "1234"), Diff(EQUAL, "xyz")); + assertEquals("diff_levenshtein: Trailing equality.", 4, dmp.diff_levenshtein(diffs)); + + diffs = diffList(Diff(EQUAL, "xyz"), Diff(DELETE, "abc"), Diff(INSERT, "1234")); + assertEquals("diff_levenshtein: Leading equality.", 4, dmp.diff_levenshtein(diffs)); + + diffs = diffList(Diff(DELETE, "abc"), Diff(EQUAL, "xyz"), Diff(INSERT, "1234")); + assertEquals("diff_levenshtein: Middle equality.", 7, dmp.diff_levenshtein(diffs)); +} + +void diff_match_patch_test::testDiffBisect() { + // Normal. + QString a = "cat"; + QString b = "map"; + // Since the resulting diff hasn't been normalized, it would be ok if + // the insertion and deletion pairs are swapped. + // If the order changes, tweak this test as required. + QList diffs = diffList(Diff(DELETE, "c"), Diff(INSERT, "m"), Diff(EQUAL, "a"), Diff(DELETE, "t"), Diff(INSERT, "p")); + assertEquals("diff_bisect: Normal.", diffs, dmp.diff_bisect(a, b, std::numeric_limits::max())); + + // Timeout. + diffs = diffList(Diff(DELETE, "cat"), Diff(INSERT, "map")); + assertEquals("diff_bisect: Timeout.", diffs, dmp.diff_bisect(a, b, 0)); +} + +void diff_match_patch_test::testDiffMain() { + // Perform a trivial diff. + QList diffs = diffList(); + assertEquals("diff_main: Null case.", diffs, dmp.diff_main("", "", false)); + + diffs = diffList(Diff(EQUAL, "abc")); + assertEquals("diff_main: Equality.", diffs, dmp.diff_main("abc", "abc", false)); + + diffs = diffList(Diff(EQUAL, "ab"), Diff(INSERT, "123"), Diff(EQUAL, "c")); + assertEquals("diff_main: Simple insertion.", diffs, dmp.diff_main("abc", "ab123c", false)); + + diffs = diffList(Diff(EQUAL, "a"), Diff(DELETE, "123"), Diff(EQUAL, "bc")); + assertEquals("diff_main: Simple deletion.", diffs, dmp.diff_main("a123bc", "abc", false)); + + diffs = diffList(Diff(EQUAL, "a"), Diff(INSERT, "123"), Diff(EQUAL, "b"), Diff(INSERT, "456"), Diff(EQUAL, "c")); + assertEquals("diff_main: Two insertions.", diffs, dmp.diff_main("abc", "a123b456c", false)); + + diffs = diffList(Diff(EQUAL, "a"), Diff(DELETE, "123"), Diff(EQUAL, "b"), Diff(DELETE, "456"), Diff(EQUAL, "c")); + assertEquals("diff_main: Two deletions.", diffs, dmp.diff_main("a123b456c", "abc", false)); + + // Perform a real diff. + // Switch off the timeout. + dmp.Diff_Timeout = 0; + diffs = diffList(Diff(DELETE, "a"), Diff(INSERT, "b")); + assertEquals("diff_main: Simple case #1.", diffs, dmp.diff_main("a", "b", false)); + + diffs = diffList(Diff(DELETE, "Apple"), Diff(INSERT, "Banana"), Diff(EQUAL, "s are a"), Diff(INSERT, "lso"), Diff(EQUAL, " fruit.")); + assertEquals("diff_main: Simple case #2.", diffs, dmp.diff_main("Apples are a fruit.", "Bananas are also fruit.", false)); + + diffs = diffList(Diff(DELETE, "a"), Diff(INSERT, QString::fromWCharArray((const wchar_t*) L"\u0680", 1)), Diff(EQUAL, "x"), Diff(DELETE, "\t"), Diff(INSERT, QString::fromWCharArray((const wchar_t*) L"\000", 1))); + assertEquals("diff_main: Simple case #3.", diffs, dmp.diff_main("ax\t", QString::fromWCharArray((const wchar_t*) L"\u0680x\000", 3), false)); + + diffs = diffList(Diff(DELETE, "1"), Diff(EQUAL, "a"), Diff(DELETE, "y"), Diff(EQUAL, "b"), Diff(DELETE, "2"), Diff(INSERT, "xab")); + assertEquals("diff_main: Overlap #1.", diffs, dmp.diff_main("1ayb2", "abxab", false)); + + diffs = diffList(Diff(INSERT, "xaxcx"), Diff(EQUAL, "abc"), Diff(DELETE, "y")); + assertEquals("diff_main: Overlap #2.", diffs, dmp.diff_main("abcy", "xaxcxabc", false)); + + diffs = diffList(Diff(DELETE, "ABCD"), Diff(EQUAL, "a"), Diff(DELETE, "="), Diff(INSERT, "-"), Diff(EQUAL, "bcd"), Diff(DELETE, "="), Diff(INSERT, "-"), Diff(EQUAL, "efghijklmnopqrs"), Diff(DELETE, "EFGHIJKLMNOefg")); + assertEquals("diff_main: Overlap #3.", diffs, dmp.diff_main("ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg", "a-bcd-efghijklmnopqrs", false)); + + diffs = diffList(Diff(INSERT, " "), Diff(EQUAL, "a"), Diff(INSERT, "nd"), Diff(EQUAL, " [[Pennsylvania]]"), Diff(DELETE, " and [[New")); + assertEquals("diff_main: Large equality.", diffs, dmp.diff_main("a [[Pennsylvania]] and [[New", " and [[Pennsylvania]]", false)); + + dmp.Diff_Timeout = 0.1f; // 100ms + // This test may 'fail' on extremely fast computers. If so, just increase the text lengths. + QString a = "`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n"; + QString b = "I am the very model of a modern major general,\nI've information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n"; + // Increase the text lengths by 1024 times to ensure a timeout. + for (int x = 0; x < 10; x++) { + a = a + a; + b = b + b; + } + clock_t startTime = clock(); + dmp.diff_main(a, b); + clock_t endTime = clock(); + // Test that we took at least the timeout period. + assertTrue("diff_main: Timeout min.", dmp.Diff_Timeout * CLOCKS_PER_SEC <= endTime - startTime); + // Test that we didn't take forever (be forgiving). + // Theoretically this test could fail very occasionally if the + // OS task swaps or locks up for a second at the wrong moment. + // Java seems to overrun by ~80% (compared with 10% for other languages). + // Therefore use an upper limit of 0.5s instead of 0.2s. + assertTrue("diff_main: Timeout max.", dmp.Diff_Timeout * CLOCKS_PER_SEC * 2 > endTime - startTime); + dmp.Diff_Timeout = 0; + + // Test the linemode speedup. + // Must be long to pass the 100 char cutoff. + a = "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n"; + b = "abcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\n"; + assertEquals("diff_main: Simple line-mode.", dmp.diff_main(a, b, true), dmp.diff_main(a, b, false)); + + a = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; + b = "abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij"; + assertEquals("diff_main: Single line-mode.", dmp.diff_main(a, b, true), dmp.diff_main(a, b, false)); + + a = "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n"; + b = "abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n"; + QStringList texts_linemode = diff_rebuildtexts(dmp.diff_main(a, b, true)); + QStringList texts_textmode = diff_rebuildtexts(dmp.diff_main(a, b, false)); + assertEquals("diff_main: Overlap line-mode.", texts_textmode, texts_linemode); + + // Test null inputs. + try { + dmp.diff_main(NULL, NULL); + assertFalse("diff_main: Null inputs.", true); + } catch (const char* ex) { + // Exception expected. + } +} + + +// MATCH TEST FUNCTIONS + + +void diff_match_patch_test::testMatchAlphabet() { + // Initialise the bitmasks for Bitap. + QMap bitmask; + bitmask.insert('a', 4); + bitmask.insert('b', 2); + bitmask.insert('c', 1); + assertEquals("match_alphabet: Unique.", bitmask, dmp.match_alphabet("abc")); + + bitmask = QMap(); + bitmask.insert('a', 37); + bitmask.insert('b', 18); + bitmask.insert('c', 8); + assertEquals("match_alphabet: Duplicates.", bitmask, dmp.match_alphabet("abcaba")); +} + +void diff_match_patch_test::testMatchBitap() { + // Bitap algorithm. + dmp.Match_Distance = 100; + dmp.Match_Threshold = 0.5f; + assertEquals("match_bitap: Exact match #1.", 5, dmp.match_bitap("abcdefghijk", "fgh", 5)); + + assertEquals("match_bitap: Exact match #2.", 5, dmp.match_bitap("abcdefghijk", "fgh", 0)); + + assertEquals("match_bitap: Fuzzy match #1.", 4, dmp.match_bitap("abcdefghijk", "efxhi", 0)); + + assertEquals("match_bitap: Fuzzy match #2.", 2, dmp.match_bitap("abcdefghijk", "cdefxyhijk", 5)); + + assertEquals("match_bitap: Fuzzy match #3.", -1, dmp.match_bitap("abcdefghijk", "bxy", 1)); + + assertEquals("match_bitap: Overflow.", 2, dmp.match_bitap("123456789xx0", "3456789x0", 2)); + + assertEquals("match_bitap: Before start match.", 0, dmp.match_bitap("abcdef", "xxabc", 4)); + + assertEquals("match_bitap: Beyond end match.", 3, dmp.match_bitap("abcdef", "defyy", 4)); + + assertEquals("match_bitap: Oversized pattern.", 0, dmp.match_bitap("abcdef", "xabcdefy", 0)); + + dmp.Match_Threshold = 0.4f; + assertEquals("match_bitap: Threshold #1.", 4, dmp.match_bitap("abcdefghijk", "efxyhi", 1)); + + dmp.Match_Threshold = 0.3f; + assertEquals("match_bitap: Threshold #2.", -1, dmp.match_bitap("abcdefghijk", "efxyhi", 1)); + + dmp.Match_Threshold = 0.0f; + assertEquals("match_bitap: Threshold #3.", 1, dmp.match_bitap("abcdefghijk", "bcdef", 1)); + + dmp.Match_Threshold = 0.5f; + assertEquals("match_bitap: Multiple select #1.", 0, dmp.match_bitap("abcdexyzabcde", "abccde", 3)); + + assertEquals("match_bitap: Multiple select #2.", 8, dmp.match_bitap("abcdexyzabcde", "abccde", 5)); + + dmp.Match_Distance = 10; // Strict location. + assertEquals("match_bitap: Distance test #1.", -1, dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24)); + + assertEquals("match_bitap: Distance test #2.", 0, dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdxxefg", 1)); + + dmp.Match_Distance = 1000; // Loose location. + assertEquals("match_bitap: Distance test #3.", 0, dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24)); +} + +void diff_match_patch_test::testMatchMain() { + // Full match. + assertEquals("match_main: Equality.", 0, dmp.match_main("abcdef", "abcdef", 1000)); + + assertEquals("match_main: Null text.", -1, dmp.match_main("", "abcdef", 1)); + + assertEquals("match_main: Null pattern.", 3, dmp.match_main("abcdef", "", 3)); + + assertEquals("match_main: Exact match.", 3, dmp.match_main("abcdef", "de", 3)); + + dmp.Match_Threshold = 0.7f; + assertEquals("match_main: Complex match.", 4, dmp.match_main("I am the very model of a modern major general.", " that berry ", 5)); + dmp.Match_Threshold = 0.5f; + + // Test null inputs. + try { + dmp.match_main(NULL, NULL, 0); + assertFalse("match_main: Null inputs.", true); + } catch (const char* ex) { + // Exception expected. + } +} + + +// PATCH TEST FUNCTIONS + + +void diff_match_patch_test::testPatchObj() { + // Patch Object. + Patch p; + p.start1 = 20; + p.start2 = 21; + p.length1 = 18; + p.length2 = 17; + p.diffs = diffList(Diff(EQUAL, "jump"), Diff(DELETE, "s"), Diff(INSERT, "ed"), Diff(EQUAL, " over "), Diff(DELETE, "the"), Diff(INSERT, "a"), Diff(EQUAL, "\nlaz")); + QString strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n"; + assertEquals("Patch: toString.", strp, p.toString()); +} + +void diff_match_patch_test::testPatchFromText() { + assertTrue("patch_fromText: #0.", dmp.patch_fromText("").isEmpty()); + + QString strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n"; + assertEquals("patch_fromText: #1.", strp, dmp.patch_fromText(strp).value(0).toString()); + + assertEquals("patch_fromText: #2.", "@@ -1 +1 @@\n-a\n+b\n", dmp.patch_fromText("@@ -1 +1 @@\n-a\n+b\n").value(0).toString()); + + assertEquals("patch_fromText: #3.", "@@ -1,3 +0,0 @@\n-abc\n", dmp.patch_fromText("@@ -1,3 +0,0 @@\n-abc\n").value(0).toString()); + + assertEquals("patch_fromText: #4.", "@@ -0,0 +1,3 @@\n+abc\n", dmp.patch_fromText("@@ -0,0 +1,3 @@\n+abc\n").value(0).toString()); + + // Generates error. + try { + dmp.patch_fromText("Bad\nPatch\n"); + assertFalse("patch_fromText: #5.", true); + } catch (QString ex) { + // Exception expected. + } +} + +void diff_match_patch_test::testPatchToText() { + QString strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n"; + QList patches; + patches = dmp.patch_fromText(strp); + assertEquals("patch_toText: Single", strp, dmp.patch_toText(patches)); + + strp = "@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n"; + patches = dmp.patch_fromText(strp); + assertEquals("patch_toText: Dual", strp, dmp.patch_toText(patches)); +} + +void diff_match_patch_test::testPatchAddContext() { + dmp.Patch_Margin = 4; + Patch p; + p = dmp.patch_fromText("@@ -21,4 +21,10 @@\n-jump\n+somersault\n").value(0); + dmp.patch_addContext(p, "The quick brown fox jumps over the lazy dog."); + assertEquals("patch_addContext: Simple case.", "@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n", p.toString()); + + p = dmp.patch_fromText("@@ -21,4 +21,10 @@\n-jump\n+somersault\n").value(0); + dmp.patch_addContext(p, "The quick brown fox jumps."); + assertEquals("patch_addContext: Not enough trailing context.", "@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n", p.toString()); + + p = dmp.patch_fromText("@@ -3 +3,2 @@\n-e\n+at\n").value(0); + dmp.patch_addContext(p, "The quick brown fox jumps."); + assertEquals("patch_addContext: Not enough leading context.", "@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n", p.toString()); + + p = dmp.patch_fromText("@@ -3 +3,2 @@\n-e\n+at\n").value(0); + dmp.patch_addContext(p, "The quick brown fox jumps. The quick brown fox crashes."); + assertEquals("patch_addContext: Ambiguity.", "@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n", p.toString()); +} + +void diff_match_patch_test::testPatchMake() { + QList patches; + patches = dmp.patch_make("", ""); + assertEquals("patch_make: Null case", "", dmp.patch_toText(patches)); + + QString text1 = "The quick brown fox jumps over the lazy dog."; + QString text2 = "That quick brown fox jumped over a lazy dog."; + QString expectedPatch = "@@ -1,8 +1,7 @@\n Th\n-at\n+e\n qui\n@@ -21,17 +21,18 @@\n jump\n-ed\n+s\n over \n-a\n+the\n laz\n"; + // The second patch must be "-21,17 +21,18", not "-22,17 +21,18" due to rolling context. + patches = dmp.patch_make(text2, text1); + assertEquals("patch_make: Text2+Text1 inputs", expectedPatch, dmp.patch_toText(patches)); + + expectedPatch = "@@ -1,11 +1,12 @@\n Th\n-e\n+at\n quick b\n@@ -22,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n"; + patches = dmp.patch_make(text1, text2); + assertEquals("patch_make: Text1+Text2 inputs", expectedPatch, dmp.patch_toText(patches)); + + QList diffs = dmp.diff_main(text1, text2, false); + patches = dmp.patch_make(diffs); + assertEquals("patch_make: Diff input", expectedPatch, dmp.patch_toText(patches)); + + patches = dmp.patch_make(text1, diffs); + assertEquals("patch_make: Text1+Diff inputs", expectedPatch, dmp.patch_toText(patches)); + + patches = dmp.patch_make(text1, text2, diffs); + assertEquals("patch_make: Text1+Text2+Diff inputs (deprecated)", expectedPatch, dmp.patch_toText(patches)); + + patches = dmp.patch_make("`1234567890-=[]\\;',./", "~!@#$%^&*()_+{}|:\"<>?"); + assertEquals("patch_toText: Character encoding.", "@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n", dmp.patch_toText(patches)); + + diffs = diffList(Diff(DELETE, "`1234567890-=[]\\;',./"), Diff(INSERT, "~!@#$%^&*()_+{}|:\"<>?")); + assertEquals("patch_fromText: Character decoding.", diffs, dmp.patch_fromText("@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n").value(0).diffs); + + text1 = ""; + for (int x = 0; x < 100; x++) { + text1 += "abcdef"; + } + text2 = text1 + "123"; + expectedPatch = "@@ -573,28 +573,31 @@\n cdefabcdefabcdefabcdefabcdef\n+123\n"; + patches = dmp.patch_make(text1, text2); + assertEquals("patch_make: Long string with repeats.", expectedPatch, dmp.patch_toText(patches)); + + // Test null inputs. + try { + dmp.patch_make(NULL, NULL); + assertFalse("patch_make: Null inputs.", true); + } catch (const char* ex) { + // Exception expected. + } +} + +void diff_match_patch_test::testPatchSplitMax() { + // Assumes that Match_MaxBits is 32. + QList patches; + patches = dmp.patch_make("abcdefghijklmnopqrstuvwxyz01234567890", "XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0"); + dmp.patch_splitMax(patches); + assertEquals("patch_splitMax: #1.", "@@ -1,32 +1,46 @@\n+X\n ab\n+X\n cd\n+X\n ef\n+X\n gh\n+X\n ij\n+X\n kl\n+X\n mn\n+X\n op\n+X\n qr\n+X\n st\n+X\n uv\n+X\n wx\n+X\n yz\n+X\n 012345\n@@ -25,13 +39,18 @@\n zX01\n+X\n 23\n+X\n 45\n+X\n 67\n+X\n 89\n+X\n 0\n", dmp.patch_toText(patches)); + + patches = dmp.patch_make("abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz", "abcdefuvwxyz"); + QString oldToText = dmp.patch_toText(patches); + dmp.patch_splitMax(patches); + assertEquals("patch_splitMax: #2.", oldToText, dmp.patch_toText(patches)); + + patches = dmp.patch_make("1234567890123456789012345678901234567890123456789012345678901234567890", "abc"); + dmp.patch_splitMax(patches); + assertEquals("patch_splitMax: #3.", "@@ -1,32 +1,4 @@\n-1234567890123456789012345678\n 9012\n@@ -29,32 +1,4 @@\n-9012345678901234567890123456\n 7890\n@@ -57,14 +1,3 @@\n-78901234567890\n+abc\n", dmp.patch_toText(patches)); + + patches = dmp.patch_make("abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1", "abcdefghij , h : 1 , t : 1 abcdefghij , h : 1 , t : 1 abcdefghij , h : 0 , t : 1"); + dmp.patch_splitMax(patches); + assertEquals("patch_splitMax: #4.", "@@ -2,32 +2,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n@@ -29,32 +29,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n", dmp.patch_toText(patches)); +} + +void diff_match_patch_test::testPatchAddPadding() { + QList patches; + patches = dmp.patch_make("", "test"); + assertEquals("patch_addPadding: Both edges full.", "@@ -0,0 +1,4 @@\n+test\n", dmp.patch_toText(patches)); + dmp.patch_addPadding(patches); + assertEquals("patch_addPadding: Both edges full.", "@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n", dmp.patch_toText(patches)); + + patches = dmp.patch_make("XY", "XtestY"); + assertEquals("patch_addPadding: Both edges partial.", "@@ -1,2 +1,6 @@\n X\n+test\n Y\n", dmp.patch_toText(patches)); + dmp.patch_addPadding(patches); + assertEquals("patch_addPadding: Both edges partial.", "@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n", dmp.patch_toText(patches)); + + patches = dmp.patch_make("XXXXYYYY", "XXXXtestYYYY"); + assertEquals("patch_addPadding: Both edges none.", "@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n", dmp.patch_toText(patches)); + dmp.patch_addPadding(patches); + assertEquals("patch_addPadding: Both edges none.", "@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n", dmp.patch_toText(patches)); +} + +void diff_match_patch_test::testPatchApply() { + dmp.Match_Distance = 1000; + dmp.Match_Threshold = 0.5f; + dmp.Patch_DeleteThreshold = 0.5f; + QList patches; + patches = dmp.patch_make("", ""); + QPair > results = dmp.patch_apply(patches, "Hello world."); + QVector boolArray = results.second; + + QString resultStr = QString("%1\t%2").arg(results.first).arg(boolArray.count()); + assertEquals("patch_apply: Null case.", "Hello world.\t0", resultStr); + + patches = dmp.patch_make("The quick brown fox jumps over the lazy dog.", "That quick brown fox jumped over a lazy dog."); + results = dmp.patch_apply(patches, "The quick brown fox jumps over the lazy dog."); + boolArray = results.second; + resultStr = results.first + "\t" + (boolArray[0] ? "true" : "false") + "\t" + (boolArray[1] ? "true" : "false"); + assertEquals("patch_apply: Exact match.", "That quick brown fox jumped over a lazy dog.\ttrue\ttrue", resultStr); + + results = dmp.patch_apply(patches, "The quick red rabbit jumps over the tired tiger."); + boolArray = results.second; + resultStr = results.first + "\t" + (boolArray[0] ? "true" : "false") + "\t" + (boolArray[1] ? "true" : "false"); + assertEquals("patch_apply: Partial match.", "That quick red rabbit jumped over a tired tiger.\ttrue\ttrue", resultStr); + + results = dmp.patch_apply(patches, "I am the very model of a modern major general."); + boolArray = results.second; + resultStr = results.first + "\t" + (boolArray[0] ? "true" : "false") + "\t" + (boolArray[1] ? "true" : "false"); + assertEquals("patch_apply: Failed match.", "I am the very model of a modern major general.\tfalse\tfalse", resultStr); + + patches = dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy"); + results = dmp.patch_apply(patches, "x123456789012345678901234567890-----++++++++++-----123456789012345678901234567890y"); + boolArray = results.second; + resultStr = results.first + "\t" + (boolArray[0] ? "true" : "false") + "\t" + (boolArray[1] ? "true" : "false"); + assertEquals("patch_apply: Big delete, small change.", "xabcy\ttrue\ttrue", resultStr); + + patches = dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy"); + results = dmp.patch_apply(patches, "x12345678901234567890---------------++++++++++---------------12345678901234567890y"); + boolArray = results.second; + resultStr = results.first + "\t" + (boolArray[0] ? "true" : "false") + "\t" + (boolArray[1] ? "true" : "false"); + assertEquals("patch_apply: Big delete, large change 1.", "xabc12345678901234567890---------------++++++++++---------------12345678901234567890y\tfalse\ttrue", resultStr); + + dmp.Patch_DeleteThreshold = 0.6f; + patches = dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy"); + results = dmp.patch_apply(patches, "x12345678901234567890---------------++++++++++---------------12345678901234567890y"); + boolArray = results.second; + resultStr = results.first + "\t" + (boolArray[0] ? "true" : "false") + "\t" + (boolArray[1] ? "true" : "false"); + assertEquals("patch_apply: Big delete, large change 2.", "xabcy\ttrue\ttrue", resultStr); + dmp.Patch_DeleteThreshold = 0.5f; + + dmp.Match_Threshold = 0.0f; + dmp.Match_Distance = 0; + patches = dmp.patch_make("abcdefghijklmnopqrstuvwxyz--------------------1234567890", "abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------1234567YYYYYYYYYY890"); + results = dmp.patch_apply(patches, "ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890"); + boolArray = results.second; + resultStr = results.first + "\t" + (boolArray[0] ? "true" : "false") + "\t" + (boolArray[1] ? "true" : "false"); + assertEquals("patch_apply: Compensate for failed patch.", "ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890\tfalse\ttrue", resultStr); + dmp.Match_Threshold = 0.5f; + dmp.Match_Distance = 1000; + + patches = dmp.patch_make("", "test"); + QString patchStr = dmp.patch_toText(patches); + dmp.patch_apply(patches, ""); + assertEquals("patch_apply: No side effects.", patchStr, dmp.patch_toText(patches)); + + patches = dmp.patch_make("The quick brown fox jumps over the lazy dog.", "Woof"); + patchStr = dmp.patch_toText(patches); + dmp.patch_apply(patches, "The quick brown fox jumps over the lazy dog."); + assertEquals("patch_apply: No side effects with major delete.", patchStr, dmp.patch_toText(patches)); + + patches = dmp.patch_make("", "test"); + results = dmp.patch_apply(patches, ""); + boolArray = results.second; + resultStr = results.first + "\t" + (boolArray[0] ? "true" : "false"); + assertEquals("patch_apply: Edge exact match.", "test\ttrue", resultStr); + + patches = dmp.patch_make("XY", "XtestY"); + results = dmp.patch_apply(patches, "XY"); + boolArray = results.second; + resultStr = results.first + "\t" + (boolArray[0] ? "true" : "false"); + assertEquals("patch_apply: Near edge exact match.", "XtestY\ttrue", resultStr); + + patches = dmp.patch_make("y", "y123"); + results = dmp.patch_apply(patches, "x"); + boolArray = results.second; + resultStr = results.first + "\t" + (boolArray[0] ? "true" : "false"); + assertEquals("patch_apply: Edge partial match.", "x123\ttrue", resultStr); +} + + +void diff_match_patch_test::assertEquals(const QString &strCase, int n1, int n2) { + if (n1 != n2) { + qDebug("%s FAIL\nExpected: %d\nActual: %d", qPrintable(strCase), n1, n2); + throw strCase; + } + qDebug("%s OK", qPrintable(strCase)); +} + +void diff_match_patch_test::assertEquals(const QString &strCase, const QString &s1, const QString &s2) { + if (s1 != s2) { + qDebug("%s FAIL\nExpected: %s\nActual: %s", + qPrintable(strCase), qPrintable(s1), qPrintable(s2)); + throw strCase; + } + qDebug("%s OK", qPrintable(strCase)); +} + +void diff_match_patch_test::assertEquals(const QString &strCase, const Diff &d1, const Diff &d2) { + if (d1 != d2) { + qDebug("%s FAIL\nExpected: %s\nActual: %s", qPrintable(strCase), + qPrintable(d1.toString()), qPrintable(d2.toString())); + throw strCase; + } + qDebug("%s OK", qPrintable(strCase)); +} + +void diff_match_patch_test::assertEquals(const QString &strCase, const QList &list1, const QList &list2) { + bool fail = false; + if (list1.count() == list2.count()) { + int i = 0; + foreach(Diff d1, list1) { + Diff d2 = list2.value(i); + if (d1 != d2) { + fail = true; + break; + } + i++; + } + } else { + fail = true; + } + + if (fail) { + // Build human readable description of both lists. + QString listString1 = "("; + bool first = true; + foreach(Diff d1, list1) { + if (!first) { + listString1 += ", "; + } + listString1 += d1.toString(); + first = false; + } + listString1 += ")"; + QString listString2 = "("; + first = true; + foreach(Diff d2, list2) { + if (!first) { + listString2 += ", "; + } + listString2 += d2.toString(); + first = false; + } + listString2 += ")"; + qDebug("%s FAIL\nExpected: %s\nActual: %s", + qPrintable(strCase), qPrintable(listString1), qPrintable(listString2)); + throw strCase; + } + qDebug("%s OK", qPrintable(strCase)); +} + +void diff_match_patch_test::assertEquals(const QString &strCase, const QList &list1, const QList &list2) { + bool fail = false; + if (list1.count() == list2.count()) { + int i = 0; + foreach(QVariant q1, list1) { + QVariant q2 = list2.value(i); + if (q1 != q2) { + fail = true; + break; + } + i++; + } + } else { + fail = true; + } + + if (fail) { + // Build human readable description of both lists. + QString listString1 = "("; + bool first = true; + foreach(QVariant q1, list1) { + if (!first) { + listString1 += ", "; + } + listString1 += q1.toString(); + first = false; + } + listString1 += ")"; + QString listString2 = "("; + first = true; + foreach(QVariant q2, list2) { + if (!first) { + listString2 += ", "; + } + listString2 += q2.toString(); + first = false; + } + listString2 += ")"; + qDebug("%s FAIL\nExpected: %s\nActual: %s", + qPrintable(strCase), qPrintable(listString1), qPrintable(listString2)); + throw strCase; + } + qDebug("%s OK", qPrintable(strCase)); +} + +void diff_match_patch_test::assertEquals(const QString &strCase, const QVariant &var1, const QVariant &var2) { + if (var1 != var2) { + qDebug("%s FAIL\nExpected: %s\nActual: %s", qPrintable(strCase), + qPrintable(var1.toString()), qPrintable(var2.toString())); + throw strCase; + } + qDebug("%s OK", qPrintable(strCase)); +} + +void diff_match_patch_test::assertEquals(const QString &strCase, const QMap &m1, const QMap &m2) { + QMapIterator i1(m1), i2(m2); + + while (i1.hasNext() && i2.hasNext()) { + i1.next(); + i2.next(); + if (i1.key() != i2.key() || i1.value() != i2.value()) { + qDebug("%s FAIL\nExpected: (%c, %d)\nActual: (%c, %d)", qPrintable(strCase), + i1.key().toAscii(), i1.value(), i2.key().toAscii(), i2.value()); + throw strCase; + } + } + + if (i1.hasNext()) { + i1.next(); + qDebug("%s FAIL\nExpected: (%c, %d)\nActual: none", + qPrintable(strCase), i1.key().toAscii(), i1.value()); + throw strCase; + } + if (i2.hasNext()) { + i2.next(); + qDebug("%s FAIL\nExpected: none\nActual: (%c, %d)", + qPrintable(strCase), i2.key().toAscii(), i2.value()); + throw strCase; + } + qDebug("%s OK", qPrintable(strCase)); +} + +void diff_match_patch_test::assertEquals(const QString &strCase, const QStringList &list1, const QStringList &list2) { + if (list1 != list2) { + qDebug("%s FAIL\nExpected: %s\nActual: %s", qPrintable(strCase), + qPrintable(list1.join(",")), qPrintable(list2.join(","))); + throw strCase; + } + qDebug("%s OK", qPrintable(strCase)); +} + +void diff_match_patch_test::assertTrue(const QString &strCase, bool value) { + if (!value) { + qDebug("%s FAIL\nExpected: true\nActual: false", qPrintable(strCase)); + throw strCase; + } + qDebug("%s OK", qPrintable(strCase)); +} + +void diff_match_patch_test::assertFalse(const QString &strCase, bool value) { + if (value) { + qDebug("%s FAIL\nExpected: false\nActual: true", qPrintable(strCase)); + throw strCase; + } + qDebug("%s OK", qPrintable(strCase)); +} + + +// Construct the two texts which made up the diff originally. +QStringList diff_match_patch_test::diff_rebuildtexts(QList diffs) { + QStringList text; + text << QString("") << QString(""); + foreach (Diff myDiff, diffs) { + if (myDiff.operation != INSERT) { + text[0] += myDiff.text; + } + if (myDiff.operation != DELETE) { + text[1] += myDiff.text; + } + } + return text; +} + +void diff_match_patch_test::assertEmpty(const QString &strCase, const QStringList &list) { + if (!list.isEmpty()) { + throw strCase; + } +} + + +// Private function for quickly building lists of diffs. +QList diff_match_patch_test::diffList(Diff d1, Diff d2, Diff d3, Diff d4, Diff d5, + Diff d6, Diff d7, Diff d8, Diff d9, Diff d10) { + // Diff(INSERT, NULL) is invalid and thus is used as the default argument. + QList listRet; + if (d1.operation == INSERT && d1.text == NULL) { + return listRet; + } + listRet << d1; + + if (d2.operation == INSERT && d2.text == NULL) { + return listRet; + } + listRet << d2; + + if (d3.operation == INSERT && d3.text == NULL) { + return listRet; + } + listRet << d3; + + if (d4.operation == INSERT && d4.text == NULL) { + return listRet; + } + listRet << d4; + + if (d5.operation == INSERT && d5.text == NULL) { + return listRet; + } + listRet << d5; + + if (d6.operation == INSERT && d6.text == NULL) { + return listRet; + } + listRet << d6; + + if (d7.operation == INSERT && d7.text == NULL) { + return listRet; + } + listRet << d7; + + if (d8.operation == INSERT && d8.text == NULL) { + return listRet; + } + listRet << d8; + + if (d9.operation == INSERT && d9.text == NULL) { + return listRet; + } + listRet << d9; + + if (d10.operation == INSERT && d10.text == NULL) { + return listRet; + } + listRet << d10; + + return listRet; +} + + +/* +Compile instructions for MinGW and QT4 on Windows: +qmake -project +qmake +mingw32-make +g++ -o diff_match_patch_test debug\diff_match_patch_test.o debug\diff_match_patch.o \qt4\lib\libQtCore4.a +diff_match_patch_test.exe + +Compile insructions for OS X: +qmake -spec macx-g++ +make +./diff_match_patch +*/ diff --git a/cpp/diff_match_patch_test.h b/cpp/diff_match_patch_test.h new file mode 100644 index 0000000..9792222 --- /dev/null +++ b/cpp/diff_match_patch_test.h @@ -0,0 +1,89 @@ +/* + * Diff Match and Patch -- Test Harness + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef DIFF_MATCH_PATCH_TEST_H +#define DIFF_MATCH_PATCH_TEST_H + +class diff_match_patch_test { + public: + diff_match_patch_test(); + void run_all_tests(); + + // DIFF TEST FUNCTIONS + void testDiffCommonPrefix(); + void testDiffCommonSuffix(); + void testDiffCommonOverlap(); + void testDiffHalfmatch(); + void testDiffLinesToChars(); + void testDiffCharsToLines(); + void testDiffCleanupMerge(); + void testDiffCleanupSemanticLossless(); + void testDiffCleanupSemantic(); + void testDiffCleanupEfficiency(); + void testDiffPrettyHtml(); + void testDiffText(); + void testDiffDelta(); + void testDiffXIndex(); + void testDiffLevenshtein(); + void testDiffBisect(); + void testDiffMain(); + + // MATCH TEST FUNCTIONS + void testMatchAlphabet(); + void testMatchBitap(); + void testMatchMain(); + + // PATCH TEST FUNCTIONS + void testPatchObj(); + void testPatchFromText(); + void testPatchToText(); + void testPatchAddContext(); + void testPatchMake(); + void testPatchSplitMax(); + void testPatchAddPadding(); + void testPatchApply(); + + private: + diff_match_patch dmp; + + // Define equality. + void assertEquals(const QString &strCase, int n1, int n2); + void assertEquals(const QString &strCase, const QString &s1, const QString &s2); + void assertEquals(const QString &strCase, const Diff &d1, const Diff &d2); + void assertEquals(const QString &strCase, const QList &list1, const QList &list2); + void assertEquals(const QString &strCase, const QList &list1, const QList &list2); + void assertEquals(const QString &strCase, const QVariant &var1, const QVariant &var2); + void assertEquals(const QString &strCase, const QMap &m1, const QMap &m2); + void assertEquals(const QString &strCase, const QStringList &list1, const QStringList &list2); + void assertTrue(const QString &strCase, bool value); + void assertFalse(const QString &strCase, bool value); + void assertEmpty(const QString &strCase, const QStringList &list); + + // Construct the two texts which made up the diff originally. + QStringList diff_rebuildtexts(QList diffs); + // Private function for quickly building lists of diffs. + QList diffList( + // Diff(INSERT, NULL) is invalid and thus is used as the default argument. + Diff d1 = Diff(INSERT, NULL), Diff d2 = Diff(INSERT, NULL), + Diff d3 = Diff(INSERT, NULL), Diff d4 = Diff(INSERT, NULL), + Diff d5 = Diff(INSERT, NULL), Diff d6 = Diff(INSERT, NULL), + Diff d7 = Diff(INSERT, NULL), Diff d8 = Diff(INSERT, NULL), + Diff d9 = Diff(INSERT, NULL), Diff d10 = Diff(INSERT, NULL)); +}; + +#endif // DIFF_MATCH_PATCH_TEST_H diff --git a/csharp/DiffMatchPatch.cs b/csharp/DiffMatchPatch.cs new file mode 100644 index 0000000..e96dc94 --- /dev/null +++ b/csharp/DiffMatchPatch.cs @@ -0,0 +1,2299 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Web; + +namespace DiffMatchPatch { + internal static class CompatibilityExtensions { + // JScript splice function + public static List Splice(this List input, int start, int count, + params T[] objects) { + List deletedRange = input.GetRange(start, count); + input.RemoveRange(start, count); + input.InsertRange(start, objects); + + return deletedRange; + } + + // Java substring function + public static string JavaSubstring(this string s, int begin, int end) { + return s.Substring(begin, end - begin); + } + } + + /**- + * The data structure representing a diff is a List of Diff objects: + * {Diff(Operation.DELETE, "Hello"), Diff(Operation.INSERT, "Goodbye"), + * Diff(Operation.EQUAL, " world.")} + * which means: delete "Hello", add "Goodbye" and keep " world." + */ + public enum Operation { + DELETE, INSERT, EQUAL + } + + + /** + * Class representing one diff operation. + */ + public class Diff { + public Operation operation; + // One of: INSERT, DELETE or EQUAL. + public string text; + // The text associated with this diff operation. + + /** + * Constructor. Initializes the diff with the provided values. + * @param operation One of INSERT, DELETE or EQUAL. + * @param text The text being applied. + */ + public Diff(Operation operation, string text) { + // Construct a diff with the specified operation and text. + this.operation = operation; + this.text = text; + } + + /** + * Display a human-readable version of this Diff. + * @return text version. + */ + public override string ToString() { + string prettyText = this.text.Replace('\n', '\u00b6'); + return "Diff(" + this.operation + ",\"" + prettyText + "\")"; + } + + /** + * Is this Diff equivalent to another Diff? + * @param d Another Diff to compare against. + * @return true or false. + */ + public override bool Equals(Object obj) { + // If parameter is null return false. + if (obj == null) { + return false; + } + + // If parameter cannot be cast to Diff return false. + Diff p = obj as Diff; + if ((System.Object)p == null) { + return false; + } + + // Return true if the fields match. + return p.operation == this.operation && p.text == this.text; + } + + public bool Equals(Diff obj) { + // If parameter is null return false. + if (obj == null) { + return false; + } + + // Return true if the fields match. + return obj.operation == this.operation && obj.text == this.text; + } + + public override int GetHashCode() { + return text.GetHashCode() ^ operation.GetHashCode(); + } + } + + + /** + * Class representing one patch operation. + */ + public class Patch { + public List diffs = new List(); + public int start1; + public int start2; + public int length1; + public int length2; + + /** + * Emmulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indicies are printed as 1-based, not 0-based. + * @return The GNU diff string. + */ + public override string ToString() { + string coords1, coords2; + if (this.length1 == 0) { + coords1 = this.start1 + ",0"; + } else if (this.length1 == 1) { + coords1 = Convert.ToString(this.start1 + 1); + } else { + coords1 = (this.start1 + 1) + "," + this.length1; + } + if (this.length2 == 0) { + coords2 = this.start2 + ",0"; + } else if (this.length2 == 1) { + coords2 = Convert.ToString(this.start2 + 1); + } else { + coords2 = (this.start2 + 1) + "," + this.length2; + } + StringBuilder text = new StringBuilder(); + text.Append("@@ -").Append(coords1).Append(" +").Append(coords2) + .Append(" @@\n"); + // Escape the body of the patch with %xx notation. + foreach (Diff aDiff in this.diffs) { + switch (aDiff.operation) { + case Operation.INSERT: + text.Append('+'); + break; + case Operation.DELETE: + text.Append('-'); + break; + case Operation.EQUAL: + text.Append(' '); + break; + } + + text.Append(HttpUtility.UrlEncode(aDiff.text, + new UTF8Encoding()).Replace('+', ' ')).Append("\n"); + } + + return diff_match_patch.unescapeForEncodeUriCompatability( + text.ToString()); + } + } + + + /** + * Class containing the diff, match and patch methods. + * Also Contains the behaviour settings. + */ + public class diff_match_patch { + // Defaults. + // Set these on your diff_match_patch instance to override the defaults. + + // Number of seconds to map a diff before giving up (0 for infinity). + public float Diff_Timeout = 1.0f; + // Cost of an empty edit operation in terms of edit characters. + public short Diff_EditCost = 4; + // At what point is no match declared (0.0 = perfection, 1.0 = very loose). + public float Match_Threshold = 0.5f; + // How far to search for a match (0 = exact location, 1000+ = broad match). + // A match this many characters away from the expected location will add + // 1.0 to the score (0.0 is a perfect match). + public int Match_Distance = 1000; + // When deleting a large block of text (over ~64 characters), how close + // do the contents have to be to match the expected contents. (0.0 = + // perfection, 1.0 = very loose). Note that Match_Threshold controls + // how closely the end points of a delete need to match. + public float Patch_DeleteThreshold = 0.5f; + // Chunk size for context length. + public short Patch_Margin = 4; + + // The number of bits in an int. + private short Match_MaxBits = 32; + + + // DIFF FUNCTIONS + + + /** + * Find the differences between two texts. + * Run a faster, slightly less optimal diff. + * This method allows the 'checklines' of diff_main() to be optional. + * Most of the time checklines is wanted, so default to true. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @return List of Diff objects. + */ + public List diff_main(string text1, string text2) { + return diff_main(text1, text2, true); + } + + /** + * Find the differences between two texts. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @return List of Diff objects. + */ + public List diff_main(string text1, string text2, bool checklines) { + // Set a deadline by which time the diff must be complete. + DateTime deadline; + if (this.Diff_Timeout <= 0) { + deadline = DateTime.MaxValue; + } else { + deadline = DateTime.Now + + new TimeSpan(((long)(Diff_Timeout * 1000)) * 10000); + } + return diff_main(text1, text2, checklines, deadline); + } + + /** + * Find the differences between two texts. Simplifies the problem by + * stripping any common prefix or suffix off the texts before diffing. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. Used + * internally for recursive calls. Users should set DiffTimeout + * instead. + * @return List of Diff objects. + */ + private List diff_main(string text1, string text2, bool checklines, + DateTime deadline) { + // Check for null inputs not needed since null can't be passed in C#. + + // Check for equality (speedup). + List diffs; + if (text1 == text2) { + diffs = new List(); + if (text1.Length != 0) { + diffs.Add(new Diff(Operation.EQUAL, text1)); + } + return diffs; + } + + // Trim off common prefix (speedup). + int commonlength = diff_commonPrefix(text1, text2); + string commonprefix = text1.Substring(0, commonlength); + text1 = text1.Substring(commonlength); + text2 = text2.Substring(commonlength); + + // Trim off common suffix (speedup). + commonlength = diff_commonSuffix(text1, text2); + string commonsuffix = text1.Substring(text1.Length - commonlength); + text1 = text1.Substring(0, text1.Length - commonlength); + text2 = text2.Substring(0, text2.Length - commonlength); + + // Compute the diff on the middle block. + diffs = diff_compute(text1, text2, checklines, deadline); + + // Restore the prefix and suffix. + if (commonprefix.Length != 0) { + diffs.Insert(0, (new Diff(Operation.EQUAL, commonprefix))); + } + if (commonsuffix.Length != 0) { + diffs.Add(new Diff(Operation.EQUAL, commonsuffix)); + } + + diff_cleanupMerge(diffs); + return diffs; + } + + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. + * @return List of Diff objects. + */ + private List diff_compute(string text1, string text2, + bool checklines, DateTime deadline) { + List diffs = new List(); + + if (text1.Length == 0) { + // Just add some text (speedup). + diffs.Add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + if (text2.Length == 0) { + // Just delete some text (speedup). + diffs.Add(new Diff(Operation.DELETE, text1)); + return diffs; + } + + string longtext = text1.Length > text2.Length ? text1 : text2; + string shorttext = text1.Length > text2.Length ? text2 : text1; + int i = longtext.IndexOf(shorttext, StringComparison.Ordinal); + if (i != -1) { + // Shorter text is inside the longer text (speedup). + Operation op = (text1.Length > text2.Length) ? + Operation.DELETE : Operation.INSERT; + diffs.Add(new Diff(op, longtext.Substring(0, i))); + diffs.Add(new Diff(Operation.EQUAL, shorttext)); + diffs.Add(new Diff(op, longtext.Substring(i + shorttext.Length))); + return diffs; + } + + if (shorttext.Length == 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + diffs.Add(new Diff(Operation.DELETE, text1)); + diffs.Add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + // Check to see if the problem can be split in two. + string[] hm = diff_halfMatch(text1, text2); + if (hm != null) { + // A half-match was found, sort out the return data. + string text1_a = hm[0]; + string text1_b = hm[1]; + string text2_a = hm[2]; + string text2_b = hm[3]; + string mid_common = hm[4]; + // Send both pairs off for separate processing. + List diffs_a = diff_main(text1_a, text2_a, checklines, deadline); + List diffs_b = diff_main(text1_b, text2_b, checklines, deadline); + // Merge the results. + diffs = diffs_a; + diffs.Add(new Diff(Operation.EQUAL, mid_common)); + diffs.AddRange(diffs_b); + return diffs; + } + + if (checklines && text1.Length > 100 && text2.Length > 100) { + return diff_lineMode(text1, text2, deadline); + } + + return diff_bisect(text1, text2, deadline); + } + + /** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time when the diff should be complete by. + * @return List of Diff objects. + */ + private List diff_lineMode(string text1, string text2, + DateTime deadline) { + // Scan the text on a line-by-line basis first. + Object[] b = diff_linesToChars(text1, text2); + text1 = (string)b[0]; + text2 = (string)b[1]; + List linearray = (List)b[2]; + + List diffs = diff_main(text1, text2, false, deadline); + + // Convert the diff back to original text. + diff_charsToLines(diffs, linearray); + // Eliminate freak matches (e.g. blank lines) + diff_cleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.Add(new Diff(Operation.EQUAL, string.Empty)); + int pointer = 0; + int count_delete = 0; + int count_insert = 0; + string text_delete = string.Empty; + string text_insert = string.Empty; + while (pointer < diffs.Count) { + switch (diffs[pointer].operation) { + case Operation.INSERT: + count_insert++; + text_insert += diffs[pointer].text; + break; + case Operation.DELETE: + count_delete++; + text_delete += diffs[pointer].text; + break; + case Operation.EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + diffs.RemoveRange(pointer - count_delete - count_insert, + count_delete + count_insert); + pointer = pointer - count_delete - count_insert; + List a = + this.diff_main(text_delete, text_insert, false, deadline); + diffs.InsertRange(pointer, a); + pointer = pointer + a.Count; + } + count_insert = 0; + count_delete = 0; + text_delete = string.Empty; + text_insert = string.Empty; + break; + } + pointer++; + } + diffs.RemoveAt(diffs.Count - 1); // Remove the dummy entry at the end. + + return diffs; + } + + /** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time at which to bail if not yet complete. + * @return List of Diff objects. + */ + protected List diff_bisect(string text1, string text2, + DateTime deadline) { + // Cache the text lengths to prevent multiple calls. + int text1_length = text1.Length; + int text2_length = text2.Length; + int max_d = (text1_length + text2_length + 1) / 2; + int v_offset = max_d; + int v_length = 2 * max_d; + int[] v1 = new int[v_length]; + int[] v2 = new int[v_length]; + for (int x = 0; x < v_length; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[v_offset + 1] = 0; + v2[v_offset + 1] = 0; + int delta = text1_length - text2_length; + // If the total number of characters is odd, then the front path will + // collide with the reverse path. + bool front = (delta % 2 != 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + int k1start = 0; + int k1end = 0; + int k2start = 0; + int k2end = 0; + for (int d = 0; d < max_d; d++) { + // Bail out if deadline is reached. + if (DateTime.Now > deadline) { + break; + } + + // Walk the front path one step. + for (int k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + int k1_offset = v_offset + k1; + int x1; + if (k1 == -d || k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1]) { + x1 = v1[k1_offset + 1]; + } else { + x1 = v1[k1_offset - 1] + 1; + } + int y1 = x1 - k1; + while (x1 < text1_length && y1 < text2_length + && text1[x1] == text2[y1]) { + x1++; + y1++; + } + v1[k1_offset] = x1; + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + int k2_offset = v_offset + delta - k1; + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { + // Mirror x2 onto top-left coordinate system. + int x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return diff_bisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + + // Walk the reverse path one step. + for (int k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + int k2_offset = v_offset + k2; + int x2; + if (k2 == -d || k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1]) { + x2 = v2[k2_offset + 1]; + } else { + x2 = v2[k2_offset - 1] + 1; + } + int y2 = x2 - k2; + while (x2 < text1_length && y2 < text2_length + && text1[text1_length - x2 - 1] + == text2[text2_length - y2 - 1]) { + x2++; + y2++; + } + v2[k2_offset] = x2; + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + int k1_offset = v_offset + delta - k2; + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { + int x1 = v1[k1_offset]; + int y1 = v_offset + x1 - k1_offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return diff_bisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + List diffs = new List(); + diffs.Add(new Diff(Operation.DELETE, text1)); + diffs.Add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + /** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param x Index of split point in text1. + * @param y Index of split point in text2. + * @param deadline Time at which to bail if not yet complete. + * @return LinkedList of Diff objects. + */ + private List diff_bisectSplit(string text1, string text2, + int x, int y, DateTime deadline) { + string text1a = text1.Substring(0, x); + string text2a = text2.Substring(0, y); + string text1b = text1.Substring(x); + string text2b = text2.Substring(y); + + // Compute both diffs serially. + List diffs = diff_main(text1a, text2a, false, deadline); + List diffsb = diff_main(text1b, text2b, false, deadline); + + diffs.AddRange(diffsb); + return diffs; + } + + /** + * Split two texts into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text1 First string. + * @param text2 Second string. + * @return Three element Object array, containing the encoded text1, the + * encoded text2 and the List of unique strings. The zeroth element + * of the List of unique strings is intentionally blank. + */ + protected Object[] diff_linesToChars(string text1, string text2) { + List lineArray = new List(); + Dictionary lineHash = new Dictionary(); + // e.g. linearray[4] == "Hello\n" + // e.g. linehash.get("Hello\n") == 4 + + // "\x00" is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray.Add(string.Empty); + + string chars1 = diff_linesToCharsMunge(text1, lineArray, lineHash); + string chars2 = diff_linesToCharsMunge(text2, lineArray, lineHash); + return new Object[] { chars1, chars2, lineArray }; + } + + /** + * Split a text into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text String to encode. + * @param lineArray List of unique strings. + * @param lineHash Map of strings to indices. + * @return Encoded string. + */ + private string diff_linesToCharsMunge(string text, List lineArray, + Dictionary lineHash) { + int lineStart = 0; + int lineEnd = -1; + string line; + StringBuilder chars = new StringBuilder(); + // Walk the text, pulling out a Substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + while (lineEnd < text.Length - 1) { + lineEnd = text.IndexOf('\n', lineStart); + if (lineEnd == -1) { + lineEnd = text.Length - 1; + } + line = text.JavaSubstring(lineStart, lineEnd + 1); + lineStart = lineEnd + 1; + + if (lineHash.ContainsKey(line)) { + chars.Append(((char)(int)lineHash[line])); + } else { + lineArray.Add(line); + lineHash.Add(line, lineArray.Count - 1); + chars.Append(((char)(lineArray.Count - 1))); + } + } + return chars.ToString(); + } + + /** + * Rehydrate the text in a diff from a string of line hashes to real lines + * of text. + * @param diffs List of Diff objects. + * @param lineArray List of unique strings. + */ + protected void diff_charsToLines(ICollection diffs, + List lineArray) { + StringBuilder text; + foreach (Diff diff in diffs) { + text = new StringBuilder(); + for (int y = 0; y < diff.text.Length; y++) { + text.Append(lineArray[diff.text[y]]); + } + diff.text = text.ToString(); + } + } + + /** + * Determine the common prefix of two strings. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the start of each string. + */ + public int diff_commonPrefix(string text1, string text2) { + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + int n = Math.Min(text1.Length, text2.Length); + for (int i = 0; i < n; i++) { + if (text1[i] != text2[i]) { + return i; + } + } + return n; + } + + /** + * Determine the common suffix of two strings. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of each string. + */ + public int diff_commonSuffix(string text1, string text2) { + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + int text1_length = text1.Length; + int text2_length = text2.Length; + int n = Math.Min(text1.Length, text2.Length); + for (int i = 1; i <= n; i++) { + if (text1[text1_length - i] != text2[text2_length - i]) { + return i - 1; + } + } + return n; + } + + /** + * Determine if the suffix of one string is the prefix of another. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of the first + * string and the start of the second string. + */ + protected int diff_commonOverlap(string text1, string text2) { + // Cache the text lengths to prevent multiple calls. + int text1_length = text1.Length; + int text2_length = text2.Length; + // Eliminate the null case. + if (text1_length == 0 || text2_length == 0) { + return 0; + } + // Truncate the longer string. + if (text1_length > text2_length) { + text1 = text1.Substring(text1_length - text2_length); + } else if (text1_length < text2_length) { + text2 = text2.Substring(0, text1_length); + } + int text_length = Math.Min(text1_length, text2_length); + // Quick check for the worst case. + if (text1 == text2) { + return text_length; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: http://neil.fraser.name/news/2010/11/04/ + int best = 0; + int length = 1; + while (true) { + string pattern = text1.Substring(text_length - length); + int found = text2.IndexOf(pattern, StringComparison.Ordinal); + if (found == -1) { + return best; + } + length += found; + if (found == 0 || text1.Substring(text_length - length) == + text2.Substring(0, length)) { + best = length; + length++; + } + } + } + + /** + * Do the two texts share a Substring which is at least half the length of + * the longer text? + * This speedup can produce non-minimal diffs. + * @param text1 First string. + * @param text2 Second string. + * @return Five element String array, containing the prefix of text1, the + * suffix of text1, the prefix of text2, the suffix of text2 and the + * common middle. Or null if there was no match. + */ + + protected string[] diff_halfMatch(string text1, string text2) { + if (this.Diff_Timeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return null; + } + string longtext = text1.Length > text2.Length ? text1 : text2; + string shorttext = text1.Length > text2.Length ? text2 : text1; + if (longtext.Length < 4 || shorttext.Length * 2 < longtext.Length) { + return null; // Pointless. + } + + // First check if the second quarter is the seed for a half-match. + string[] hm1 = diff_halfMatchI(longtext, shorttext, + (longtext.Length + 3) / 4); + // Check again based on the third quarter. + string[] hm2 = diff_halfMatchI(longtext, shorttext, + (longtext.Length + 1) / 2); + string[] hm; + if (hm1 == null && hm2 == null) { + return null; + } else if (hm2 == null) { + hm = hm1; + } else if (hm1 == null) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].Length > hm2[4].Length ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + if (text1.Length > text2.Length) { + return hm; + //return new string[]{hm[0], hm[1], hm[2], hm[3], hm[4]}; + } else { + return new string[] { hm[2], hm[3], hm[0], hm[1], hm[4] }; + } + } + + /** + * Does a Substring of shorttext exist within longtext such that the + * Substring is at least half the length of longtext? + * @param longtext Longer string. + * @param shorttext Shorter string. + * @param i Start index of quarter length Substring within longtext. + * @return Five element string array, containing the prefix of longtext, the + * suffix of longtext, the prefix of shorttext, the suffix of shorttext + * and the common middle. Or null if there was no match. + */ + private string[] diff_halfMatchI(string longtext, string shorttext, int i) { + // Start with a 1/4 length Substring at position i as a seed. + string seed = longtext.Substring(i, longtext.Length / 4); + int j = -1; + string best_common = string.Empty; + string best_longtext_a = string.Empty, best_longtext_b = string.Empty; + string best_shorttext_a = string.Empty, best_shorttext_b = string.Empty; + while (j < shorttext.Length && (j = shorttext.IndexOf(seed, j + 1, + StringComparison.Ordinal)) != -1) { + int prefixLength = diff_commonPrefix(longtext.Substring(i), + shorttext.Substring(j)); + int suffixLength = diff_commonSuffix(longtext.Substring(0, i), + shorttext.Substring(0, j)); + if (best_common.Length < suffixLength + prefixLength) { + best_common = shorttext.Substring(j - suffixLength, suffixLength) + + shorttext.Substring(j, prefixLength); + best_longtext_a = longtext.Substring(0, i - suffixLength); + best_longtext_b = longtext.Substring(i + prefixLength); + best_shorttext_a = shorttext.Substring(0, j - suffixLength); + best_shorttext_b = shorttext.Substring(j + prefixLength); + } + } + if (best_common.Length * 2 >= longtext.Length) { + return new string[]{best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common}; + } else { + return null; + } + } + + /** + * Reduce the number of edits by eliminating semantically trivial + * equalities. + * @param diffs List of Diff objects. + */ + public void diff_cleanupSemantic(List diffs) { + bool changes = false; + // Stack of indices where equalities are found. + Stack equalities = new Stack(); + // Always equal to equalities[equalitiesLength-1][1] + string lastequality = null; + int pointer = 0; // Index of current position. + // Number of characters that changed prior to the equality. + int length_insertions1 = 0; + int length_deletions1 = 0; + // Number of characters that changed after the equality. + int length_insertions2 = 0; + int length_deletions2 = 0; + while (pointer < diffs.Count) { + if (diffs[pointer].operation == Operation.EQUAL) { // Equality found. + equalities.Push(pointer); + length_insertions1 = length_insertions2; + length_deletions1 = length_deletions2; + length_insertions2 = 0; + length_deletions2 = 0; + lastequality = diffs[pointer].text; + } else { // an insertion or deletion + if (diffs[pointer].operation == Operation.INSERT) { + length_insertions2 += diffs[pointer].text.Length; + } else { + length_deletions2 += diffs[pointer].text.Length; + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastequality != null && (lastequality.Length + <= Math.Max(length_insertions1, length_deletions1)) + && (lastequality.Length + <= Math.Max(length_insertions2, length_deletions2))) { + // Duplicate record. + diffs.Insert(equalities.Peek(), + new Diff(Operation.DELETE, lastequality)); + // Change second copy to insert. + diffs[equalities.Peek() + 1].operation = Operation.INSERT; + // Throw away the equality we just deleted. + equalities.Pop(); + if (equalities.Count > 0) { + equalities.Pop(); + } + pointer = equalities.Count > 0 ? equalities.Peek() : -1; + length_insertions1 = 0; // Reset the counters. + length_deletions1 = 0; + length_insertions2 = 0; + length_deletions2 = 0; + lastequality = null; + changes = true; + } + } + pointer++; + } + + // Normalize the diff. + if (changes) { + diff_cleanupMerge(diffs); + } + diff_cleanupSemanticLossless(diffs); + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 1; + while (pointer < diffs.Count) { + if (diffs[pointer - 1].operation == Operation.DELETE && + diffs[pointer].operation == Operation.INSERT) { + string deletion = diffs[pointer - 1].text; + string insertion = diffs[pointer].text; + int overlap_length1 = diff_commonOverlap(deletion, insertion); + int overlap_length2 = diff_commonOverlap(insertion, deletion); + if (overlap_length1 >= overlap_length2) { + if (overlap_length1 >= deletion.Length / 2.0 || + overlap_length1 >= insertion.Length / 2.0) { + // Overlap found. + // Insert an equality and trim the surrounding edits. + diffs.Insert(pointer, new Diff(Operation.EQUAL, + insertion.Substring(0, overlap_length1))); + diffs[pointer - 1].text = + deletion.Substring(0, deletion.Length - overlap_length1); + diffs[pointer + 1].text = insertion.Substring(overlap_length1); + pointer++; + } + } else { + if (overlap_length2 >= deletion.Length / 2.0 || + overlap_length2 >= insertion.Length / 2.0) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + diffs.Insert(pointer, new Diff(Operation.EQUAL, + deletion.Substring(0, overlap_length2))); + diffs[pointer - 1].operation = Operation.INSERT; + diffs[pointer - 1].text = + insertion.Substring(0, insertion.Length - overlap_length2); + diffs[pointer + 1].operation = Operation.DELETE; + diffs[pointer + 1].text = deletion.Substring(overlap_length2); + pointer++; + } + } + pointer++; + } + pointer++; + } + } + + /** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The cat came. -> The cat came. + * @param diffs List of Diff objects. + */ + public void diff_cleanupSemanticLossless(List diffs) { + int pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.Count - 1) { + if (diffs[pointer - 1].operation == Operation.EQUAL && + diffs[pointer + 1].operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + string equality1 = diffs[pointer - 1].text; + string edit = diffs[pointer].text; + string equality2 = diffs[pointer + 1].text; + + // First, shift the edit as far left as possible. + int commonOffset = this.diff_commonSuffix(equality1, edit); + if (commonOffset > 0) { + string commonString = edit.Substring(edit.Length - commonOffset); + equality1 = equality1.Substring(0, equality1.Length - commonOffset); + edit = commonString + edit.Substring(0, edit.Length - commonOffset); + equality2 = commonString + equality2; + } + + // Second, step character by character right, + // looking for the best fit. + string bestEquality1 = equality1; + string bestEdit = edit; + string bestEquality2 = equality2; + int bestScore = diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2); + while (edit.Length != 0 && equality2.Length != 0 + && edit[0] == equality2[0]) { + equality1 += edit[0]; + edit = edit.Substring(1) + equality2[0]; + equality2 = equality2.Substring(1); + int score = diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2); + // The >= encourages trailing rather than leading whitespace on + // edits. + if (score >= bestScore) { + bestScore = score; + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + } + } + + if (diffs[pointer - 1].text != bestEquality1) { + // We have an improvement, save it back to the diff. + if (bestEquality1.Length != 0) { + diffs[pointer - 1].text = bestEquality1; + } else { + diffs.RemoveAt(pointer - 1); + pointer--; + } + diffs[pointer].text = bestEdit; + if (bestEquality2.Length != 0) { + diffs[pointer + 1].text = bestEquality2; + } else { + diffs.RemoveAt(pointer + 1); + pointer--; + } + } + } + pointer++; + } + } + + /** + * Given two strings, comAdde a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 6 (best) to 0 (worst). + * @param one First string. + * @param two Second string. + * @return The score. + */ + private int diff_cleanupSemanticScore(string one, string two) { + if (one.Length == 0 || two.Length == 0) { + // Edges are the best. + return 6; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + char char1 = one[one.Length - 1]; + char char2 = two[0]; + bool nonAlphaNumeric1 = !Char.IsLetterOrDigit(char1); + bool nonAlphaNumeric2 = !Char.IsLetterOrDigit(char2); + bool whitespace1 = nonAlphaNumeric1 && Char.IsWhiteSpace(char1); + bool whitespace2 = nonAlphaNumeric2 && Char.IsWhiteSpace(char2); + bool lineBreak1 = whitespace1 && Char.IsControl(char1); + bool lineBreak2 = whitespace2 && Char.IsControl(char2); + bool blankLine1 = lineBreak1 && BLANKLINEEND.IsMatch(one); + bool blankLine2 = lineBreak2 && BLANKLINESTART.IsMatch(two); + + if (blankLine1 || blankLine2) { + // Five points for blank lines. + return 5; + } else if (lineBreak1 || lineBreak2) { + // Four points for line breaks. + return 4; + } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { + // Three points for end of sentences. + return 3; + } else if (whitespace1 || whitespace2) { + // Two points for whitespace. + return 2; + } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { + // One point for non-alphanumeric. + return 1; + } + return 0; + } + + // Define some regex patterns for matching boundaries. + private Regex BLANKLINEEND = new Regex("\\n\\r?\\n\\Z"); + private Regex BLANKLINESTART = new Regex("\\A\\r?\\n\\r?\\n"); + + /** + * Reduce the number of edits by eliminating operationally trivial + * equalities. + * @param diffs List of Diff objects. + */ + public void diff_cleanupEfficiency(List diffs) { + bool changes = false; + // Stack of indices where equalities are found. + Stack equalities = new Stack(); + // Always equal to equalities[equalitiesLength-1][1] + string lastequality = string.Empty; + int pointer = 0; // Index of current position. + // Is there an insertion operation before the last equality. + bool pre_ins = false; + // Is there a deletion operation before the last equality. + bool pre_del = false; + // Is there an insertion operation after the last equality. + bool post_ins = false; + // Is there a deletion operation after the last equality. + bool post_del = false; + while (pointer < diffs.Count) { + if (diffs[pointer].operation == Operation.EQUAL) { // Equality found. + if (diffs[pointer].text.Length < this.Diff_EditCost + && (post_ins || post_del)) { + // Candidate found. + equalities.Push(pointer); + pre_ins = post_ins; + pre_del = post_del; + lastequality = diffs[pointer].text; + } else { + // Not a candidate, and can never become one. + equalities.Clear(); + lastequality = string.Empty; + } + post_ins = post_del = false; + } else { // An insertion or deletion. + if (diffs[pointer].operation == Operation.DELETE) { + post_del = true; + } else { + post_ins = true; + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if ((lastequality.Length != 0) + && ((pre_ins && pre_del && post_ins && post_del) + || ((lastequality.Length < this.Diff_EditCost / 2) + && ((pre_ins ? 1 : 0) + (pre_del ? 1 : 0) + (post_ins ? 1 : 0) + + (post_del ? 1 : 0)) == 3))) { + // Duplicate record. + diffs.Insert(equalities.Peek(), + new Diff(Operation.DELETE, lastequality)); + // Change second copy to insert. + diffs[equalities.Peek() + 1].operation = Operation.INSERT; + equalities.Pop(); // Throw away the equality we just deleted. + lastequality = string.Empty; + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_ins = post_del = true; + equalities.Clear(); + } else { + if (equalities.Count > 0) { + equalities.Pop(); + } + + pointer = equalities.Count > 0 ? equalities.Peek() : -1; + post_ins = post_del = false; + } + changes = true; + } + } + pointer++; + } + + if (changes) { + diff_cleanupMerge(diffs); + } + } + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param diffs List of Diff objects. + */ + public void diff_cleanupMerge(List diffs) { + // Add a dummy entry at the end. + diffs.Add(new Diff(Operation.EQUAL, string.Empty)); + int pointer = 0; + int count_delete = 0; + int count_insert = 0; + string text_delete = string.Empty; + string text_insert = string.Empty; + int commonlength; + while (pointer < diffs.Count) { + switch (diffs[pointer].operation) { + case Operation.INSERT: + count_insert++; + text_insert += diffs[pointer].text; + pointer++; + break; + case Operation.DELETE: + count_delete++; + text_delete += diffs[pointer].text; + pointer++; + break; + case Operation.EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete + count_insert > 1) { + if (count_delete != 0 && count_insert != 0) { + // Factor out any common prefixies. + commonlength = this.diff_commonPrefix(text_insert, text_delete); + if (commonlength != 0) { + if ((pointer - count_delete - count_insert) > 0 && + diffs[pointer - count_delete - count_insert - 1].operation + == Operation.EQUAL) { + diffs[pointer - count_delete - count_insert - 1].text + += text_insert.Substring(0, commonlength); + } else { + diffs.Insert(0, new Diff(Operation.EQUAL, + text_insert.Substring(0, commonlength))); + pointer++; + } + text_insert = text_insert.Substring(commonlength); + text_delete = text_delete.Substring(commonlength); + } + // Factor out any common suffixies. + commonlength = this.diff_commonSuffix(text_insert, text_delete); + if (commonlength != 0) { + diffs[pointer].text = text_insert.Substring(text_insert.Length + - commonlength) + diffs[pointer].text; + text_insert = text_insert.Substring(0, text_insert.Length + - commonlength); + text_delete = text_delete.Substring(0, text_delete.Length + - commonlength); + } + } + // Delete the offending records and add the merged ones. + if (count_delete == 0) { + diffs.Splice(pointer - count_insert, + count_delete + count_insert, + new Diff(Operation.INSERT, text_insert)); + } else if (count_insert == 0) { + diffs.Splice(pointer - count_delete, + count_delete + count_insert, + new Diff(Operation.DELETE, text_delete)); + } else { + diffs.Splice(pointer - count_delete - count_insert, + count_delete + count_insert, + new Diff(Operation.DELETE, text_delete), + new Diff(Operation.INSERT, text_insert)); + } + pointer = pointer - count_delete - count_insert + + (count_delete != 0 ? 1 : 0) + (count_insert != 0 ? 1 : 0) + 1; + } else if (pointer != 0 + && diffs[pointer - 1].operation == Operation.EQUAL) { + // Merge this equality with the previous one. + diffs[pointer - 1].text += diffs[pointer].text; + diffs.RemoveAt(pointer); + } else { + pointer++; + } + count_insert = 0; + count_delete = 0; + text_delete = string.Empty; + text_insert = string.Empty; + break; + } + } + if (diffs[diffs.Count - 1].text.Length == 0) { + diffs.RemoveAt(diffs.Count - 1); // Remove the dummy entry at the end. + } + + // Second pass: look for single edits surrounded on both sides by + // equalities which can be shifted sideways to eliminate an equality. + // e.g: ABAC -> ABAC + bool changes = false; + pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < (diffs.Count - 1)) { + if (diffs[pointer - 1].operation == Operation.EQUAL && + diffs[pointer + 1].operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + if (diffs[pointer].text.EndsWith(diffs[pointer - 1].text, + StringComparison.Ordinal)) { + // Shift the edit over the previous equality. + diffs[pointer].text = diffs[pointer - 1].text + + diffs[pointer].text.Substring(0, diffs[pointer].text.Length - + diffs[pointer - 1].text.Length); + diffs[pointer + 1].text = diffs[pointer - 1].text + + diffs[pointer + 1].text; + diffs.Splice(pointer - 1, 1); + changes = true; + } else if (diffs[pointer].text.StartsWith(diffs[pointer + 1].text, + StringComparison.Ordinal)) { + // Shift the edit over the next equality. + diffs[pointer - 1].text += diffs[pointer + 1].text; + diffs[pointer].text = + diffs[pointer].text.Substring(diffs[pointer + 1].text.Length) + + diffs[pointer + 1].text; + diffs.Splice(pointer + 1, 1); + changes = true; + } + } + pointer++; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + this.diff_cleanupMerge(diffs); + } + } + + /** + * loc is a location in text1, comAdde and return the equivalent location in + * text2. + * e.g. "The cat" vs "The big cat", 1->1, 5->8 + * @param diffs List of Diff objects. + * @param loc Location within text1. + * @return Location within text2. + */ + public int diff_xIndex(List diffs, int loc) { + int chars1 = 0; + int chars2 = 0; + int last_chars1 = 0; + int last_chars2 = 0; + Diff lastDiff = null; + foreach (Diff aDiff in diffs) { + if (aDiff.operation != Operation.INSERT) { + // Equality or deletion. + chars1 += aDiff.text.Length; + } + if (aDiff.operation != Operation.DELETE) { + // Equality or insertion. + chars2 += aDiff.text.Length; + } + if (chars1 > loc) { + // Overshot the location. + lastDiff = aDiff; + break; + } + last_chars1 = chars1; + last_chars2 = chars2; + } + if (lastDiff != null && lastDiff.operation == Operation.DELETE) { + // The location was deleted. + return last_chars2; + } + // Add the remaining character length. + return last_chars2 + (loc - last_chars1); + } + + /** + * Convert a Diff list into a pretty HTML report. + * @param diffs List of Diff objects. + * @return HTML representation. + */ + public string diff_prettyHtml(List diffs) { + StringBuilder html = new StringBuilder(); + foreach (Diff aDiff in diffs) { + string text = aDiff.text.Replace("&", "&").Replace("<", "<") + .Replace(">", ">").Replace("\n", "¶
"); + switch (aDiff.operation) { + case Operation.INSERT: + html.Append("").Append(text) + .Append(""); + break; + case Operation.DELETE: + html.Append("").Append(text) + .Append(""); + break; + case Operation.EQUAL: + html.Append("").Append(text).Append(""); + break; + } + } + return html.ToString(); + } + + /** + * Compute and return the source text (all equalities and deletions). + * @param diffs List of Diff objects. + * @return Source text. + */ + public string diff_text1(List diffs) { + StringBuilder text = new StringBuilder(); + foreach (Diff aDiff in diffs) { + if (aDiff.operation != Operation.INSERT) { + text.Append(aDiff.text); + } + } + return text.ToString(); + } + + /** + * Compute and return the destination text (all equalities and insertions). + * @param diffs List of Diff objects. + * @return Destination text. + */ + public string diff_text2(List diffs) { + StringBuilder text = new StringBuilder(); + foreach (Diff aDiff in diffs) { + if (aDiff.operation != Operation.DELETE) { + text.Append(aDiff.text); + } + } + return text.ToString(); + } + + /** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * @param diffs List of Diff objects. + * @return Number of changes. + */ + public int diff_levenshtein(List diffs) { + int levenshtein = 0; + int insertions = 0; + int deletions = 0; + foreach (Diff aDiff in diffs) { + switch (aDiff.operation) { + case Operation.INSERT: + insertions += aDiff.text.Length; + break; + case Operation.DELETE: + deletions += aDiff.text.Length; + break; + case Operation.EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += Math.Max(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += Math.Max(insertions, deletions); + return levenshtein; + } + + /** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx + * notation. + * @param diffs Array of Diff objects. + * @return Delta text. + */ + public string diff_toDelta(List diffs) { + StringBuilder text = new StringBuilder(); + foreach (Diff aDiff in diffs) { + switch (aDiff.operation) { + case Operation.INSERT: + text.Append("+").Append(HttpUtility.UrlEncode(aDiff.text, + new UTF8Encoding()).Replace('+', ' ')).Append("\t"); + break; + case Operation.DELETE: + text.Append("-").Append(aDiff.text.Length).Append("\t"); + break; + case Operation.EQUAL: + text.Append("=").Append(aDiff.text.Length).Append("\t"); + break; + } + } + string delta = text.ToString(); + if (delta.Length != 0) { + // Strip off trailing tab character. + delta = delta.Substring(0, delta.Length - 1); + delta = unescapeForEncodeUriCompatability(delta); + } + return delta; + } + + /** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, comAdde the full diff. + * @param text1 Source string for the diff. + * @param delta Delta text. + * @return Array of Diff objects or null if invalid. + * @throws ArgumentException If invalid input. + */ + public List diff_fromDelta(string text1, string delta) { + List diffs = new List(); + int pointer = 0; // Cursor in text1 + string[] tokens = delta.Split(new string[] { "\t" }, + StringSplitOptions.None); + foreach (string token in tokens) { + if (token.Length == 0) { + // Blank tokens are ok (from a trailing \t). + continue; + } + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + string param = token.Substring(1); + switch (token[0]) { + case '+': + // decode would change all "+" to " " + param = param.Replace("+", "%2b"); + + param = HttpUtility.UrlDecode(param, new UTF8Encoding(false, true)); + //} catch (UnsupportedEncodingException e) { + // // Not likely on modern system. + // throw new Error("This system does not support UTF-8.", e); + //} catch (IllegalArgumentException e) { + // // Malformed URI sequence. + // throw new IllegalArgumentException( + // "Illegal escape in diff_fromDelta: " + param, e); + //} + diffs.Add(new Diff(Operation.INSERT, param)); + break; + case '-': + // Fall through. + case '=': + int n; + try { + n = Convert.ToInt32(param); + } catch (FormatException e) { + throw new ArgumentException( + "Invalid number in diff_fromDelta: " + param, e); + } + if (n < 0) { + throw new ArgumentException( + "Negative number in diff_fromDelta: " + param); + } + string text; + try { + text = text1.Substring(pointer, n); + pointer += n; + } catch (ArgumentOutOfRangeException e) { + throw new ArgumentException("Delta length (" + pointer + + ") larger than source text length (" + text1.Length + + ").", e); + } + if (token[0] == '=') { + diffs.Add(new Diff(Operation.EQUAL, text)); + } else { + diffs.Add(new Diff(Operation.DELETE, text)); + } + break; + default: + // Anything else is an error. + throw new ArgumentException( + "Invalid diff operation in diff_fromDelta: " + token[0]); + } + } + if (pointer != text1.Length) { + throw new ArgumentException("Delta length (" + pointer + + ") smaller than source text length (" + text1.Length + ")."); + } + return diffs; + } + + + // MATCH FUNCTIONS + + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + public int match_main(string text, string pattern, int loc) { + // Check for null inputs not needed since null can't be passed in C#. + + loc = Math.Max(0, Math.Min(loc, text.Length)); + if (text == pattern) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (text.Length == 0) { + // Nothing to match. + return -1; + } else if (loc + pattern.Length <= text.Length + && text.Substring(loc, pattern.Length) == pattern) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return loc; + } else { + // Do a fuzzy compare. + return match_bitap(text, pattern, loc); + } + } + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + protected int match_bitap(string text, string pattern, int loc) { + // assert (Match_MaxBits == 0 || pattern.Length <= Match_MaxBits) + // : "Pattern too long for this application."; + + // Initialise the alphabet. + Dictionary s = match_alphabet(pattern); + + // Highest score beyond which we give up. + double score_threshold = Match_Threshold; + // Is there a nearby exact match? (speedup) + int best_loc = text.IndexOf(pattern, loc, StringComparison.Ordinal); + if (best_loc != -1) { + score_threshold = Math.Min(match_bitapScore(0, best_loc, loc, + pattern), score_threshold); + // What about in the other direction? (speedup) + best_loc = text.LastIndexOf(pattern, + Math.Min(loc + pattern.Length, text.Length), + StringComparison.Ordinal); + if (best_loc != -1) { + score_threshold = Math.Min(match_bitapScore(0, best_loc, loc, + pattern), score_threshold); + } + } + + // Initialise the bit arrays. + int matchmask = 1 << (pattern.Length - 1); + best_loc = -1; + + int bin_min, bin_mid; + int bin_max = pattern.Length + text.Length; + // Empty initialization added to appease C# compiler. + int[] last_rd = new int[0]; + for (int d = 0; d < pattern.Length; d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at + // this error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (match_bitapScore(d, loc + bin_mid, loc, pattern) + <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = (bin_max - bin_min) / 2 + bin_min; + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + int start = Math.Max(1, loc - bin_mid + 1); + int finish = Math.Min(loc + bin_mid, text.Length) + pattern.Length; + + int[] rd = new int[finish + 2]; + rd[finish + 1] = (1 << d) - 1; + for (int j = finish; j >= start; j--) { + int charMatch; + if (text.Length <= j - 1 || !s.ContainsKey(text[j - 1])) { + // Out of range. + charMatch = 0; + } else { + charMatch = s[text[j - 1]]; + } + if (d == 0) { + // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { + // Subsequent passes: fuzzy match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch + | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]; + } + if ((rd[j] & matchmask) != 0) { + double score = match_bitapScore(d, j - 1, loc, pattern); + // This match will almost certainly be better than any existing + // match. But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = Math.Max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + if (match_bitapScore(d + 1, loc, loc, pattern) > score_threshold) { + // No hope for a (better) match at greater error levels. + break; + } + last_rd = rd; + } + return best_loc; + } + + /** + * Compute and return the score for a match with e errors and x location. + * @param e Number of errors in match. + * @param x Location of match. + * @param loc Expected location of match. + * @param pattern Pattern being sought. + * @return Overall score for match (0.0 = good, 1.0 = bad). + */ + private double match_bitapScore(int e, int x, int loc, string pattern) { + float accuracy = (float)e / pattern.Length; + int proximity = Math.Abs(loc - x); + if (Match_Distance == 0) { + // Dodge divide by zero error. + return proximity == 0 ? accuracy : 1.0; + } + return accuracy + (proximity / (float) Match_Distance); + } + + /** + * Initialise the alphabet for the Bitap algorithm. + * @param pattern The text to encode. + * @return Hash of character locations. + */ + protected Dictionary match_alphabet(string pattern) { + Dictionary s = new Dictionary(); + char[] char_pattern = pattern.ToCharArray(); + foreach (char c in char_pattern) { + if (!s.ContainsKey(c)) { + s.Add(c, 0); + } + } + int i = 0; + foreach (char c in char_pattern) { + int value = s[c] | (1 << (pattern.Length - i - 1)); + s[c] = value; + i++; + } + return s; + } + + + // PATCH FUNCTIONS + + + /** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * @param patch The patch to grow. + * @param text Source text. + */ + protected void patch_addContext(Patch patch, string text) { + if (text.Length == 0) { + return; + } + string pattern = text.Substring(patch.start2, patch.length1); + int padding = 0; + + // Look for the first and last matches of pattern in text. If two + // different matches are found, increase the pattern length. + while (text.IndexOf(pattern, StringComparison.Ordinal) + != text.LastIndexOf(pattern, StringComparison.Ordinal) + && pattern.Length < Match_MaxBits - Patch_Margin - Patch_Margin) { + padding += Patch_Margin; + pattern = text.JavaSubstring(Math.Max(0, patch.start2 - padding), + Math.Min(text.Length, patch.start2 + patch.length1 + padding)); + } + // Add one chunk for good luck. + padding += Patch_Margin; + + // Add the prefix. + string prefix = text.JavaSubstring(Math.Max(0, patch.start2 - padding), + patch.start2); + if (prefix.Length != 0) { + patch.diffs.Insert(0, new Diff(Operation.EQUAL, prefix)); + } + // Add the suffix. + string suffix = text.JavaSubstring(patch.start2 + patch.length1, + Math.Min(text.Length, patch.start2 + patch.length1 + padding)); + if (suffix.Length != 0) { + patch.diffs.Add(new Diff(Operation.EQUAL, suffix)); + } + + // Roll back the start points. + patch.start1 -= prefix.Length; + patch.start2 -= prefix.Length; + // Extend the lengths. + patch.length1 += prefix.Length + suffix.Length; + patch.length2 += prefix.Length + suffix.Length; + } + + /** + * Compute a list of patches to turn text1 into text2. + * A set of diffs will be computed. + * @param text1 Old text. + * @param text2 New text. + * @return List of Patch objects. + */ + public List patch_make(string text1, string text2) { + // Check for null inputs not needed since null can't be passed in C#. + // No diffs provided, comAdde our own. + List diffs = diff_main(text1, text2, true); + if (diffs.Count > 2) { + diff_cleanupSemantic(diffs); + diff_cleanupEfficiency(diffs); + } + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text1 will be derived from the provided diffs. + * @param diffs Array of Diff objects for text1 to text2. + * @return List of Patch objects. + */ + public List patch_make(List diffs) { + // Check for null inputs not needed since null can't be passed in C#. + // No origin string provided, comAdde our own. + string text1 = diff_text1(diffs); + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is ignored, diffs are the delta between text1 and text2. + * @param text1 Old text + * @param text2 Ignored. + * @param diffs Array of Diff objects for text1 to text2. + * @return List of Patch objects. + * @deprecated Prefer patch_make(string text1, List diffs). + */ + public List patch_make(string text1, string text2, + List diffs) { + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is not provided, diffs are the delta between text1 and text2. + * @param text1 Old text. + * @param diffs Array of Diff objects for text1 to text2. + * @return List of Patch objects. + */ + public List patch_make(string text1, List diffs) { + // Check for null inputs not needed since null can't be passed in C#. + List patches = new List(); + if (diffs.Count == 0) { + return patches; // Get rid of the null case. + } + Patch patch = new Patch(); + int char_count1 = 0; // Number of characters into the text1 string. + int char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + string prepatch_text = text1; + string postpatch_text = text1; + foreach (Diff aDiff in diffs) { + if (patch.diffs.Count == 0 && aDiff.operation != Operation.EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (aDiff.operation) { + case Operation.INSERT: + patch.diffs.Add(aDiff); + patch.length2 += aDiff.text.Length; + postpatch_text = postpatch_text.Insert(char_count2, aDiff.text); + break; + case Operation.DELETE: + patch.length1 += aDiff.text.Length; + patch.diffs.Add(aDiff); + postpatch_text = postpatch_text.Remove(char_count2, + aDiff.text.Length); + break; + case Operation.EQUAL: + if (aDiff.text.Length <= 2 * Patch_Margin + && patch.diffs.Count() != 0 && aDiff != diffs.Last()) { + // Small equality inside a patch. + patch.diffs.Add(aDiff); + patch.length1 += aDiff.text.Length; + patch.length2 += aDiff.text.Length; + } + + if (aDiff.text.Length >= 2 * Patch_Margin) { + // Time for a new patch. + if (patch.diffs.Count != 0) { + patch_addContext(patch, prepatch_text); + patches.Add(patch); + patch = new Patch(); + // Unlike Unidiff, our patch lists have a rolling context. + // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (aDiff.operation != Operation.INSERT) { + char_count1 += aDiff.text.Length; + } + if (aDiff.operation != Operation.DELETE) { + char_count2 += aDiff.text.Length; + } + } + // Pick up the leftover patch if not empty. + if (patch.diffs.Count != 0) { + patch_addContext(patch, prepatch_text); + patches.Add(patch); + } + + return patches; + } + + /** + * Given an array of patches, return another array that is identical. + * @param patches Array of Patch objects. + * @return Array of Patch objects. + */ + public List patch_deepCopy(List patches) { + List patchesCopy = new List(); + foreach (Patch aPatch in patches) { + Patch patchCopy = new Patch(); + foreach (Diff aDiff in aPatch.diffs) { + Diff diffCopy = new Diff(aDiff.operation, aDiff.text); + patchCopy.diffs.Add(diffCopy); + } + patchCopy.start1 = aPatch.start1; + patchCopy.start2 = aPatch.start2; + patchCopy.length1 = aPatch.length1; + patchCopy.length2 = aPatch.length2; + patchesCopy.Add(patchCopy); + } + return patchesCopy; + } + + /** + * Merge a set of patches onto the text. Return a patched text, as well + * as an array of true/false values indicating which patches were applied. + * @param patches Array of Patch objects + * @param text Old text. + * @return Two element Object array, containing the new text and an array of + * bool values. + */ + public Object[] patch_apply(List patches, string text) { + if (patches.Count == 0) { + return new Object[] { text, new bool[0] }; + } + + // Deep copy the patches so that no changes are made to originals. + patches = patch_deepCopy(patches); + + string nullPadding = this.patch_addPadding(patches); + text = nullPadding + text + nullPadding; + patch_splitMax(patches); + + int x = 0; + // delta keeps track of the offset between the expected and actual + // location of the previous patch. If there are patches expected at + // positions 10 and 20, but the first patch was found at 12, delta is 2 + // and the second patch has an effective expected position of 22. + int delta = 0; + bool[] results = new bool[patches.Count]; + foreach (Patch aPatch in patches) { + int expected_loc = aPatch.start2 + delta; + string text1 = diff_text1(aPatch.diffs); + int start_loc; + int end_loc = -1; + if (text1.Length > this.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern + // in the case of a monster delete. + start_loc = match_main(text, + text1.Substring(0, this.Match_MaxBits), expected_loc); + if (start_loc != -1) { + end_loc = match_main(text, + text1.Substring(text1.Length - this.Match_MaxBits), + expected_loc + text1.Length - this.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = this.match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= aPatch.length2 - aPatch.length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + string text2; + if (end_loc == -1) { + text2 = text.JavaSubstring(start_loc, + Math.Min(start_loc + text1.Length, text.Length)); + } else { + text2 = text.JavaSubstring(start_loc, + Math.Min(end_loc + this.Match_MaxBits, text.Length)); + } + if (text1 == text2) { + // Perfect match, just shove the Replacement text in. + text = text.Substring(0, start_loc) + diff_text2(aPatch.diffs) + + text.Substring(start_loc + text1.Length); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + List diffs = diff_main(text1, text2, false); + if (text1.Length > this.Match_MaxBits + && this.diff_levenshtein(diffs) / (float) text1.Length + > this.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + diff_cleanupSemanticLossless(diffs); + int index1 = 0; + foreach (Diff aDiff in aPatch.diffs) { + if (aDiff.operation != Operation.EQUAL) { + int index2 = diff_xIndex(diffs, index1); + if (aDiff.operation == Operation.INSERT) { + // Insertion + text = text.Insert(start_loc + index2, aDiff.text); + } else if (aDiff.operation == Operation.DELETE) { + // Deletion + text = text.Remove(start_loc + index2, diff_xIndex(diffs, + index1 + aDiff.text.Length) - index2); + } + } + if (aDiff.operation != Operation.DELETE) { + index1 += aDiff.text.Length; + } + } + } + } + } + x++; + } + // Strip the padding off. + text = text.Substring(nullPadding.Length, text.Length + - 2 * nullPadding.Length); + return new Object[] { text, results }; + } + + /** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * @param patches Array of Patch objects. + * @return The padding string added to each side. + */ + public string patch_addPadding(List patches) { + short paddingLength = this.Patch_Margin; + string nullPadding = string.Empty; + for (short x = 1; x <= paddingLength; x++) { + nullPadding += (char)x; + } + + // Bump all the patches forward. + foreach (Patch aPatch in patches) { + aPatch.start1 += paddingLength; + aPatch.start2 += paddingLength; + } + + // Add some padding on start of first diff. + Patch patch = patches.First(); + List diffs = patch.diffs; + if (diffs.Count == 0 || diffs.First().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.Insert(0, new Diff(Operation.EQUAL, nullPadding)); + patch.start1 -= paddingLength; // Should be 0. + patch.start2 -= paddingLength; // Should be 0. + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.First().text.Length) { + // Grow first equality. + Diff firstDiff = diffs.First(); + int extraLength = paddingLength - firstDiff.text.Length; + firstDiff.text = nullPadding.Substring(firstDiff.text.Length) + + firstDiff.text; + patch.start1 -= extraLength; + patch.start2 -= extraLength; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + // Add some padding on end of last diff. + patch = patches.Last(); + diffs = patch.diffs; + if (diffs.Count == 0 || diffs.Last().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.Add(new Diff(Operation.EQUAL, nullPadding)); + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.Last().text.Length) { + // Grow last equality. + Diff lastDiff = diffs.Last(); + int extraLength = paddingLength - lastDiff.text.Length; + lastDiff.text += nullPadding.Substring(0, extraLength); + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + return nullPadding; + } + + /** + * Look through the patches and break up any which are longer than the + * maximum limit of the match algorithm. + * Intended to be called only from within patch_apply. + * @param patches List of Patch objects. + */ + public void patch_splitMax(List patches) { + short patch_size = this.Match_MaxBits; + for (int x = 0; x < patches.Count; x++) { + if (patches[x].length1 <= patch_size) { + continue; + } + Patch bigpatch = patches[x]; + // Remove the big old patch. + patches.Splice(x--, 1); + int start1 = bigpatch.start1; + int start2 = bigpatch.start2; + string precontext = string.Empty; + while (bigpatch.diffs.Count != 0) { + // Create one of several smaller patches. + Patch patch = new Patch(); + bool empty = true; + patch.start1 = start1 - precontext.Length; + patch.start2 = start2 - precontext.Length; + if (precontext.Length != 0) { + patch.length1 = patch.length2 = precontext.Length; + patch.diffs.Add(new Diff(Operation.EQUAL, precontext)); + } + while (bigpatch.diffs.Count != 0 + && patch.length1 < patch_size - this.Patch_Margin) { + Operation diff_type = bigpatch.diffs[0].operation; + string diff_text = bigpatch.diffs[0].text; + if (diff_type == Operation.INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.Length; + start2 += diff_text.Length; + patch.diffs.Add(bigpatch.diffs.First()); + bigpatch.diffs.RemoveAt(0); + empty = false; + } else if (diff_type == Operation.DELETE && patch.diffs.Count == 1 + && patch.diffs.First().operation == Operation.EQUAL + && diff_text.Length > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.Length; + start1 += diff_text.Length; + empty = false; + patch.diffs.Add(new Diff(diff_type, diff_text)); + bigpatch.diffs.RemoveAt(0); + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text.Substring(0, Math.Min(diff_text.Length, + patch_size - patch.length1 - Patch_Margin)); + patch.length1 += diff_text.Length; + start1 += diff_text.Length; + if (diff_type == Operation.EQUAL) { + patch.length2 += diff_text.Length; + start2 += diff_text.Length; + } else { + empty = false; + } + patch.diffs.Add(new Diff(diff_type, diff_text)); + if (diff_text == bigpatch.diffs[0].text) { + bigpatch.diffs.RemoveAt(0); + } else { + bigpatch.diffs[0].text = + bigpatch.diffs[0].text.Substring(diff_text.Length); + } + } + } + // Compute the head context for the next patch. + precontext = this.diff_text2(patch.diffs); + precontext = precontext.Substring(Math.Max(0, + precontext.Length - this.Patch_Margin)); + + string postcontext = null; + // Append the end context for this patch. + if (diff_text1(bigpatch.diffs).Length > Patch_Margin) { + postcontext = diff_text1(bigpatch.diffs) + .Substring(0, Patch_Margin); + } else { + postcontext = diff_text1(bigpatch.diffs); + } + + if (postcontext.Length != 0) { + patch.length1 += postcontext.Length; + patch.length2 += postcontext.Length; + if (patch.diffs.Count != 0 + && patch.diffs[patch.diffs.Count - 1].operation + == Operation.EQUAL) { + patch.diffs[patch.diffs.Count - 1].text += postcontext; + } else { + patch.diffs.Add(new Diff(Operation.EQUAL, postcontext)); + } + } + if (!empty) { + patches.Splice(++x, 0, patch); + } + } + } + } + + /** + * Take a list of patches and return a textual representation. + * @param patches List of Patch objects. + * @return Text representation of patches. + */ + public string patch_toText(List patches) { + StringBuilder text = new StringBuilder(); + foreach (Patch aPatch in patches) { + text.Append(aPatch); + } + return text.ToString(); + } + + /** + * Parse a textual representation of patches and return a List of Patch + * objects. + * @param textline Text representation of patches. + * @return List of Patch objects. + * @throws ArgumentException If invalid input. + */ + public List patch_fromText(string textline) { + List patches = new List(); + if (textline.Length == 0) { + return patches; + } + string[] text = textline.Split('\n'); + int textPointer = 0; + Patch patch; + Regex patchHeader + = new Regex("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$"); + Match m; + char sign; + string line; + while (textPointer < text.Length) { + m = patchHeader.Match(text[textPointer]); + if (!m.Success) { + throw new ArgumentException("Invalid patch string: " + + text[textPointer]); + } + patch = new Patch(); + patches.Add(patch); + patch.start1 = Convert.ToInt32(m.Groups[1].Value); + if (m.Groups[2].Length == 0) { + patch.start1--; + patch.length1 = 1; + } else if (m.Groups[2].Value == "0") { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = Convert.ToInt32(m.Groups[2].Value); + } + + patch.start2 = Convert.ToInt32(m.Groups[3].Value); + if (m.Groups[4].Length == 0) { + patch.start2--; + patch.length2 = 1; + } else if (m.Groups[4].Value == "0") { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = Convert.ToInt32(m.Groups[4].Value); + } + textPointer++; + + while (textPointer < text.Length) { + try { + sign = text[textPointer][0]; + } catch (IndexOutOfRangeException) { + // Blank line? Whatever. + textPointer++; + continue; + } + line = text[textPointer].Substring(1); + line = line.Replace("+", "%2b"); + line = HttpUtility.UrlDecode(line, new UTF8Encoding(false, true)); + if (sign == '-') { + // Deletion. + patch.diffs.Add(new Diff(Operation.DELETE, line)); + } else if (sign == '+') { + // Insertion. + patch.diffs.Add(new Diff(Operation.INSERT, line)); + } else if (sign == ' ') { + // Minor equality. + patch.diffs.Add(new Diff(Operation.EQUAL, line)); + } else if (sign == '@') { + // Start of next patch. + break; + } else { + // WTF? + throw new ArgumentException( + "Invalid patch mode '" + sign + "' in: " + line); + } + textPointer++; + } + } + return patches; + } + + /** + * Unescape selected chars for compatability with JavaScript's encodeURI. + * In speed critical applications this could be dropped since the + * receiving application will certainly decode these fine. + * Note that this function is case-sensitive. Thus "%3F" would not be + * unescaped. But this is ok because it is only called with the output of + * HttpUtility.UrlEncode which returns lowercase hex. + * + * Example: "%3f" -> "?", "%24" -> "$", etc. + * + * @param str The string to escape. + * @return The escaped string. + */ + public static string unescapeForEncodeUriCompatability(string str) { + return str.Replace("%21", "!").Replace("%7e", "~") + .Replace("%27", "'").Replace("%28", "(").Replace("%29", ")") + .Replace("%3b", ";").Replace("%2f", "/").Replace("%3f", "?") + .Replace("%3a", ":").Replace("%40", "@").Replace("%26", "&") + .Replace("%3d", "=").Replace("%2b", "+").Replace("%24", "$") + .Replace("%2c", ",").Replace("%23", "#"); + } + } +} diff --git a/csharp/DiffMatchPatchTest.cs b/csharp/DiffMatchPatchTest.cs new file mode 100644 index 0000000..25c8dec --- /dev/null +++ b/csharp/DiffMatchPatchTest.cs @@ -0,0 +1,1179 @@ +/* + * Diff Match and Patch -- Test Harness + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using DiffMatchPatch; +using System.Collections.Generic; +using System; +using System.Text; +using NUnit.Framework; + +namespace nicTest { + [TestFixture()] + public class diff_match_patchTest : diff_match_patch { + [Test()] + public void diff_commonPrefixTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Detect any common suffix. + // Null case. + Assert.AreEqual(0, dmp.diff_commonPrefix("abc", "xyz")); + + // Non-null case. + Assert.AreEqual(4, dmp.diff_commonPrefix("1234abcdef", "1234xyz")); + + // Whole case. + Assert.AreEqual(4, dmp.diff_commonPrefix("1234", "1234xyz")); + } + + [Test()] + public void diff_commonSuffixTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Detect any common suffix. + // Null case. + Assert.AreEqual(0, dmp.diff_commonSuffix("abc", "xyz")); + + // Non-null case. + Assert.AreEqual(4, dmp.diff_commonSuffix("abcdef1234", "xyz1234")); + + // Whole case. + Assert.AreEqual(4, dmp.diff_commonSuffix("1234", "xyz1234")); + } + + [Test()] + public void diff_commonOverlapTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Detect any suffix/prefix overlap. + // Null case. + Assert.AreEqual(0, dmp.diff_commonOverlap("", "abcd")); + + // Whole case. + Assert.AreEqual(3, dmp.diff_commonOverlap("abc", "abcd")); + + // No overlap. + Assert.AreEqual(0, dmp.diff_commonOverlap("123456", "abcd")); + + // Overlap. + Assert.AreEqual(3, dmp.diff_commonOverlap("123456xxx", "xxxabcd")); + + // Unicode. + // Some overly clever languages (C#) may treat ligatures as equal to their + // component letters. E.g. U+FB01 == 'fi' + Assert.AreEqual(0, dmp.diff_commonOverlap("fi", "\ufb01i")); + } + + [Test()] + public void diff_halfmatchTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + dmp.Diff_Timeout = 1; + // No match. + Assert.IsNull(dmp.diff_halfMatch("1234567890", "abcdef")); + + Assert.IsNull(dmp.diff_halfMatch("12345", "23")); + + // Single Match. + CollectionAssert.AreEqual(new string[] { "12", "90", "a", "z", "345678" }, dmp.diff_halfMatch("1234567890", "a345678z")); + + CollectionAssert.AreEqual(new string[] { "a", "z", "12", "90", "345678" }, dmp.diff_halfMatch("a345678z", "1234567890")); + + CollectionAssert.AreEqual(new string[] { "abc", "z", "1234", "0", "56789" }, dmp.diff_halfMatch("abc56789z", "1234567890")); + + CollectionAssert.AreEqual(new string[] { "a", "xyz", "1", "7890", "23456" }, dmp.diff_halfMatch("a23456xyz", "1234567890")); + + // Multiple Matches. + CollectionAssert.AreEqual(new string[] { "12123", "123121", "a", "z", "1234123451234" }, dmp.diff_halfMatch("121231234123451234123121", "a1234123451234z")); + + CollectionAssert.AreEqual(new string[] { "", "-=-=-=-=-=", "x", "", "x-=-=-=-=-=-=-=" }, dmp.diff_halfMatch("x-=-=-=-=-=-=-=-=-=-=-=-=", "xx-=-=-=-=-=-=-=")); + + CollectionAssert.AreEqual(new string[] { "-=-=-=-=-=", "", "", "y", "-=-=-=-=-=-=-=y" }, dmp.diff_halfMatch("-=-=-=-=-=-=-=-=-=-=-=-=y", "-=-=-=-=-=-=-=yy")); + + // Non-optimal halfmatch. + // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy + CollectionAssert.AreEqual(new string[] { "qHillo", "w", "x", "Hulloy", "HelloHe" }, dmp.diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy")); + + // Optimal no halfmatch. + dmp.Diff_Timeout = 0; + Assert.IsNull(dmp.diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy")); + } + + [Test()] + public void diff_linesToCharsTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Convert lines down to characters. + List tmpVector = new List(); + tmpVector.Add(""); + tmpVector.Add("alpha\n"); + tmpVector.Add("beta\n"); + Object[] result = dmp.diff_linesToChars("alpha\nbeta\nalpha\n", "beta\nalpha\nbeta\n"); + Assert.AreEqual("\u0001\u0002\u0001", result[0]); + Assert.AreEqual("\u0002\u0001\u0002", result[1]); + CollectionAssert.AreEqual(tmpVector, (List)result[2]); + + tmpVector.Clear(); + tmpVector.Add(""); + tmpVector.Add("alpha\r\n"); + tmpVector.Add("beta\r\n"); + tmpVector.Add("\r\n"); + result = dmp.diff_linesToChars("", "alpha\r\nbeta\r\n\r\n\r\n"); + Assert.AreEqual("", result[0]); + Assert.AreEqual("\u0001\u0002\u0003\u0003", result[1]); + CollectionAssert.AreEqual(tmpVector, (List)result[2]); + + tmpVector.Clear(); + tmpVector.Add(""); + tmpVector.Add("a"); + tmpVector.Add("b"); + result = dmp.diff_linesToChars("a", "b"); + Assert.AreEqual("\u0001", result[0]); + Assert.AreEqual("\u0002", result[1]); + CollectionAssert.AreEqual(tmpVector, (List)result[2]); + + // More than 256 to reveal any 8-bit limitations. + int n = 300; + tmpVector.Clear(); + StringBuilder lineList = new StringBuilder(); + StringBuilder charList = new StringBuilder(); + for (int x = 1; x < n + 1; x++) { + tmpVector.Add(x + "\n"); + lineList.Append(x + "\n"); + charList.Append(Convert.ToChar(x)); + } + Assert.AreEqual(n, tmpVector.Count); + string lines = lineList.ToString(); + string chars = charList.ToString(); + Assert.AreEqual(n, chars.Length); + tmpVector.Insert(0, ""); + result = dmp.diff_linesToChars(lines, ""); + Assert.AreEqual(chars, result[0]); + Assert.AreEqual("", result[1]); + CollectionAssert.AreEqual(tmpVector, (List)result[2]); + } + + [Test()] + public void diff_charsToLinesTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Convert chars up to lines. + List diffs = new List { + new Diff(Operation.EQUAL, "\u0001\u0002\u0001"), + new Diff(Operation.INSERT, "\u0002\u0001\u0002")}; + List tmpVector = new List(); + tmpVector.Add(""); + tmpVector.Add("alpha\n"); + tmpVector.Add("beta\n"); + dmp.diff_charsToLines(diffs, tmpVector); + CollectionAssert.AreEqual(new List { + new Diff(Operation.EQUAL, "alpha\nbeta\nalpha\n"), + new Diff(Operation.INSERT, "beta\nalpha\nbeta\n")}, diffs); + + // More than 256 to reveal any 8-bit limitations. + int n = 300; + tmpVector.Clear(); + StringBuilder lineList = new StringBuilder(); + StringBuilder charList = new StringBuilder(); + for (int x = 1; x < n + 1; x++) { + tmpVector.Add(x + "\n"); + lineList.Append(x + "\n"); + charList.Append(Convert.ToChar (x)); + } + Assert.AreEqual(n, tmpVector.Count); + string lines = lineList.ToString(); + string chars = charList.ToString(); + Assert.AreEqual(n, chars.Length); + tmpVector.Insert(0, ""); + diffs = new List {new Diff(Operation.DELETE, chars)}; + dmp.diff_charsToLines(diffs, tmpVector); + CollectionAssert.AreEqual(new List + {new Diff(Operation.DELETE, lines)}, diffs); + } + + [Test()] + public void diff_cleanupMergeTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Cleanup a messy diff. + // Null case. + List diffs = new List(); + dmp.diff_cleanupMerge(diffs); + CollectionAssert.AreEqual(new List(), diffs); + + // No change case. + diffs = new List {new Diff(Operation.EQUAL, "a"), new Diff(Operation.DELETE, "b"), new Diff(Operation.INSERT, "c")}; + dmp.diff_cleanupMerge(diffs); + CollectionAssert.AreEqual(new List {new Diff(Operation.EQUAL, "a"), new Diff(Operation.DELETE, "b"), new Diff(Operation.INSERT, "c")}, diffs); + + // Merge equalities. + diffs = new List {new Diff(Operation.EQUAL, "a"), new Diff(Operation.EQUAL, "b"), new Diff(Operation.EQUAL, "c")}; + dmp.diff_cleanupMerge(diffs); + CollectionAssert.AreEqual(new List {new Diff(Operation.EQUAL, "abc")}, diffs); + + // Merge deletions. + diffs = new List {new Diff(Operation.DELETE, "a"), new Diff(Operation.DELETE, "b"), new Diff(Operation.DELETE, "c")}; + dmp.diff_cleanupMerge(diffs); + CollectionAssert.AreEqual(new List {new Diff(Operation.DELETE, "abc")}, diffs); + + // Merge insertions. + diffs = new List {new Diff(Operation.INSERT, "a"), new Diff(Operation.INSERT, "b"), new Diff(Operation.INSERT, "c")}; + dmp.diff_cleanupMerge(diffs); + CollectionAssert.AreEqual(new List {new Diff(Operation.INSERT, "abc")}, diffs); + + // Merge interweave. + diffs = new List {new Diff(Operation.DELETE, "a"), new Diff(Operation.INSERT, "b"), new Diff(Operation.DELETE, "c"), new Diff(Operation.INSERT, "d"), new Diff(Operation.EQUAL, "e"), new Diff(Operation.EQUAL, "f")}; + dmp.diff_cleanupMerge(diffs); + CollectionAssert.AreEqual(new List {new Diff(Operation.DELETE, "ac"), new Diff(Operation.INSERT, "bd"), new Diff(Operation.EQUAL, "ef")}, diffs); + + // Prefix and suffix detection. + diffs = new List {new Diff(Operation.DELETE, "a"), new Diff(Operation.INSERT, "abc"), new Diff(Operation.DELETE, "dc")}; + dmp.diff_cleanupMerge(diffs); + CollectionAssert.AreEqual(new List {new Diff(Operation.EQUAL, "a"), new Diff(Operation.DELETE, "d"), new Diff(Operation.INSERT, "b"), new Diff(Operation.EQUAL, "c")}, diffs); + + // Prefix and suffix detection with equalities. + diffs = new List {new Diff(Operation.EQUAL, "x"), new Diff(Operation.DELETE, "a"), new Diff(Operation.INSERT, "abc"), new Diff(Operation.DELETE, "dc"), new Diff(Operation.EQUAL, "y")}; + dmp.diff_cleanupMerge(diffs); + CollectionAssert.AreEqual(new List {new Diff(Operation.EQUAL, "xa"), new Diff(Operation.DELETE, "d"), new Diff(Operation.INSERT, "b"), new Diff(Operation.EQUAL, "cy")}, diffs); + + // Slide edit left. + diffs = new List {new Diff(Operation.EQUAL, "a"), new Diff(Operation.INSERT, "ba"), new Diff(Operation.EQUAL, "c")}; + dmp.diff_cleanupMerge(diffs); + CollectionAssert.AreEqual(new List {new Diff(Operation.INSERT, "ab"), new Diff(Operation.EQUAL, "ac")}, diffs); + + // Slide edit right. + diffs = new List {new Diff(Operation.EQUAL, "c"), new Diff(Operation.INSERT, "ab"), new Diff(Operation.EQUAL, "a")}; + dmp.diff_cleanupMerge(diffs); + CollectionAssert.AreEqual(new List {new Diff(Operation.EQUAL, "ca"), new Diff(Operation.INSERT, "ba")}, diffs); + + // Slide edit left recursive. + diffs = new List {new Diff(Operation.EQUAL, "a"), new Diff(Operation.DELETE, "b"), new Diff(Operation.EQUAL, "c"), new Diff(Operation.DELETE, "ac"), new Diff(Operation.EQUAL, "x")}; + dmp.diff_cleanupMerge(diffs); + CollectionAssert.AreEqual(new List {new Diff(Operation.DELETE, "abc"), new Diff(Operation.EQUAL, "acx")}, diffs); + + // Slide edit right recursive. + diffs = new List {new Diff(Operation.EQUAL, "x"), new Diff(Operation.DELETE, "ca"), new Diff(Operation.EQUAL, "c"), new Diff(Operation.DELETE, "b"), new Diff(Operation.EQUAL, "a")}; + dmp.diff_cleanupMerge(diffs); + CollectionAssert.AreEqual(new List {new Diff(Operation.EQUAL, "xca"), new Diff(Operation.DELETE, "cba")}, diffs); + } + + [Test()] + public void diff_cleanupSemanticLosslessTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Slide diffs to match logical boundaries. + // Null case. + List diffs = new List(); + dmp.diff_cleanupSemanticLossless(diffs); + CollectionAssert.AreEqual(new List(), diffs); + + // Blank lines. + diffs = new List { + new Diff(Operation.EQUAL, "AAA\r\n\r\nBBB"), + new Diff(Operation.INSERT, "\r\nDDD\r\n\r\nBBB"), + new Diff(Operation.EQUAL, "\r\nEEE") + }; + dmp.diff_cleanupSemanticLossless(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.EQUAL, "AAA\r\n\r\n"), + new Diff(Operation.INSERT, "BBB\r\nDDD\r\n\r\n"), + new Diff(Operation.EQUAL, "BBB\r\nEEE")}, diffs); + + // Line boundaries. + diffs = new List { + new Diff(Operation.EQUAL, "AAA\r\nBBB"), + new Diff(Operation.INSERT, " DDD\r\nBBB"), + new Diff(Operation.EQUAL, " EEE")}; + dmp.diff_cleanupSemanticLossless(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.EQUAL, "AAA\r\n"), + new Diff(Operation.INSERT, "BBB DDD\r\n"), + new Diff(Operation.EQUAL, "BBB EEE")}, diffs); + + // Word boundaries. + diffs = new List { + new Diff(Operation.EQUAL, "The c"), + new Diff(Operation.INSERT, "ow and the c"), + new Diff(Operation.EQUAL, "at.")}; + dmp.diff_cleanupSemanticLossless(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.EQUAL, "The "), + new Diff(Operation.INSERT, "cow and the "), + new Diff(Operation.EQUAL, "cat.")}, diffs); + + // Alphanumeric boundaries. + diffs = new List { + new Diff(Operation.EQUAL, "The-c"), + new Diff(Operation.INSERT, "ow-and-the-c"), + new Diff(Operation.EQUAL, "at.")}; + dmp.diff_cleanupSemanticLossless(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.EQUAL, "The-"), + new Diff(Operation.INSERT, "cow-and-the-"), + new Diff(Operation.EQUAL, "cat.")}, diffs); + + // Hitting the start. + diffs = new List { + new Diff(Operation.EQUAL, "a"), + new Diff(Operation.DELETE, "a"), + new Diff(Operation.EQUAL, "ax")}; + dmp.diff_cleanupSemanticLossless(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.DELETE, "a"), + new Diff(Operation.EQUAL, "aax")}, diffs); + + // Hitting the end. + diffs = new List { + new Diff(Operation.EQUAL, "xa"), + new Diff(Operation.DELETE, "a"), + new Diff(Operation.EQUAL, "a")}; + dmp.diff_cleanupSemanticLossless(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.EQUAL, "xaa"), + new Diff(Operation.DELETE, "a")}, diffs); + + // Sentence boundaries. + diffs = new List { + new Diff(Operation.EQUAL, "The xxx. The "), + new Diff(Operation.INSERT, "zzz. The "), + new Diff(Operation.EQUAL, "yyy.")}; + dmp.diff_cleanupSemanticLossless(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.EQUAL, "The xxx."), + new Diff(Operation.INSERT, " The zzz."), + new Diff(Operation.EQUAL, " The yyy.")}, diffs); + } + + [Test()] + public void diff_cleanupSemanticTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Cleanup semantically trivial equalities. + // Null case. + List diffs = new List(); + dmp.diff_cleanupSemantic(diffs); + CollectionAssert.AreEqual(new List(), diffs); + + // No elimination #1. + diffs = new List { + new Diff(Operation.DELETE, "ab"), + new Diff(Operation.INSERT, "cd"), + new Diff(Operation.EQUAL, "12"), + new Diff(Operation.DELETE, "e")}; + dmp.diff_cleanupSemantic(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.DELETE, "ab"), + new Diff(Operation.INSERT, "cd"), + new Diff(Operation.EQUAL, "12"), + new Diff(Operation.DELETE, "e")}, diffs); + + // No elimination #2. + diffs = new List { + new Diff(Operation.DELETE, "abc"), + new Diff(Operation.INSERT, "ABC"), + new Diff(Operation.EQUAL, "1234"), + new Diff(Operation.DELETE, "wxyz")}; + dmp.diff_cleanupSemantic(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.DELETE, "abc"), + new Diff(Operation.INSERT, "ABC"), + new Diff(Operation.EQUAL, "1234"), + new Diff(Operation.DELETE, "wxyz")}, diffs); + + // Simple elimination. + diffs = new List { + new Diff(Operation.DELETE, "a"), + new Diff(Operation.EQUAL, "b"), + new Diff(Operation.DELETE, "c")}; + dmp.diff_cleanupSemantic(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.DELETE, "abc"), + new Diff(Operation.INSERT, "b")}, diffs); + + // Backpass elimination. + diffs = new List { + new Diff(Operation.DELETE, "ab"), + new Diff(Operation.EQUAL, "cd"), + new Diff(Operation.DELETE, "e"), + new Diff(Operation.EQUAL, "f"), + new Diff(Operation.INSERT, "g")}; + dmp.diff_cleanupSemantic(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.DELETE, "abcdef"), + new Diff(Operation.INSERT, "cdfg")}, diffs); + + // Multiple eliminations. + diffs = new List { + new Diff(Operation.INSERT, "1"), + new Diff(Operation.EQUAL, "A"), + new Diff(Operation.DELETE, "B"), + new Diff(Operation.INSERT, "2"), + new Diff(Operation.EQUAL, "_"), + new Diff(Operation.INSERT, "1"), + new Diff(Operation.EQUAL, "A"), + new Diff(Operation.DELETE, "B"), + new Diff(Operation.INSERT, "2")}; + dmp.diff_cleanupSemantic(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.DELETE, "AB_AB"), + new Diff(Operation.INSERT, "1A2_1A2")}, diffs); + + // Word boundaries. + diffs = new List { + new Diff(Operation.EQUAL, "The c"), + new Diff(Operation.DELETE, "ow and the c"), + new Diff(Operation.EQUAL, "at.")}; + dmp.diff_cleanupSemantic(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.EQUAL, "The "), + new Diff(Operation.DELETE, "cow and the "), + new Diff(Operation.EQUAL, "cat.")}, diffs); + + // No overlap elimination. + diffs = new List { + new Diff(Operation.DELETE, "abcxx"), + new Diff(Operation.INSERT, "xxdef")}; + dmp.diff_cleanupSemantic(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.DELETE, "abcxx"), + new Diff(Operation.INSERT, "xxdef")}, diffs); + + // Overlap elimination. + diffs = new List { + new Diff(Operation.DELETE, "abcxxx"), + new Diff(Operation.INSERT, "xxxdef")}; + dmp.diff_cleanupSemantic(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.DELETE, "abc"), + new Diff(Operation.EQUAL, "xxx"), + new Diff(Operation.INSERT, "def")}, diffs); + + // Reverse overlap elimination. + diffs = new List { + new Diff(Operation.DELETE, "xxxabc"), + new Diff(Operation.INSERT, "defxxx")}; + dmp.diff_cleanupSemantic(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.INSERT, "def"), + new Diff(Operation.EQUAL, "xxx"), + new Diff(Operation.DELETE, "abc")}, diffs); + + // Two overlap eliminations. + diffs = new List { + new Diff(Operation.DELETE, "abcd1212"), + new Diff(Operation.INSERT, "1212efghi"), + new Diff(Operation.EQUAL, "----"), + new Diff(Operation.DELETE, "A3"), + new Diff(Operation.INSERT, "3BC")}; + dmp.diff_cleanupSemantic(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.DELETE, "abcd"), + new Diff(Operation.EQUAL, "1212"), + new Diff(Operation.INSERT, "efghi"), + new Diff(Operation.EQUAL, "----"), + new Diff(Operation.DELETE, "A"), + new Diff(Operation.EQUAL, "3"), + new Diff(Operation.INSERT, "BC")}, diffs); + } + + [Test()] + public void diff_cleanupEfficiencyTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Cleanup operationally trivial equalities. + dmp.Diff_EditCost = 4; + // Null case. + List diffs = new List (); + dmp.diff_cleanupEfficiency(diffs); + CollectionAssert.AreEqual(new List(), diffs); + + // No elimination. + diffs = new List { + new Diff(Operation.DELETE, "ab"), + new Diff(Operation.INSERT, "12"), + new Diff(Operation.EQUAL, "wxyz"), + new Diff(Operation.DELETE, "cd"), + new Diff(Operation.INSERT, "34")}; + dmp.diff_cleanupEfficiency(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.DELETE, "ab"), + new Diff(Operation.INSERT, "12"), + new Diff(Operation.EQUAL, "wxyz"), + new Diff(Operation.DELETE, "cd"), + new Diff(Operation.INSERT, "34")}, diffs); + + // Four-edit elimination. + diffs = new List { + new Diff(Operation.DELETE, "ab"), + new Diff(Operation.INSERT, "12"), + new Diff(Operation.EQUAL, "xyz"), + new Diff(Operation.DELETE, "cd"), + new Diff(Operation.INSERT, "34")}; + dmp.diff_cleanupEfficiency(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.DELETE, "abxyzcd"), + new Diff(Operation.INSERT, "12xyz34")}, diffs); + + // Three-edit elimination. + diffs = new List { + new Diff(Operation.INSERT, "12"), + new Diff(Operation.EQUAL, "x"), + new Diff(Operation.DELETE, "cd"), + new Diff(Operation.INSERT, "34")}; + dmp.diff_cleanupEfficiency(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.DELETE, "xcd"), + new Diff(Operation.INSERT, "12x34")}, diffs); + + // Backpass elimination. + diffs = new List { + new Diff(Operation.DELETE, "ab"), + new Diff(Operation.INSERT, "12"), + new Diff(Operation.EQUAL, "xy"), + new Diff(Operation.INSERT, "34"), + new Diff(Operation.EQUAL, "z"), + new Diff(Operation.DELETE, "cd"), + new Diff(Operation.INSERT, "56")}; + dmp.diff_cleanupEfficiency(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.DELETE, "abxyzcd"), + new Diff(Operation.INSERT, "12xy34z56")}, diffs); + + // High cost elimination. + dmp.Diff_EditCost = 5; + diffs = new List { + new Diff(Operation.DELETE, "ab"), + new Diff(Operation.INSERT, "12"), + new Diff(Operation.EQUAL, "wxyz"), + new Diff(Operation.DELETE, "cd"), + new Diff(Operation.INSERT, "34")}; + dmp.diff_cleanupEfficiency(diffs); + CollectionAssert.AreEqual(new List { + new Diff(Operation.DELETE, "abwxyzcd"), + new Diff(Operation.INSERT, "12wxyz34")}, diffs); + dmp.Diff_EditCost = 4; + } + + [Test()] + public void diff_prettyHtmlTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Pretty print. + List diffs = new List { + new Diff(Operation.EQUAL, "a\n"), + new Diff(Operation.DELETE, "b"), + new Diff(Operation.INSERT, "c&d")}; + Assert.AreEqual("
<B>b</B>c&d", + dmp.diff_prettyHtml(diffs)); + } + + [Test()] + public void diff_textTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Compute the source and destination texts. + List diffs = new List { + new Diff(Operation.EQUAL, "jump"), + new Diff(Operation.DELETE, "s"), + new Diff(Operation.INSERT, "ed"), + new Diff(Operation.EQUAL, " over "), + new Diff(Operation.DELETE, "the"), + new Diff(Operation.INSERT, "a"), + new Diff(Operation.EQUAL, " lazy")}; + Assert.AreEqual("jumps over the lazy", dmp.diff_text1(diffs)); + + Assert.AreEqual("jumped over a lazy", dmp.diff_text2(diffs)); + } + + [Test()] + public void diff_deltaTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Convert a diff into delta string. + List diffs = new List { + new Diff(Operation.EQUAL, "jump"), + new Diff(Operation.DELETE, "s"), + new Diff(Operation.INSERT, "ed"), + new Diff(Operation.EQUAL, " over "), + new Diff(Operation.DELETE, "the"), + new Diff(Operation.INSERT, "a"), + new Diff(Operation.EQUAL, " lazy"), + new Diff(Operation.INSERT, "old dog")}; + string text1 = dmp.diff_text1(diffs); + Assert.AreEqual("jumps over the lazy", text1); + + string delta = dmp.diff_toDelta(diffs); + Assert.AreEqual("=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog", delta); + + // Convert delta string into a diff. + CollectionAssert.AreEqual(diffs, dmp.diff_fromDelta(text1, delta)); + + // Generates error (19 < 20). + try { + dmp.diff_fromDelta(text1 + "x", delta); + Assert.Fail("diff_fromDelta: Too long."); + } catch (ArgumentException) { + // Exception expected. + } + + // Generates error (19 > 18). + try { + dmp.diff_fromDelta(text1.Substring(1), delta); + Assert.Fail("diff_fromDelta: Too short."); + } catch (ArgumentException) { + // Exception expected. + } + + // Generates error (%c3%xy invalid Unicode). + try { + dmp.diff_fromDelta("", "+%c3%xy"); + Assert.Fail("diff_fromDelta: Invalid character."); + } catch (ArgumentException) { + // Exception expected. + } + + // Test deltas with special characters. + char zero = (char)0; + char one = (char)1; + char two = (char)2; + diffs = new List { + new Diff(Operation.EQUAL, "\u0680 " + zero + " \t %"), + new Diff(Operation.DELETE, "\u0681 " + one + " \n ^"), + new Diff(Operation.INSERT, "\u0682 " + two + " \\ |")}; + text1 = dmp.diff_text1(diffs); + Assert.AreEqual("\u0680 " + zero + " \t %\u0681 " + one + " \n ^", text1); + + delta = dmp.diff_toDelta(diffs); + // Lowercase, due to UrlEncode uses lower. + Assert.AreEqual("=7\t-7\t+%da%82 %02 %5c %7c", delta, "diff_toDelta: Unicode."); + + CollectionAssert.AreEqual(diffs, dmp.diff_fromDelta(text1, delta), "diff_fromDelta: Unicode."); + + // Verify pool of unchanged characters. + diffs = new List { + new Diff(Operation.INSERT, "A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ")}; + string text2 = dmp.diff_text2(diffs); + Assert.AreEqual("A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", text2, "diff_text2: Unchanged characters."); + + delta = dmp.diff_toDelta(diffs); + Assert.AreEqual("+A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", delta, "diff_toDelta: Unchanged characters."); + + // Convert delta string into a diff. + CollectionAssert.AreEqual(diffs, dmp.diff_fromDelta("", delta), "diff_fromDelta: Unchanged characters."); + } + + [Test()] + public void diff_xIndexTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Translate a location in text1 to text2. + List diffs = new List { + new Diff(Operation.DELETE, "a"), + new Diff(Operation.INSERT, "1234"), + new Diff(Operation.EQUAL, "xyz")}; + Assert.AreEqual(5, dmp.diff_xIndex(diffs, 2), "diff_xIndex: Translation on equality."); + + diffs = new List { + new Diff(Operation.EQUAL, "a"), + new Diff(Operation.DELETE, "1234"), + new Diff(Operation.EQUAL, "xyz")}; + Assert.AreEqual(1, dmp.diff_xIndex(diffs, 3), "diff_xIndex: Translation on deletion."); + } + + [Test()] + public void diff_levenshteinTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + List diffs = new List { + new Diff(Operation.DELETE, "abc"), + new Diff(Operation.INSERT, "1234"), + new Diff(Operation.EQUAL, "xyz")}; + Assert.AreEqual(4, dmp.diff_levenshtein(diffs), "diff_levenshtein: Levenshtein with trailing equality."); + + diffs = new List { + new Diff(Operation.EQUAL, "xyz"), + new Diff(Operation.DELETE, "abc"), + new Diff(Operation.INSERT, "1234")}; + Assert.AreEqual(4, dmp.diff_levenshtein(diffs), "diff_levenshtein: Levenshtein with leading equality."); + + diffs = new List { + new Diff(Operation.DELETE, "abc"), + new Diff(Operation.EQUAL, "xyz"), + new Diff(Operation.INSERT, "1234")}; + Assert.AreEqual(7, dmp.diff_levenshtein(diffs), "diff_levenshtein: Levenshtein with middle equality."); + } + + [Test()] + public void diff_bisectTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Normal. + string a = "cat"; + string b = "map"; + // Since the resulting diff hasn't been normalized, it would be ok if + // the insertion and deletion pairs are swapped. + // If the order changes, tweak this test as required. + List diffs = new List {new Diff(Operation.DELETE, "c"), new Diff(Operation.INSERT, "m"), new Diff(Operation.EQUAL, "a"), new Diff(Operation.DELETE, "t"), new Diff(Operation.INSERT, "p")}; + CollectionAssert.AreEqual(diffs, dmp.diff_bisect(a, b, DateTime.MaxValue)); + + // Timeout. + diffs = new List {new Diff(Operation.DELETE, "cat"), new Diff(Operation.INSERT, "map")}; + CollectionAssert.AreEqual(diffs, dmp.diff_bisect(a, b, DateTime.MinValue)); + } + + [Test()] + public void diff_mainTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Perform a trivial diff. + List diffs = new List {}; + CollectionAssert.AreEqual(diffs, dmp.diff_main("", "", false), "diff_main: Null case."); + + diffs = new List {new Diff(Operation.EQUAL, "abc")}; + CollectionAssert.AreEqual(diffs, dmp.diff_main("abc", "abc", false), "diff_main: Equality."); + + diffs = new List {new Diff(Operation.EQUAL, "ab"), new Diff(Operation.INSERT, "123"), new Diff(Operation.EQUAL, "c")}; + CollectionAssert.AreEqual(diffs, dmp.diff_main("abc", "ab123c", false), "diff_main: Simple insertion."); + + diffs = new List {new Diff(Operation.EQUAL, "a"), new Diff(Operation.DELETE, "123"), new Diff(Operation.EQUAL, "bc")}; + CollectionAssert.AreEqual(diffs, dmp.diff_main("a123bc", "abc", false), "diff_main: Simple deletion."); + + diffs = new List {new Diff(Operation.EQUAL, "a"), new Diff(Operation.INSERT, "123"), new Diff(Operation.EQUAL, "b"), new Diff(Operation.INSERT, "456"), new Diff(Operation.EQUAL, "c")}; + CollectionAssert.AreEqual(diffs, dmp.diff_main("abc", "a123b456c", false), "diff_main: Two insertions."); + + diffs = new List {new Diff(Operation.EQUAL, "a"), new Diff(Operation.DELETE, "123"), new Diff(Operation.EQUAL, "b"), new Diff(Operation.DELETE, "456"), new Diff(Operation.EQUAL, "c")}; + CollectionAssert.AreEqual(diffs, dmp.diff_main("a123b456c", "abc", false), "diff_main: Two deletions."); + + // Perform a real diff. + // Switch off the timeout. + dmp.Diff_Timeout = 0; + diffs = new List {new Diff(Operation.DELETE, "a"), new Diff(Operation.INSERT, "b")}; + CollectionAssert.AreEqual(diffs, dmp.diff_main("a", "b", false), "diff_main: Simple case #1."); + + diffs = new List {new Diff(Operation.DELETE, "Apple"), new Diff(Operation.INSERT, "Banana"), new Diff(Operation.EQUAL, "s are a"), new Diff(Operation.INSERT, "lso"), new Diff(Operation.EQUAL, " fruit.")}; + CollectionAssert.AreEqual(diffs, dmp.diff_main("Apples are a fruit.", "Bananas are also fruit.", false), "diff_main: Simple case #2."); + + diffs = new List {new Diff(Operation.DELETE, "a"), new Diff(Operation.INSERT, "\u0680"), new Diff(Operation.EQUAL, "x"), new Diff(Operation.DELETE, "\t"), new Diff(Operation.INSERT, new string (new char[]{(char)0}))}; + CollectionAssert.AreEqual(diffs, dmp.diff_main("ax\t", "\u0680x" + (char)0, false), "diff_main: Simple case #3."); + + diffs = new List {new Diff(Operation.DELETE, "1"), new Diff(Operation.EQUAL, "a"), new Diff(Operation.DELETE, "y"), new Diff(Operation.EQUAL, "b"), new Diff(Operation.DELETE, "2"), new Diff(Operation.INSERT, "xab")}; + CollectionAssert.AreEqual(diffs, dmp.diff_main("1ayb2", "abxab", false), "diff_main: Overlap #1."); + + diffs = new List {new Diff(Operation.INSERT, "xaxcx"), new Diff(Operation.EQUAL, "abc"), new Diff(Operation.DELETE, "y")}; + CollectionAssert.AreEqual(diffs, dmp.diff_main("abcy", "xaxcxabc", false), "diff_main: Overlap #2."); + + diffs = new List {new Diff(Operation.DELETE, "ABCD"), new Diff(Operation.EQUAL, "a"), new Diff(Operation.DELETE, "="), new Diff(Operation.INSERT, "-"), new Diff(Operation.EQUAL, "bcd"), new Diff(Operation.DELETE, "="), new Diff(Operation.INSERT, "-"), new Diff(Operation.EQUAL, "efghijklmnopqrs"), new Diff(Operation.DELETE, "EFGHIJKLMNOefg")}; + CollectionAssert.AreEqual(diffs, dmp.diff_main("ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg", "a-bcd-efghijklmnopqrs", false), "diff_main: Overlap #3."); + + diffs = new List {new Diff(Operation.INSERT, " "), new Diff(Operation.EQUAL, "a"), new Diff(Operation.INSERT, "nd"), new Diff(Operation.EQUAL, " [[Pennsylvania]]"), new Diff(Operation.DELETE, " and [[New")}; + CollectionAssert.AreEqual(diffs, dmp.diff_main("a [[Pennsylvania]] and [[New", " and [[Pennsylvania]]", false), "diff_main: Large equality."); + + dmp.Diff_Timeout = 0.1f; // 100ms + string a = "`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n"; + string b = "I am the very model of a modern major general,\nI've information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n"; + // Increase the text lengths by 1024 times to ensure a timeout. + for (int x = 0; x < 10; x++) { + a = a + a; + b = b + b; + } + DateTime startTime = DateTime.Now; + dmp.diff_main(a, b); + DateTime endTime = DateTime.Now; + // Test that we took at least the timeout period. + Assert.IsTrue(new TimeSpan(((long)(dmp.Diff_Timeout * 1000)) * 10000) <= endTime - startTime); + // Test that we didn't take forever (be forgiving). + // Theoretically this test could fail very occasionally if the + // OS task swaps or locks up for a second at the wrong moment. + Assert.IsTrue(new TimeSpan(((long)(dmp.Diff_Timeout * 1000)) * 10000 * 2) > endTime - startTime); + dmp.Diff_Timeout = 0; + + // Test the linemode speedup. + // Must be long to pass the 100 char cutoff. + a = "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n"; + b = "abcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\n"; + CollectionAssert.AreEqual(dmp.diff_main(a, b, true), dmp.diff_main(a, b, false), "diff_main: Simple line-mode."); + + a = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; + b = "abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij"; + CollectionAssert.AreEqual(dmp.diff_main(a, b, true), dmp.diff_main(a, b, false), "diff_main: Single line-mode."); + + a = "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n"; + b = "abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n"; + string[] texts_linemode = diff_rebuildtexts(dmp.diff_main(a, b, true)); + string[] texts_textmode = diff_rebuildtexts(dmp.diff_main(a, b, false)); + CollectionAssert.AreEqual(texts_textmode, texts_linemode, "diff_main: Overlap line-mode."); + + // Test null inputs -- not needed because nulls can't be passed in C#. + } + + [Test()] + public void match_alphabetTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Initialise the bitmasks for Bitap. + Dictionary bitmask = new Dictionary(); + bitmask.Add('a', 4); bitmask.Add('b', 2); bitmask.Add('c', 1); + CollectionAssert.AreEqual(bitmask, dmp.match_alphabet("abc"), "match_alphabet: Unique."); + + bitmask.Clear(); + bitmask.Add('a', 37); bitmask.Add('b', 18); bitmask.Add('c', 8); + CollectionAssert.AreEqual(bitmask, dmp.match_alphabet("abcaba"), "match_alphabet: Duplicates."); + } + + [Test()] + public void match_bitapTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + + // Bitap algorithm. + dmp.Match_Distance = 100; + dmp.Match_Threshold = 0.5f; + Assert.AreEqual(5, dmp.match_bitap("abcdefghijk", "fgh", 5), "match_bitap: Exact match #1."); + + Assert.AreEqual(5, dmp.match_bitap("abcdefghijk", "fgh", 0), "match_bitap: Exact match #2."); + + Assert.AreEqual(4, dmp.match_bitap("abcdefghijk", "efxhi", 0), "match_bitap: Fuzzy match #1."); + + Assert.AreEqual(2, dmp.match_bitap("abcdefghijk", "cdefxyhijk", 5), "match_bitap: Fuzzy match #2."); + + Assert.AreEqual(-1, dmp.match_bitap("abcdefghijk", "bxy", 1), "match_bitap: Fuzzy match #3."); + + Assert.AreEqual(2, dmp.match_bitap("123456789xx0", "3456789x0", 2), "match_bitap: Overflow."); + + Assert.AreEqual(0, dmp.match_bitap("abcdef", "xxabc", 4), "match_bitap: Before start match."); + + Assert.AreEqual(3, dmp.match_bitap("abcdef", "defyy", 4), "match_bitap: Beyond end match."); + + Assert.AreEqual(0, dmp.match_bitap("abcdef", "xabcdefy", 0), "match_bitap: Oversized pattern."); + + dmp.Match_Threshold = 0.4f; + Assert.AreEqual(4, dmp.match_bitap("abcdefghijk", "efxyhi", 1), "match_bitap: Threshold #1."); + + dmp.Match_Threshold = 0.3f; + Assert.AreEqual(-1, dmp.match_bitap("abcdefghijk", "efxyhi", 1), "match_bitap: Threshold #2."); + + dmp.Match_Threshold = 0.0f; + Assert.AreEqual(1, dmp.match_bitap("abcdefghijk", "bcdef", 1), "match_bitap: Threshold #3."); + + dmp.Match_Threshold = 0.5f; + Assert.AreEqual(0, dmp.match_bitap("abcdexyzabcde", "abccde", 3), "match_bitap: Multiple select #1."); + + Assert.AreEqual(8, dmp.match_bitap("abcdexyzabcde", "abccde", 5), "match_bitap: Multiple select #2."); + + dmp.Match_Distance = 10; // Strict location. + Assert.AreEqual(-1, dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24), "match_bitap: Distance test #1."); + + Assert.AreEqual(0, dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdxxefg", 1), "match_bitap: Distance test #2."); + + dmp.Match_Distance = 1000; // Loose location. + Assert.AreEqual(0, dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24), "match_bitap: Distance test #3."); + } + + [Test()] + public void match_mainTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + // Full match. + Assert.AreEqual(0, dmp.match_main("abcdef", "abcdef", 1000), "match_main: Equality."); + + Assert.AreEqual(-1, dmp.match_main("", "abcdef", 1), "match_main: Null text."); + + Assert.AreEqual(3, dmp.match_main("abcdef", "", 3), "match_main: Null pattern."); + + Assert.AreEqual(3, dmp.match_main("abcdef", "de", 3), "match_main: Exact match."); + + Assert.AreEqual(3, dmp.match_main("abcdef", "defy", 4), "match_main: Beyond end match."); + + Assert.AreEqual(0, dmp.match_main("abcdef", "abcdefy", 0), "match_main: Oversized pattern."); + + dmp.Match_Threshold = 0.7f; + Assert.AreEqual(4, dmp.match_main("I am the very model of a modern major general.", " that berry ", 5), "match_main: Complex match."); + dmp.Match_Threshold = 0.5f; + + // Test null inputs -- not needed because nulls can't be passed in C#. + } + + [Test()] + public void patch_patchObjTest() { + // Patch Object. + Patch p = new Patch(); + p.start1 = 20; + p.start2 = 21; + p.length1 = 18; + p.length2 = 17; + p.diffs = new List { + new Diff(Operation.EQUAL, "jump"), + new Diff(Operation.DELETE, "s"), + new Diff(Operation.INSERT, "ed"), + new Diff(Operation.EQUAL, " over "), + new Diff(Operation.DELETE, "the"), + new Diff(Operation.INSERT, "a"), + new Diff(Operation.EQUAL, "\nlaz")}; + string strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0alaz\n"; + Assert.AreEqual(strp, p.ToString(), "Patch: toString."); + } + + [Test()] + public void patch_fromTextTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + Assert.IsTrue(dmp.patch_fromText("").Count == 0, "patch_fromText: #0."); + + string strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0alaz\n"; + Assert.AreEqual(strp, dmp.patch_fromText(strp)[0].ToString(), "patch_fromText: #1."); + + Assert.AreEqual("@@ -1 +1 @@\n-a\n+b\n", dmp.patch_fromText("@@ -1 +1 @@\n-a\n+b\n")[0].ToString(), "patch_fromText: #2."); + + Assert.AreEqual("@@ -1,3 +0,0 @@\n-abc\n", dmp.patch_fromText("@@ -1,3 +0,0 @@\n-abc\n") [0].ToString(), "patch_fromText: #3."); + + Assert.AreEqual("@@ -0,0 +1,3 @@\n+abc\n", dmp.patch_fromText("@@ -0,0 +1,3 @@\n+abc\n") [0].ToString(), "patch_fromText: #4."); + + // Generates error. + try { + dmp.patch_fromText("Bad\nPatch\n"); + Assert.Fail("patch_fromText: #5."); + } catch (ArgumentException) { + // Exception expected. + } + } + + [Test()] + public void patch_toTextTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + string strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n"; + List patches; + patches = dmp.patch_fromText(strp); + string result = dmp.patch_toText(patches); + Assert.AreEqual(strp, result); + + strp = "@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n"; + patches = dmp.patch_fromText(strp); + result = dmp.patch_toText(patches); + Assert.AreEqual(strp, result); + } + + [Test()] + public void patch_addContextTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + dmp.Patch_Margin = 4; + Patch p; + p = dmp.patch_fromText("@@ -21,4 +21,10 @@\n-jump\n+somersault\n") [0]; + dmp.patch_addContext(p, "The quick brown fox jumps over the lazy dog."); + Assert.AreEqual("@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n", p.ToString(), "patch_addContext: Simple case."); + + p = dmp.patch_fromText("@@ -21,4 +21,10 @@\n-jump\n+somersault\n")[0]; + dmp.patch_addContext(p, "The quick brown fox jumps."); + Assert.AreEqual("@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n", p.ToString(), "patch_addContext: Not enough trailing context."); + + p = dmp.patch_fromText("@@ -3 +3,2 @@\n-e\n+at\n")[0]; + dmp.patch_addContext(p, "The quick brown fox jumps."); + Assert.AreEqual("@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n", p.ToString(), "patch_addContext: Not enough leading context."); + + p = dmp.patch_fromText("@@ -3 +3,2 @@\n-e\n+at\n")[0]; + dmp.patch_addContext(p, "The quick brown fox jumps. The quick brown fox crashes."); + Assert.AreEqual("@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n", p.ToString(), "patch_addContext: Ambiguity."); + } + + [Test()] + public void patch_makeTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + List patches; + patches = dmp.patch_make("", ""); + Assert.AreEqual("", dmp.patch_toText(patches), "patch_make: Null case."); + + string text1 = "The quick brown fox jumps over the lazy dog."; + string text2 = "That quick brown fox jumped over a lazy dog."; + string expectedPatch = "@@ -1,8 +1,7 @@\n Th\n-at\n+e\n qui\n@@ -21,17 +21,18 @@\n jump\n-ed\n+s\n over \n-a\n+the\n laz\n"; + // The second patch must be "-21,17 +21,18", not "-22,17 +21,18" due to rolling context. + patches = dmp.patch_make(text2, text1); + Assert.AreEqual(expectedPatch, dmp.patch_toText(patches), "patch_make: Text2+Text1 inputs."); + + expectedPatch = "@@ -1,11 +1,12 @@\n Th\n-e\n+at\n quick b\n@@ -22,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n"; + patches = dmp.patch_make(text1, text2); + Assert.AreEqual(expectedPatch, dmp.patch_toText(patches), "patch_make: Text1+Text2 inputs."); + + List diffs = dmp.diff_main(text1, text2, false); + patches = dmp.patch_make(diffs); + Assert.AreEqual(expectedPatch, dmp.patch_toText(patches), "patch_make: Diff input."); + + patches = dmp.patch_make(text1, diffs); + Assert.AreEqual(expectedPatch, dmp.patch_toText(patches), "patch_make: Text1+Diff inputs."); + + patches = dmp.patch_make(text1, text2, diffs); + Assert.AreEqual(expectedPatch, dmp.patch_toText(patches), "patch_make: Text1+Text2+Diff inputs (deprecated)."); + + patches = dmp.patch_make("`1234567890-=[]\\;',./", "~!@#$%^&*()_+{}|:\"<>?"); + Assert.AreEqual("@@ -1,21 +1,21 @@\n-%601234567890-=%5b%5d%5c;',./\n+~!@#$%25%5e&*()_+%7b%7d%7c:%22%3c%3e?\n", + dmp.patch_toText(patches), + "patch_toText: Character encoding."); + + diffs = new List { + new Diff(Operation.DELETE, "`1234567890-=[]\\;',./"), + new Diff(Operation.INSERT, "~!@#$%^&*()_+{}|:\"<>?")}; + CollectionAssert.AreEqual(diffs, + dmp.patch_fromText("@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n") [0].diffs, + "patch_fromText: Character decoding."); + + text1 = ""; + for (int x = 0; x < 100; x++) { + text1 += "abcdef"; + } + text2 = text1 + "123"; + expectedPatch = "@@ -573,28 +573,31 @@\n cdefabcdefabcdefabcdefabcdef\n+123\n"; + patches = dmp.patch_make(text1, text2); + Assert.AreEqual(expectedPatch, dmp.patch_toText(patches), "patch_make: Long string with repeats."); + + // Test null inputs -- not needed because nulls can't be passed in C#. + } + + [Test()] + public void patch_splitMaxTest() { + // Assumes that Match_MaxBits is 32. + diff_match_patchTest dmp = new diff_match_patchTest(); + List patches; + + patches = dmp.patch_make("abcdefghijklmnopqrstuvwxyz01234567890", "XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0"); + dmp.patch_splitMax(patches); + Assert.AreEqual("@@ -1,32 +1,46 @@\n+X\n ab\n+X\n cd\n+X\n ef\n+X\n gh\n+X\n ij\n+X\n kl\n+X\n mn\n+X\n op\n+X\n qr\n+X\n st\n+X\n uv\n+X\n wx\n+X\n yz\n+X\n 012345\n@@ -25,13 +39,18 @@\n zX01\n+X\n 23\n+X\n 45\n+X\n 67\n+X\n 89\n+X\n 0\n", dmp.patch_toText(patches)); + + patches = dmp.patch_make("abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz", "abcdefuvwxyz"); + string oldToText = dmp.patch_toText(patches); + dmp.patch_splitMax(patches); + Assert.AreEqual(oldToText, dmp.patch_toText(patches)); + + patches = dmp.patch_make("1234567890123456789012345678901234567890123456789012345678901234567890", "abc"); + dmp.patch_splitMax(patches); + Assert.AreEqual("@@ -1,32 +1,4 @@\n-1234567890123456789012345678\n 9012\n@@ -29,32 +1,4 @@\n-9012345678901234567890123456\n 7890\n@@ -57,14 +1,3 @@\n-78901234567890\n+abc\n", dmp.patch_toText(patches)); + + patches = dmp.patch_make("abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1", "abcdefghij , h : 1 , t : 1 abcdefghij , h : 1 , t : 1 abcdefghij , h : 0 , t : 1"); + dmp.patch_splitMax(patches); + Assert.AreEqual("@@ -2,32 +2,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n@@ -29,32 +29,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n", dmp.patch_toText(patches)); + } + + [Test()] + public void patch_addPaddingTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + List patches; + patches = dmp.patch_make("", "test"); + Assert.AreEqual("@@ -0,0 +1,4 @@\n+test\n", + dmp.patch_toText(patches), + "patch_addPadding: Both edges full."); + dmp.patch_addPadding(patches); + Assert.AreEqual("@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n", + dmp.patch_toText(patches), + "patch_addPadding: Both edges full."); + + patches = dmp.patch_make("XY", "XtestY"); + Assert.AreEqual("@@ -1,2 +1,6 @@\n X\n+test\n Y\n", + dmp.patch_toText(patches), + "patch_addPadding: Both edges partial."); + dmp.patch_addPadding(patches); + Assert.AreEqual("@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n", + dmp.patch_toText(patches), + "patch_addPadding: Both edges partial."); + + patches = dmp.patch_make("XXXXYYYY", "XXXXtestYYYY"); + Assert.AreEqual("@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n", + dmp.patch_toText(patches), + "patch_addPadding: Both edges none."); + dmp.patch_addPadding(patches); + Assert.AreEqual("@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n", + dmp.patch_toText(patches), + "patch_addPadding: Both edges none."); + } + + [Test()] + public void patch_applyTest() { + diff_match_patchTest dmp = new diff_match_patchTest(); + dmp.Match_Distance = 1000; + dmp.Match_Threshold = 0.5f; + dmp.Patch_DeleteThreshold = 0.5f; + List patches; + patches = dmp.patch_make("", ""); + Object[] results = dmp.patch_apply(patches, "Hello world."); + bool[] boolArray = (bool[])results[1]; + string resultStr = results[0] + "\t" + boolArray.Length; + Assert.AreEqual("Hello world.\t0", resultStr, "patch_apply: Null case."); + + patches = dmp.patch_make("The quick brown fox jumps over the lazy dog.", "That quick brown fox jumped over a lazy dog."); + results = dmp.patch_apply(patches, "The quick brown fox jumps over the lazy dog."); + boolArray = (bool[])results[1]; + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; + Assert.AreEqual("That quick brown fox jumped over a lazy dog.\tTrue\tTrue", resultStr, "patch_apply: Exact match."); + + results = dmp.patch_apply(patches, "The quick red rabbit jumps over the tired tiger."); + boolArray = (bool[])results[1]; + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; + Assert.AreEqual("That quick red rabbit jumped over a tired tiger.\tTrue\tTrue", resultStr, "patch_apply: Partial match."); + + results = dmp.patch_apply(patches, "I am the very model of a modern major general."); + boolArray = (bool[])results[1]; + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; + Assert.AreEqual("I am the very model of a modern major general.\tFalse\tFalse", resultStr, "patch_apply: Failed match."); + + patches = dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy"); + results = dmp.patch_apply(patches, "x123456789012345678901234567890-----++++++++++-----123456789012345678901234567890y"); + boolArray = (bool[])results[1]; + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; + Assert.AreEqual("xabcy\tTrue\tTrue", resultStr, "patch_apply: Big delete, small change."); + + patches = dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy"); + results = dmp.patch_apply(patches, "x12345678901234567890---------------++++++++++---------------12345678901234567890y"); + boolArray = (bool[])results[1]; + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; + Assert.AreEqual("xabc12345678901234567890---------------++++++++++---------------12345678901234567890y\tFalse\tTrue", resultStr, "patch_apply: Big delete, big change 1."); + + dmp.Patch_DeleteThreshold = 0.6f; + patches = dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy"); + results = dmp.patch_apply(patches, "x12345678901234567890---------------++++++++++---------------12345678901234567890y"); + boolArray = (bool[])results[1]; + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; + Assert.AreEqual("xabcy\tTrue\tTrue", resultStr, "patch_apply: Big delete, big change 2."); + dmp.Patch_DeleteThreshold = 0.5f; + + dmp.Match_Threshold = 0.0f; + dmp.Match_Distance = 0; + patches = dmp.patch_make("abcdefghijklmnopqrstuvwxyz--------------------1234567890", "abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------1234567YYYYYYYYYY890"); + results = dmp.patch_apply(patches, "ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890"); + boolArray = (bool[])results[1]; + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; + Assert.AreEqual("ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890\tFalse\tTrue", resultStr, "patch_apply: Compensate for failed patch."); + dmp.Match_Threshold = 0.5f; + dmp.Match_Distance = 1000; + + patches = dmp.patch_make("", "test"); + string patchStr = dmp.patch_toText(patches); + dmp.patch_apply(patches, ""); + Assert.AreEqual(patchStr, dmp.patch_toText(patches), "patch_apply: No side effects."); + + patches = dmp.patch_make("The quick brown fox jumps over the lazy dog.", "Woof"); + patchStr = dmp.patch_toText(patches); + dmp.patch_apply(patches, "The quick brown fox jumps over the lazy dog."); + Assert.AreEqual(patchStr, dmp.patch_toText(patches), "patch_apply: No side effects with major delete."); + + patches = dmp.patch_make("", "test"); + results = dmp.patch_apply(patches, ""); + boolArray = (bool[])results[1]; + resultStr = results[0] + "\t" + boolArray[0]; + Assert.AreEqual("test\tTrue", resultStr, "patch_apply: Edge exact match."); + + patches = dmp.patch_make("XY", "XtestY"); + results = dmp.patch_apply(patches, "XY"); + boolArray = (bool[])results[1]; + resultStr = results[0] + "\t" + boolArray[0]; + Assert.AreEqual("XtestY\tTrue", resultStr, "patch_apply: Near edge exact match."); + + patches = dmp.patch_make("y", "y123"); + results = dmp.patch_apply(patches, "x"); + boolArray = (bool[])results[1]; + resultStr = results[0] + "\t" + boolArray[0]; + Assert.AreEqual("x123\tTrue", resultStr, "patch_apply: Edge partial match."); + } + + private static string[] diff_rebuildtexts(List diffs) { + string[] text = { "", "" }; + foreach (Diff myDiff in diffs) { + if (myDiff.operation != Operation.INSERT) { + text[0] += myDiff.text; + } + if (myDiff.operation != Operation.DELETE) { + text[1] += myDiff.text; + } + } + return text; + } + } +} diff --git a/dart/DMPClass.dart b/dart/DMPClass.dart new file mode 100644 index 0000000..c460f3c --- /dev/null +++ b/dart/DMPClass.dart @@ -0,0 +1,2117 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Functions for diff, match and patch. + * Computes the difference between two texts to create a patch. + * Applies the patch onto another text, allowing for errors. + * + * @author fraser@google.com (Neil Fraser) + */ + +part of DiffMatchPatch; + +/** + * The data structure representing a diff is a List of Diff objects: + * {Diff(DIFF_DELETE, 'Hello'), Diff(DIFF_INSERT, 'Goodbye'), + * Diff(DIFF_EQUAL, ' world.')} + * which means: delete 'Hello', add 'Goodbye' and keep ' world.' + */ +const DIFF_DELETE = -1; +const DIFF_INSERT = 1; +const DIFF_EQUAL = 0; + +/** + * Class containing the diff, match and patch methods. + * Also contains the behaviour settings. + */ +class DiffMatchPatch { + + // Defaults. + // Set these on your diff_match_patch instance to override the defaults. + + /** + * Number of seconds to map a diff before giving up (0 for infinity). + */ + double Diff_Timeout = 1.0; + /** + * Cost of an empty edit operation in terms of edit characters. + */ + int Diff_EditCost = 4; + /** + * At what point is no match declared (0.0 = perfection, 1.0 = very loose). + */ + double Match_Threshold = 0.5; + /** + * How far to search for a match (0 = exact location, 1000+ = broad match). + * A match this many characters away from the expected location will add + * 1.0 to the score (0.0 is a perfect match). + */ + int Match_Distance = 1000; + /** + * When deleting a large block of text (over ~64 characters), how close do + * the contents have to be to match the expected contents. (0.0 = perfection, + * 1.0 = very loose). Note that Match_Threshold controls how closely the + * end points of a delete need to match. + */ + double Patch_DeleteThreshold = 0.5; + /** + * Chunk size for context length. + */ + int Patch_Margin = 4; + + /** + * The number of bits in an int. + */ + int Match_MaxBits = 32; + + + // DIFF FUNCTIONS + + + /** + * Find the differences between two texts. Simplifies the problem by + * stripping any common prefix or suffix off the texts before diffing. + * [text1] is the old string to be diffed. + * [text2] is the new string to be diffed. + * [checklines] is an optional speedup flag. If present and false, then don't + * run a line-level diff first to identify the changed areas. + * Defaults to true, which does a faster, slightly less optimal diff. + * [deadline] is an optional time when the diff should be complete by. Used + * internally for recursive calls. Users should set DiffTimeout instead. + * Returns a List of Diff objects. + */ + List diff_main(String text1, String text2, + [bool checklines = true, Date deadline]) { + // Set a deadline by which time the diff must be complete. + if (deadline == null) { + deadline = new Date.now(); + if (Diff_Timeout <= 0) { + // One year should be sufficient for 'infinity'. + deadline = deadline.add(new Duration(days: 365)); + } else { + deadline = deadline.add(new Duration( + milliseconds: (Diff_Timeout * 1000).toInt())); + } + } + // Check for null inputs. + if (text1 == null || text2 == null) { + throw new ArgumentError('Null inputs. (diff_main)'); + } + + // Check for equality (speedup). + List diffs; + if (text1 == text2) { + diffs = []; + if (!text1.isEmpty) { + diffs.add(new Diff(DIFF_EQUAL, text1)); + } + return diffs; + } + + // Trim off common prefix (speedup). + int commonlength = diff_commonPrefix(text1, text2); + String commonprefix = text1.substring(0, commonlength); + text1 = text1.substring(commonlength); + text2 = text2.substring(commonlength); + + // Trim off common suffix (speedup). + commonlength = diff_commonSuffix(text1, text2); + String commonsuffix = text1.substring(text1.length - commonlength); + text1 = text1.substring(0, text1.length - commonlength); + text2 = text2.substring(0, text2.length - commonlength); + + // Compute the diff on the middle block. + diffs = _diff_compute(text1, text2, checklines, deadline); + + // Restore the prefix and suffix. + if (!commonprefix.isEmpty) { + diffs.insertRange(0, 1, new Diff(DIFF_EQUAL, commonprefix)); + } + if (!commonsuffix.isEmpty) { + diffs.addLast(new Diff(DIFF_EQUAL, commonsuffix)); + } + + diff_cleanupMerge(diffs); + return diffs; + } + + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * [text1] is the old string to be diffed. + * [text2] is the new string to be diffed. + * [checklines] is a speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * [deadline] is the time when the diff should be complete by. + * Returns a List of Diff objects. + */ + List _diff_compute(String text1, String text2, + bool checklines, Date deadline) { + List diffs = []; + + if (text1.length == 0) { + // Just add some text (speedup). + diffs.add(new Diff(DIFF_INSERT, text2)); + return diffs; + } + + if (text2.length == 0) { + // Just delete some text (speedup). + diffs.add(new Diff(DIFF_DELETE, text1)); + return diffs; + } + + String longtext = text1.length > text2.length ? text1 : text2; + String shorttext = text1.length > text2.length ? text2 : text1; + int i = longtext.indexOf(shorttext); + if (i != -1) { + // Shorter text is inside the longer text (speedup). + int op = (text1.length > text2.length) ? + DIFF_DELETE : DIFF_INSERT; + diffs.add(new Diff(op, longtext.substring(0, i))); + diffs.add(new Diff(DIFF_EQUAL, shorttext)); + diffs.add(new Diff(op, longtext.substring(i + shorttext.length))); + return diffs; + } + + if (shorttext.length == 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + diffs.add(new Diff(DIFF_DELETE, text1)); + diffs.add(new Diff(DIFF_INSERT, text2)); + return diffs; + } + + // Check to see if the problem can be split in two. + final hm = _diff_halfMatch(text1, text2); + if (hm != null) { + // A half-match was found, sort out the return data. + final text1_a = hm[0]; + final text1_b = hm[1]; + final text2_a = hm[2]; + final text2_b = hm[3]; + final mid_common = hm[4]; + // Send both pairs off for separate processing. + final diffs_a = diff_main(text1_a, text2_a, checklines, deadline); + final diffs_b = diff_main(text1_b, text2_b, checklines, deadline); + // Merge the results. + diffs = diffs_a; + diffs.add(new Diff(DIFF_EQUAL, mid_common)); + diffs.addAll(diffs_b); + return diffs; + } + + if (checklines && text1.length > 100 && text2.length > 100) { + return _diff_lineMode(text1, text2, deadline); + } + + return _diff_bisect(text1, text2, deadline); + } + + /** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * [text1] is the old string to be diffed. + * [text2] is the new string to be diffed. + * [deadline] is the time when the diff should be complete by. + * Returns a List of Diff objects. + */ + List _diff_lineMode(String text1, String text2, Date deadline) { + // Scan the text on a line-by-line basis first. + final a = _diff_linesToChars(text1, text2); + text1 = a['chars1']; + text2 = a['chars2']; + final linearray = a['lineArray']; + + final diffs = diff_main(text1, text2, false, deadline); + + // Convert the diff back to original text. + _diff_charsToLines(diffs, linearray); + // Eliminate freak matches (e.g. blank lines) + diff_cleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.add(new Diff(DIFF_EQUAL, '')); + int pointer = 0; + int count_delete = 0; + int count_insert = 0; + final text_delete = new StringBuffer(); + final text_insert = new StringBuffer(); + while (pointer < diffs.length) { + switch (diffs[pointer].operation) { + case DIFF_INSERT: + count_insert++; + text_insert.add(diffs[pointer].text); + break; + case DIFF_DELETE: + count_delete++; + text_delete.add(diffs[pointer].text); + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + diffs.removeRange(pointer - count_delete - count_insert, + count_delete + count_insert); + pointer = pointer - count_delete - count_insert; + final a = diff_main(text_delete.toString(), text_insert.toString(), + false, deadline); + for (int j = a.length - 1; j >= 0; j--) { + diffs.insertRange(pointer, 1, a[j]); + } + pointer = pointer + a.length; + } + count_insert = 0; + count_delete = 0; + text_delete.clear(); + text_insert.clear(); + break; + } + pointer++; + } + diffs.removeLast(); // Remove the dummy entry at the end. + + return diffs; + } + + /** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * [text1] is the old string to be diffed. + * [text2] is the new string to be diffed. + * [deadline] is the time at which to bail if not yet complete. + * Returns a List of Diff objects. + */ + List _diff_bisect(String text1, String text2, Date deadline) { + // Cache the text lengths to prevent multiple calls. + final text1_length = text1.length; + final text2_length = text2.length; + final max_d = (text1_length + text2_length + 1) ~/ 2; + final v_offset = max_d; + final v_length = 2 * max_d; + final v1 = new List(v_length); + final v2 = new List(v_length); + for (int x = 0; x < v_length; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[v_offset + 1] = 0; + v2[v_offset + 1] = 0; + final delta = text1_length - text2_length; + // If the total number of characters is odd, then the front path will + // collide with the reverse path. + final front = (delta % 2 != 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + int k1start = 0; + int k1end = 0; + int k2start = 0; + int k2end = 0; + for (int d = 0; d < max_d; d++) { + // Bail out if deadline is reached. + if ((new Date.now()).compareTo(deadline) == 1) { + break; + } + + // Walk the front path one step. + for (int k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + int k1_offset = v_offset + k1; + int x1; + if (k1 == -d || k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1]) { + x1 = v1[k1_offset + 1]; + } else { + x1 = v1[k1_offset - 1] + 1; + } + int y1 = x1 - k1; + while (x1 < text1_length && y1 < text2_length + && text1[x1] == text2[y1]) { + x1++; + y1++; + } + v1[k1_offset] = x1; + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + int k2_offset = v_offset + delta - k1; + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { + // Mirror x2 onto top-left coordinate system. + int x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return _diff_bisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + + // Walk the reverse path one step. + for (int k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + int k2_offset = v_offset + k2; + int x2; + if (k2 == -d || k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1]) { + x2 = v2[k2_offset + 1]; + } else { + x2 = v2[k2_offset - 1] + 1; + } + int y2 = x2 - k2; + while (x2 < text1_length && y2 < text2_length + && text1[text1_length - x2 - 1] + == text2[text2_length - y2 - 1]) { + x2++; + y2++; + } + v2[k2_offset] = x2; + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + int k1_offset = v_offset + delta - k2; + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { + int x1 = v1[k1_offset]; + int y1 = v_offset + x1 - k1_offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2; + if (x1 >= x2) { + // Overlap detected. + return _diff_bisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + return [new Diff(DIFF_DELETE, text1), new Diff(DIFF_INSERT, text2)]; + } + + /** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * [text1] is the old string to be diffed. + * [text2] is the new string to be diffed. + * [x] is the index of split point in text1. + * [y] is the index of split point in text2. + * [deadline] is the time at which to bail if not yet complete. + * Returns a List of Diff objects. + */ + List _diff_bisectSplit(String text1, String text2, + int x, int y, Date deadline) { + final text1a = text1.substring(0, x); + final text2a = text2.substring(0, y); + final text1b = text1.substring(x); + final text2b = text2.substring(y); + + // Compute both diffs serially. + final diffs = diff_main(text1a, text2a, false, deadline); + final diffsb = diff_main(text1b, text2b, false, deadline); + + diffs.addAll(diffsb); + return diffs; + } + + /** + * Split two texts into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * [text1] is the first string. + * [text2] is the second string. + * Returns a Map containing the encoded text1, the encoded text2 and + * the List of unique strings. The zeroth element of the List of + * unique strings is intentionally blank. + */ + Map _diff_linesToChars(String text1, String text2) { + final lineArray = []; + final lineHash = new HashMap(); + // e.g. linearray[4] == 'Hello\n' + // e.g. linehash['Hello\n'] == 4 + + // '\x00' is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray.add(''); + + String chars1 = _diff_linesToCharsMunge(text1, lineArray, lineHash); + String chars2 = _diff_linesToCharsMunge(text2, lineArray, lineHash); + return {'chars1': chars1, 'chars2': chars2, 'lineArray': lineArray}; + } + + /** + * Split a text into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * [text] is the string to encode. + * [lineArray] is a List of unique strings. + * [lineHash] is a Map of strings to indices. + * Returns an encoded string. + */ + String _diff_linesToCharsMunge(String text, List lineArray, + Map lineHash) { + int lineStart = 0; + int lineEnd = -1; + String line; + final chars = new StringBuffer(); + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + while (lineEnd < text.length - 1) { + lineEnd = text.indexOf('\n', lineStart); + if (lineEnd == -1) { + lineEnd = text.length - 1; + } + line = text.substring(lineStart, lineEnd + 1); + lineStart = lineEnd + 1; + + if (lineHash.containsKey(line)) { + chars.add(new String.fromCharCodes([lineHash[line]])); + } else { + lineArray.add(line); + lineHash[line] = lineArray.length - 1; + chars.add(new String.fromCharCodes([lineArray.length - 1])); + } + } + return chars.toString(); + } + + /** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * [diffs] is a List of Diff objects. + * [lineArray] is a List of unique strings. + */ + void _diff_charsToLines(List diffs, List lineArray) { + final text = new StringBuffer(); + for (Diff diff in diffs) { + for (int y = 0; y < diff.text.length; y++) { + text.add(lineArray[diff.text.charCodeAt(y)]); + } + diff.text = text.toString(); + text.clear(); + } + } + + /** + * Determine the common prefix of two strings + * [text1] is the first string. + * [text2] is the second string. + * Returns the number of characters common to the start of each string. + */ + int diff_commonPrefix(String text1, String text2) { + // TODO: Once Dart's performance stabilizes, determine if linear or binary + // search is better. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + final n = min(text1.length, text2.length); + for (int i = 0; i < n; i++) { + if (text1[i] != text2[i]) { + return i; + } + } + return n; + } + + /** + * Determine the common suffix of two strings + * [text1] is the first string. + * [text2] is the second string. + * Returns the number of characters common to the end of each string. + */ + int diff_commonSuffix(String text1, String text2) { + // TODO: Once Dart's performance stabilizes, determine if linear or binary + // search is better. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + final text1_length = text1.length; + final text2_length = text2.length; + final n = min(text1_length, text2_length); + for (int i = 1; i <= n; i++) { + if (text1[text1_length - i] != text2[text2_length - i]) { + return i - 1; + } + } + return n; + } + + /** + * Determine if the suffix of one string is the prefix of another. + * [text1] is the first string. + * [text2] is the second string. + * Returns the number of characters common to the end of the first + * string and the start of the second string. + */ + int _diff_commonOverlap(String text1, String text2) { + // Eliminate the null case. + if (text1.isEmpty || text2.isEmpty) { + return 0; + } + // Cache the text lengths to prevent multiple calls. + final text1_length = text1.length; + final text2_length = text2.length; + // Truncate the longer string. + if (text1_length > text2_length) { + text1 = text1.substring(text1_length - text2_length); + } else if (text1_length < text2_length) { + text2 = text2.substring(0, text1_length); + } + final text_length = min(text1_length, text2_length); + // Quick check for the worst case. + if (text1 == text2) { + return text_length; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: http://neil.fraser.name/news/2010/11/04/ + int best = 0; + int length = 1; + while (true) { + String pattern = text1.substring(text_length - length); + int found = text2.indexOf(pattern); + if (found == -1) { + return best; + } + length += found; + if (found == 0 || text1.substring(text_length - length) == + text2.substring(0, length)) { + best = length; + length++; + } + } + } + + /** + * Do the two texts share a substring which is at least half the length of + * the longer text? + * This speedup can produce non-minimal diffs. + * [text1] is the first string. + * [text2] is the second string. + * Returns a five element List of Strings, containing the prefix of text1, + * the suffix of text1, the prefix of text2, the suffix of text2 and the + * common middle. Or null if there was no match. + */ + List _diff_halfMatch(String text1, String text2) { + if (Diff_Timeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return null; + } + final longtext = text1.length > text2.length ? text1 : text2; + final shorttext = text1.length > text2.length ? text2 : text1; + if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { + return null; // Pointless. + } + + // First check if the second quarter is the seed for a half-match. + final hm1 = _diff_halfMatchI(longtext, shorttext, + ((longtext.length + 3) / 4).ceil().toInt()); + // Check again based on the third quarter. + final hm2 = _diff_halfMatchI(longtext, shorttext, + ((longtext.length + 1) / 2).ceil().toInt()); + List hm; + if (hm1 == null && hm2 == null) { + return null; + } else if (hm2 == null) { + hm = hm1; + } else if (hm1 == null) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].length > hm2[4].length ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + if (text1.length > text2.length) { + return hm; + //return [hm[0], hm[1], hm[2], hm[3], hm[4]]; + } else { + return [hm[2], hm[3], hm[0], hm[1], hm[4]]; + } + } + + /** + * Does a substring of shorttext exist within longtext such that the + * substring is at least half the length of longtext? + * [longtext] is the longer string. + * [shorttext is the shorter string. + * [i] Start index of quarter length substring within longtext. + * Returns a five element String array, containing the prefix of longtext, + * the suffix of longtext, the prefix of shorttext, the suffix of + * shorttext and the common middle. Or null if there was no match. + */ + List _diff_halfMatchI(String longtext, String shorttext, int i) { + // Start with a 1/4 length substring at position i as a seed. + final seed = longtext.substring(i, + i + (longtext.length / 4).floor().toInt()); + int j = -1; + String best_common = ''; + String best_longtext_a = '', best_longtext_b = ''; + String best_shorttext_a = '', best_shorttext_b = ''; + while ((j = shorttext.indexOf(seed, j + 1)) != -1) { + int prefixLength = diff_commonPrefix(longtext.substring(i), + shorttext.substring(j)); + int suffixLength = diff_commonSuffix(longtext.substring(0, i), + shorttext.substring(0, j)); + if (best_common.length < suffixLength + prefixLength) { + best_common = '${shorttext.substring(j - suffixLength, j)}' + '${shorttext.substring(j, j + prefixLength)}'; + best_longtext_a = longtext.substring(0, i - suffixLength); + best_longtext_b = longtext.substring(i + prefixLength); + best_shorttext_a = shorttext.substring(0, j - suffixLength); + best_shorttext_b = shorttext.substring(j + prefixLength); + } + } + if (best_common.length * 2 >= longtext.length) { + return [best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common]; + } else { + return null; + } + } + + /** + * Reduce the number of edits by eliminating semantically trivial equalities. + * [diffs] is a List of Diff objects. + */ + void diff_cleanupSemantic(List diffs) { + bool changes = false; + // Stack of indices where equalities are found. + final equalities = []; + // Always equal to diffs[equalities.last()].text + String lastequality = null; + int pointer = 0; // Index of current position. + // Number of characters that changed prior to the equality. + int length_insertions1 = 0; + int length_deletions1 = 0; + // Number of characters that changed after the equality. + int length_insertions2 = 0; + int length_deletions2 = 0; + while (pointer < diffs.length) { + if (diffs[pointer].operation == DIFF_EQUAL) { // Equality found. + equalities.addLast(pointer); + length_insertions1 = length_insertions2; + length_deletions1 = length_deletions2; + length_insertions2 = 0; + length_deletions2 = 0; + lastequality = diffs[pointer].text; + } else { // An insertion or deletion. + if (diffs[pointer].operation == DIFF_INSERT) { + length_insertions2 += diffs[pointer].text.length; + } else { + length_deletions2 += diffs[pointer].text.length; + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastequality != null && (lastequality.length + <= max(length_insertions1, length_deletions1)) + && (lastequality.length <= max(length_insertions2, + length_deletions2))) { + // Duplicate record. + diffs.insertRange(equalities.last, 1, + new Diff(DIFF_DELETE, lastequality)); + // Change second copy to insert. + diffs[equalities.last + 1].operation = DIFF_INSERT; + // Throw away the equality we just deleted. + equalities.removeLast(); + // Throw away the previous equality (it needs to be reevaluated). + if (!equalities.isEmpty) { + equalities.removeLast(); + } + pointer = equalities.isEmpty ? -1 : equalities.last; + length_insertions1 = 0; // Reset the counters. + length_deletions1 = 0; + length_insertions2 = 0; + length_deletions2 = 0; + lastequality = null; + changes = true; + } + } + pointer++; + } + + // Normalize the diff. + if (changes) { + diff_cleanupMerge(diffs); + } + _diff_cleanupSemanticLossless(diffs); + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 1; + while (pointer < diffs.length) { + if (diffs[pointer - 1].operation == DIFF_DELETE + && diffs[pointer].operation == DIFF_INSERT) { + String deletion = diffs[pointer - 1].text; + String insertion = diffs[pointer].text; + int overlap_length1 = _diff_commonOverlap(deletion, insertion); + int overlap_length2 = _diff_commonOverlap(insertion, deletion); + if (overlap_length1 >= overlap_length2) { + if (overlap_length1 >= deletion.length / 2 || + overlap_length1 >= insertion.length / 2) { + // Overlap found. + // Insert an equality and trim the surrounding edits. + diffs.insertRange(pointer, 1, + new Diff(DIFF_EQUAL, insertion.substring(0, overlap_length1))); + diffs[pointer - 1].text = + deletion.substring(0, deletion.length - overlap_length1); + diffs[pointer + 1].text = insertion.substring(overlap_length1); + pointer++; + } + } else { + if (overlap_length2 >= deletion.length / 2 || + overlap_length2 >= insertion.length / 2) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + diffs.insertRange(pointer, 1, + new Diff(DIFF_EQUAL, deletion.substring(0, overlap_length2))); + diffs[pointer - 1] = new Diff(DIFF_INSERT, + insertion.substring(0, insertion.length - overlap_length2)); + diffs[pointer + 1] = new Diff(DIFF_DELETE, + deletion.substring(overlap_length2)); + pointer++; + } + } + pointer++; + } + pointer++; + } + } + + /** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The cat came. -> The cat came. + * [diffs] is a List of Diff objects. + */ + void _diff_cleanupSemanticLossless(List diffs) { + /** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 6 (best) to 0 (worst). + * Closure, but does not reference any external variables. + * [one] the first string. + * [two] the second string. + * Returns the score. + */ + int _diff_cleanupSemanticScore(String one, String two) { + if (one.isEmpty || two.isEmpty) { + // Edges are the best. + return 6; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + String char1 = one[one.length - 1]; + String char2 = two[0]; + bool nonAlphaNumeric1 = char1.contains(nonAlphaNumericRegex_); + bool nonAlphaNumeric2 = char2.contains(nonAlphaNumericRegex_); + bool whitespace1 = nonAlphaNumeric1 && char1.contains(whitespaceRegex_); + bool whitespace2 = nonAlphaNumeric2 && char2.contains(whitespaceRegex_); + bool lineBreak1 = whitespace1 && char1.contains(linebreakRegex_); + bool lineBreak2 = whitespace2 && char2.contains(linebreakRegex_); + bool blankLine1 = lineBreak1 && one.contains(blanklineEndRegex_); + bool blankLine2 = lineBreak2 && two.contains(blanklineStartRegex_); + + if (blankLine1 || blankLine2) { + // Five points for blank lines. + return 5; + } else if (lineBreak1 || lineBreak2) { + // Four points for line breaks. + return 4; + } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { + // Three points for end of sentences. + return 3; + } else if (whitespace1 || whitespace2) { + // Two points for whitespace. + return 2; + } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { + // One point for non-alphanumeric. + return 1; + } + return 0; + } + + int pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.length - 1) { + if (diffs[pointer - 1].operation == DIFF_EQUAL + && diffs[pointer + 1].operation == DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + String equality1 = diffs[pointer - 1].text; + String edit = diffs[pointer].text; + String equality2 = diffs[pointer + 1].text; + + // First, shift the edit as far left as possible. + int commonOffset = diff_commonSuffix(equality1, edit); + if (commonOffset != 0) { + String commonString = edit.substring(edit.length - commonOffset); + equality1 = equality1.substring(0, equality1.length - commonOffset); + edit = + '$commonString${edit.substring(0, edit.length - commonOffset)}'; + equality2 = '$commonString$equality2'; + } + + // Second, step character by character right, looking for the best fit. + String bestEquality1 = equality1; + String bestEdit = edit; + String bestEquality2 = equality2; + int bestScore = _diff_cleanupSemanticScore(equality1, edit) + + _diff_cleanupSemanticScore(edit, equality2); + while (!edit.isEmpty && !equality2.isEmpty + && edit[0] == equality2[0]) { + equality1 = '$equality1${edit[0]}'; + edit = '${edit.substring(1)}${equality2[0]}'; + equality2 = equality2.substring(1); + int score = _diff_cleanupSemanticScore(equality1, edit) + + _diff_cleanupSemanticScore(edit, equality2); + // The >= encourages trailing rather than leading whitespace on edits. + if (score >= bestScore) { + bestScore = score; + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + } + } + + if (diffs[pointer - 1].text != bestEquality1) { + // We have an improvement, save it back to the diff. + if (!bestEquality1.isEmpty) { + diffs[pointer - 1].text = bestEquality1; + } else { + diffs.removeRange(pointer - 1, 1); + pointer--; + } + diffs[pointer].text = bestEdit; + if (!bestEquality2.isEmpty) { + diffs[pointer + 1].text = bestEquality2; + } else { + diffs.removeRange(pointer + 1, 1); + pointer--; + } + } + } + pointer++; + } + } + + // Define some regex patterns for matching boundaries. + RegExp nonAlphaNumericRegex_ = new RegExp(r'[^a-zA-Z0-9]'); + RegExp whitespaceRegex_ = new RegExp(r'\s'); + RegExp linebreakRegex_ = new RegExp(r'[\r\n]'); + RegExp blanklineEndRegex_ = new RegExp(r'\n\r?\n$'); + RegExp blanklineStartRegex_ = new RegExp(r'^\r?\n\r?\n'); + + /** + * Reduce the number of edits by eliminating operationally trivial equalities. + * [diffs] is a List of Diff objects. + */ + void diff_cleanupEfficiency(List diffs) { + bool changes = false; + // Stack of indices where equalities are found. + final equalities = []; + // Always equal to diffs[equalities.last()].text + String lastequality = null; + int pointer = 0; // Index of current position. + // Is there an insertion operation before the last equality. + bool pre_ins = false; + // Is there a deletion operation before the last equality. + bool pre_del = false; + // Is there an insertion operation after the last equality. + bool post_ins = false; + // Is there a deletion operation after the last equality. + bool post_del = false; + while (pointer < diffs.length) { + if (diffs[pointer].operation == DIFF_EQUAL) { // Equality found. + if (diffs[pointer].text.length < Diff_EditCost + && (post_ins || post_del)) { + // Candidate found. + equalities.addLast(pointer); + pre_ins = post_ins; + pre_del = post_del; + lastequality = diffs[pointer].text; + } else { + // Not a candidate, and can never become one. + equalities.clear(); + lastequality = null; + } + post_ins = post_del = false; + } else { // An insertion or deletion. + if (diffs[pointer].operation == DIFF_DELETE) { + post_del = true; + } else { + post_ins = true; + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if (lastequality != null + && ((pre_ins && pre_del && post_ins && post_del) + || ((lastequality.length < Diff_EditCost / 2) + && ((pre_ins ? 1 : 0) + (pre_del ? 1 : 0) + (post_ins ? 1 : 0) + + (post_del ? 1 : 0)) == 3))) { + // Duplicate record. + diffs.insertRange(equalities.last, 1, + new Diff(DIFF_DELETE, lastequality)); + // Change second copy to insert. + diffs[equalities.last + 1].operation = DIFF_INSERT; + equalities.removeLast(); // Throw away the equality we just deleted. + lastequality = null; + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_ins = post_del = true; + equalities.clear(); + } else { + if (!equalities.isEmpty) { + equalities.removeLast(); + } + pointer = equalities.isEmpty ? -1 : equalities.last; + post_ins = post_del = false; + } + changes = true; + } + } + pointer++; + } + + if (changes) { + diff_cleanupMerge(diffs); + } + } + + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * [diffs] is a List of Diff objects. + */ + void diff_cleanupMerge(List diffs) { + diffs.addLast(new Diff(DIFF_EQUAL, '')); // Add a dummy entry at the end. + int pointer = 0; + int count_delete = 0; + int count_insert = 0; + String text_delete = ''; + String text_insert = ''; + int commonlength; + while (pointer < diffs.length) { + switch (diffs[pointer].operation) { + case DIFF_INSERT: + count_insert++; + text_insert = '$text_insert${diffs[pointer].text}'; + pointer++; + break; + case DIFF_DELETE: + count_delete++; + text_delete = '$text_delete${diffs[pointer].text}'; + pointer++; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete + count_insert > 1) { + if (count_delete != 0 && count_insert != 0) { + // Factor out any common prefixies. + commonlength = diff_commonPrefix(text_insert, text_delete); + if (commonlength != 0) { + if ((pointer - count_delete - count_insert) > 0 + && diffs[pointer - count_delete - count_insert - 1] + .operation == DIFF_EQUAL) { + final i = pointer - count_delete - count_insert - 1; + diffs[i].text = '${diffs[i].text}' + '${text_insert.substring(0, commonlength)}'; + } else { + diffs.insertRange(0, 1, new Diff(DIFF_EQUAL, + text_insert.substring(0, commonlength))); + pointer++; + } + text_insert = text_insert.substring(commonlength); + text_delete = text_delete.substring(commonlength); + } + // Factor out any common suffixies. + commonlength = diff_commonSuffix(text_insert, text_delete); + if (commonlength != 0) { + diffs[pointer].text = + '${text_insert.substring(text_insert.length + - commonlength)}${diffs[pointer].text}'; + text_insert = text_insert.substring(0, text_insert.length + - commonlength); + text_delete = text_delete.substring(0, text_delete.length + - commonlength); + } + } + // Delete the offending records and add the merged ones. + if (count_delete == 0) { + diffs.removeRange(pointer - count_insert, count_insert); + diffs.insertRange(pointer - count_insert, 1, + new Diff(DIFF_INSERT, text_insert)); + } else if (count_insert == 0) { + diffs.removeRange(pointer - count_delete, count_delete); + diffs.insertRange(pointer - count_delete, 1, + new Diff(DIFF_DELETE, text_delete)); + } else { + diffs.removeRange(pointer - count_delete - count_insert, + count_delete + count_insert); + diffs.insertRange(pointer - count_delete - count_insert, 1, + new Diff(DIFF_INSERT, text_insert)); + diffs.insertRange(pointer - count_delete - count_insert, 1, + new Diff(DIFF_DELETE, text_delete)); + } + pointer = pointer - count_delete - count_insert + + (count_delete == 0 ? 0 : 1) + + (count_insert == 0 ? 0 : 1) + 1; + } else if (pointer != 0 && diffs[pointer - 1].operation + == DIFF_EQUAL) { + // Merge this equality with the previous one. + diffs[pointer - 1].text = + '${diffs[pointer - 1].text}${diffs[pointer].text}'; + diffs.removeRange(pointer, 1); + } else { + pointer++; + } + count_insert = 0; + count_delete = 0; + text_delete = ''; + text_insert = ''; + break; + } + } + if (diffs.last.text.isEmpty) { + diffs.removeLast(); // Remove the dummy entry at the end. + } + + // Second pass: look for single edits surrounded on both sides by equalities + // which can be shifted sideways to eliminate an equality. + // e.g: ABAC -> ABAC + bool changes = false; + pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.length - 1) { + if (diffs[pointer - 1].operation == DIFF_EQUAL + && diffs[pointer + 1].operation == DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + if (diffs[pointer].text.endsWith(diffs[pointer - 1].text)) { + // Shift the edit over the previous equality. + diffs[pointer].text = '${diffs[pointer - 1].text}' + '${diffs[pointer].text.substring(0, + diffs[pointer].text.length - diffs[pointer - 1].text.length)}'; + diffs[pointer + 1].text = + '${diffs[pointer - 1].text}${diffs[pointer + 1].text}'; + diffs.removeRange(pointer - 1, 1); + changes = true; + } else if (diffs[pointer].text.startsWith(diffs[pointer + 1].text)) { + // Shift the edit over the next equality. + diffs[pointer - 1].text = + '${diffs[pointer - 1].text}${diffs[pointer + 1].text}'; + diffs[pointer].text = + '${diffs[pointer].text.substring(diffs[pointer + 1].text.length)}' + '${diffs[pointer + 1].text}'; + diffs.removeRange(pointer + 1, 1); + changes = true; + } + } + pointer++; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + diff_cleanupMerge(diffs); + } + } + + /** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. "The cat" vs "The big cat", 1->1, 5->8 + * [diffs] is a List of Diff objects. + * [loc] is the location within text1. + * Returns the location within text2. + */ + int diff_xIndex(List diffs, int loc) { + int chars1 = 0; + int chars2 = 0; + int last_chars1 = 0; + int last_chars2 = 0; + Diff lastDiff = null; + for (Diff aDiff in diffs) { + if (aDiff.operation != DIFF_INSERT) { + // Equality or deletion. + chars1 += aDiff.text.length; + } + if (aDiff.operation != DIFF_DELETE) { + // Equality or insertion. + chars2 += aDiff.text.length; + } + if (chars1 > loc) { + // Overshot the location. + lastDiff = aDiff; + break; + } + last_chars1 = chars1; + last_chars2 = chars2; + } + if (lastDiff != null && lastDiff.operation == DIFF_DELETE) { + // The location was deleted. + return last_chars2; + } + // Add the remaining character length. + return last_chars2 + (loc - last_chars1); + } + + /** + * Convert a Diff list into a pretty HTML report. + * [diffs] is a List of Diff objects. + * Returns an HTML representation. + */ + String diff_prettyHtml(List diffs) { + final html = new StringBuffer(); + for (Diff aDiff in diffs) { + String text = aDiff.text.replaceAll('&', '&').replaceAll('<', '<') + .replaceAll('>', '>').replaceAll('\n', '¶
'); + switch (aDiff.operation) { + case DIFF_INSERT: + html.add('').add(text) + .add(''); + break; + case DIFF_DELETE: + html.add('').add(text) + .add(''); + break; + case DIFF_EQUAL: + html.add('').add(text).add(''); + break; + } + } + return html.toString(); + } + + /** + * Compute and return the source text (all equalities and deletions). + * [diffs] is a List of Diff objects. + * Returns the source text. + */ + String diff_text1(List diffs) { + final text = new StringBuffer(); + for (Diff aDiff in diffs) { + if (aDiff.operation != DIFF_INSERT) { + text.add(aDiff.text); + } + } + return text.toString(); + } + + /** + * Compute and return the destination text (all equalities and insertions). + * [diffs] is a List of Diff objects. + * Returns the destination text. + */ + String diff_text2(List diffs) { + final text = new StringBuffer(); + for (Diff aDiff in diffs) { + if (aDiff.operation != DIFF_DELETE) { + text.add(aDiff.text); + } + } + return text.toString(); + } + + /** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * [diffs] is a List of Diff objects. + * Returns the number of changes. + */ + int diff_levenshtein(List diffs) { + int levenshtein = 0; + int insertions = 0; + int deletions = 0; + for (Diff aDiff in diffs) { + switch (aDiff.operation) { + case DIFF_INSERT: + insertions += aDiff.text.length; + break; + case DIFF_DELETE: + deletions += aDiff.text.length; + break; + case DIFF_EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += max(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += max(insertions, deletions); + return levenshtein; + } + + /** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx notation. + * [diffs] is a List of Diff objects. + * Returns the delta text. + */ + String diff_toDelta(List diffs) { + final text = new StringBuffer(); + for (Diff aDiff in diffs) { + switch (aDiff.operation) { + case DIFF_INSERT: + text.add('+').add(encodeUri(aDiff.text)).add('\t'); + break; + case DIFF_DELETE: + text.add('-').add(aDiff.text.length).add('\t'); + break; + case DIFF_EQUAL: + text.add('=').add(aDiff.text.length).add('\t'); + break; + } + } + String delta = text.toString(); + if (!delta.isEmpty) { + // Strip off trailing tab character. + delta = delta.substring(0, delta.length - 1); + } + return delta.replaceAll('%20', ' '); + } + + /** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, compute the full diff. + * [text1] is the source string for the diff. + * [delta] is the delta text. + * Returns a List of Diff objects or null if invalid. + * Throws ArgumentError if invalid input. + */ + List diff_fromDelta(String text1, String delta) { + final diffs = []; + int pointer = 0; // Cursor in text1 + final tokens = delta.split('\t'); + for (String token in tokens) { + if (token.length == 0) { + // Blank tokens are ok (from a trailing \t). + continue; + } + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + String param = token.substring(1); + switch (token[0]) { + case '+': + // decode would change all "+" to " " + param = param.replaceAll('+', '%2B'); + try { + param = decodeUri(param); + } on ArgumentError catch (e) { + // Malformed URI sequence. + throw new ArgumentError( + 'Illegal escape in diff_fromDelta: $param'); + } + diffs.add(new Diff(DIFF_INSERT, param)); + break; + case '-': + // Fall through. + case '=': + int n; + try { + n = int.parse(param); + } on FormatException catch (e) { + throw new ArgumentError( + 'Invalid number in diff_fromDelta: $param'); + } + if (n < 0) { + throw new ArgumentError( + 'Negative number in diff_fromDelta: $param'); + } + String text; + try { + text = text1.substring(pointer, pointer += n); + } on RangeError catch (e) { + throw new ArgumentError('Delta length ($pointer)' + ' larger than source text length (${text1.length}).'); + } + if (token[0] == '=') { + diffs.add(new Diff(DIFF_EQUAL, text)); + } else { + diffs.add(new Diff(DIFF_DELETE, text)); + } + break; + default: + // Anything else is an error. + throw new ArgumentError( + 'Invalid diff operation in diff_fromDelta: ${token[0]}'); + } + } + if (pointer != text1.length) { + throw new ArgumentError('Delta length ($pointer)' + ' smaller than source text length (${text1.length}).'); + } + return diffs; + } + + + // MATCH FUNCTIONS + + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * Returns -1 if no match found. + * [text] is the text to search. + * [pattern] is the pattern to search for. + * [loc] is the location to search around. + * Returns the best match index or -1. + */ + int match_main(String text, String pattern, int loc) { + // Check for null inputs. + if (text == null || pattern == null) { + throw new ArgumentError('Null inputs. (match_main)'); + } + + loc = max(0, min(loc, text.length)); + if (text == pattern) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (text.length == 0) { + // Nothing to match. + return -1; + } else if (loc + pattern.length <= text.length + && text.substring(loc, loc + pattern.length) == pattern) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return loc; + } else { + // Do a fuzzy compare. + return _match_bitap(text, pattern, loc); + } + } + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. Returns -1 if no match found. + * [text] is the the text to search. + * [pattern] is the pattern to search for. + * [loc] is the location to search around. + * Returns the best match index or -1. + */ + int _match_bitap(String text, String pattern, int loc) { + Expect.isTrue(Match_MaxBits == 0 || pattern.length <= Match_MaxBits, + 'Pattern too long for this application.'); + + // Initialise the alphabet. + Map s = _match_alphabet(pattern); + + // Highest score beyond which we give up. + double score_threshold = Match_Threshold; + // Is there a nearby exact match? (speedup) + int best_loc = text.indexOf(pattern, loc); + if (best_loc != -1) { + score_threshold = min(_match_bitapScore(0, best_loc, loc, pattern), + score_threshold); + // What about in the other direction? (speedup) + best_loc = text.lastIndexOf(pattern, loc + pattern.length); + if (best_loc != -1) { + score_threshold = min(_match_bitapScore(0, best_loc, loc, pattern), + score_threshold); + } + } + + // Initialise the bit arrays. + final matchmask = 1 << (pattern.length - 1); + best_loc = -1; + + int bin_min, bin_mid; + int bin_max = pattern.length + text.length; + List last_rd; + for (int d = 0; d < pattern.length; d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at + // this error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (_match_bitapScore(d, loc + bin_mid, loc, pattern) + <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = ((bin_max - bin_min) / 2 + bin_min).toInt(); + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + int start = max(1, loc - bin_mid + 1); + int finish = min(loc + bin_mid, text.length) + pattern.length; + + final rd = new List(finish + 2); + rd[finish + 1] = (1 << d) - 1; + for (int j = finish; j >= start; j--) { + int charMatch; + if (text.length <= j - 1 || !s.containsKey(text[j - 1])) { + // Out of range. + charMatch = 0; + } else { + charMatch = s[text[j - 1]]; + } + if (d == 0) { + // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { + // Subsequent passes: fuzzy match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch + | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]; + } + if ((rd[j] & matchmask) != 0) { + double score = _match_bitapScore(d, j - 1, loc, pattern); + // This match will almost certainly be better than any existing + // match. But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + if (_match_bitapScore(d + 1, loc, loc, pattern) > score_threshold) { + // No hope for a (better) match at greater error levels. + break; + } + last_rd = rd; + } + return best_loc; + } + + /** + * Compute and return the score for a match with e errors and x location. + * [e] is the number of errors in match. + * [x] is the location of match. + * [loc] is the expected location of match. + * [pattern] is the pattern being sought. + * Returns the overall score for match (0.0 = good, 1.0 = bad). + */ + double _match_bitapScore(int e, int x, int loc, String pattern) { + final accuracy = e / pattern.length; + final proximity = (loc - x).abs(); + if (Match_Distance == 0) { + // Dodge divide by zero error. + return proximity == 0 ? accuracy : 1.0; + } + return accuracy + proximity / Match_Distance; + } + + /** + * Initialise the alphabet for the Bitap algorithm. + * [pattern] is the the text to encode. + * Returns a Map of character locations. + */ + Map _match_alphabet(String pattern) { + final s = new HashMap(); + for (int i = 0; i < pattern.length; i++) { + s[pattern[i]] = 0; + } + for (int i = 0; i < pattern.length; i++) { + s[pattern[i]] = s[pattern[i]] | (1 << (pattern.length - i - 1)); + } + return s; + } + + + // PATCH FUNCTIONS + + + /** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * [patch] is the phe patch to grow. + * [text] is the source text. + */ + void _patch_addContext(Patch patch, String text) { + if (text.isEmpty) { + return; + } + String pattern = text.substring(patch.start2, patch.start2 + patch.length1); + int padding = 0; + + // Look for the first and last matches of pattern in text. If two different + // matches are found, increase the pattern length. + while (text.indexOf(pattern) != text.lastIndexOf(pattern) + && pattern.length < Match_MaxBits - Patch_Margin - Patch_Margin) { + padding += Patch_Margin; + pattern = text.substring(max(0, patch.start2 - padding), + min(text.length, patch.start2 + patch.length1 + padding)); + } + // Add one chunk for good luck. + padding += Patch_Margin; + + // Add the prefix. + final prefix = text.substring(max(0, patch.start2 - padding), + patch.start2); + if (!prefix.isEmpty) { + patch.diffs.insertRange(0, 1, new Diff(DIFF_EQUAL, prefix)); + } + // Add the suffix. + final suffix = text.substring(patch.start2 + patch.length1, + min(text.length, patch.start2 + patch.length1 + padding)); + if (!suffix.isEmpty) { + patch.diffs.addLast(new Diff(DIFF_EQUAL, suffix)); + } + + // Roll back the start points. + patch.start1 -= prefix.length; + patch.start2 -= prefix.length; + // Extend the lengths. + patch.length1 += prefix.length + suffix.length; + patch.length2 += prefix.length + suffix.length; + } + + /** + * Compute a list of patches to turn text1 into text2. + * Use diffs if provided, otherwise compute it ourselves. + * There are four ways to call this function, depending on what data is + * available to the caller: + * Method 1: + * [a] = text1, [opt_b] = text2 + * Method 2: + * [a] = diffs + * Method 3 (optimal): + * [a] = text1, [opt_b] = diffs + * Method 4 (deprecated, use method 3): + * [a] = text1, [opt_b] = text2, [opt_c] = diffs + * Returns a List of Patch objects. + */ + List patch_make(a, [opt_b, opt_c]) { + String text1; + List diffs; + if (a is String && opt_b is String && opt_c == null) { + // Method 1: text1, text2 + // Compute diffs from text1 and text2. + text1 = a; + diffs = diff_main(text1, opt_b, true); + if (diffs.length > 2) { + diff_cleanupSemantic(diffs); + diff_cleanupEfficiency(diffs); + } + } else if (a is List && opt_b == null && opt_c == null) { + // Method 2: diffs + // Compute text1 from diffs. + diffs = a; + text1 = diff_text1(diffs); + } else if (a is String && opt_b is List && opt_c == null) { + // Method 3: text1, diffs + text1 = a; + diffs = opt_b; + } else if (a is String && opt_b is String && opt_c is List) { + // Method 4: text1, text2, diffs + // text2 is not used. + text1 = a; + diffs = opt_c; + } else { + throw new ArgumentError('Unknown call format to patch_make.'); + } + + final patches = []; + if (diffs.isEmpty) { + return patches; // Get rid of the null case. + } + Patch patch = new Patch(); + final postpatch_buffer = new StringBuffer(); + int char_count1 = 0; // Number of characters into the text1 string. + int char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + String prepatch_text = text1; + String postpatch_text = text1; + for (Diff aDiff in diffs) { + if (patch.diffs.isEmpty && aDiff.operation != DIFF_EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (aDiff.operation) { + case DIFF_INSERT: + patch.diffs.add(aDiff); + patch.length2 += aDiff.text.length; + postpatch_buffer.clear(); + postpatch_text = + postpatch_buffer.add(postpatch_text.substring(0, char_count2)) + .add(aDiff.text).add(postpatch_text.substring(char_count2)) + .toString(); + break; + case DIFF_DELETE: + patch.length1 += aDiff.text.length; + patch.diffs.add(aDiff); + postpatch_buffer.clear(); + postpatch_text = + postpatch_buffer.add(postpatch_text.substring(0, char_count2)) + .add(postpatch_text.substring(char_count2 + aDiff.text.length)) + .toString(); + break; + case DIFF_EQUAL: + if (aDiff.text.length <= 2 * Patch_Margin + && !patch.diffs.isEmpty && aDiff != diffs.last) { + // Small equality inside a patch. + patch.diffs.add(aDiff); + patch.length1 += aDiff.text.length; + patch.length2 += aDiff.text.length; + } + + if (aDiff.text.length >= 2 * Patch_Margin) { + // Time for a new patch. + if (!patch.diffs.isEmpty) { + _patch_addContext(patch, prepatch_text); + patches.add(patch); + patch = new Patch(); + // Unlike Unidiff, our patch lists have a rolling context. + // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (aDiff.operation != DIFF_INSERT) { + char_count1 += aDiff.text.length; + } + if (aDiff.operation != DIFF_DELETE) { + char_count2 += aDiff.text.length; + } + } + // Pick up the leftover patch if not empty. + if (!patch.diffs.isEmpty) { + _patch_addContext(patch, prepatch_text); + patches.add(patch); + } + + return patches; + } + + /** + * Given an array of patches, return another array that is identical. + * [patches] is a List of Patch objects. + * Returns a List of Patch objects. + */ + List patch_deepCopy(List patches) { + final patchesCopy = []; + for (Patch aPatch in patches) { + final patchCopy = new Patch(); + for (Diff aDiff in aPatch.diffs) { + patchCopy.diffs.add(new Diff(aDiff.operation, aDiff.text)); + } + patchCopy.start1 = aPatch.start1; + patchCopy.start2 = aPatch.start2; + patchCopy.length1 = aPatch.length1; + patchCopy.length2 = aPatch.length2; + patchesCopy.add(patchCopy); + } + return patchesCopy; + } + + /** + * Merge a set of patches onto the text. Return a patched text, as well + * as an array of true/false values indicating which patches were applied. + * [patches] is a List of Patch objects + * [text] is the old text. + * Returns a two element List, containing the new text and a List of + * bool values. + */ + List patch_apply(List patches, String text) { + if (patches.isEmpty) { + return [text, []]; + } + + // Deep copy the patches so that no changes are made to originals. + patches = patch_deepCopy(patches); + + final nullPadding = patch_addPadding(patches); + text = '$nullPadding$text$nullPadding'; + patch_splitMax(patches); + + final text_buffer = new StringBuffer(); + int x = 0; + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + int delta = 0; + final results = new List(patches.length); + for (Patch aPatch in patches) { + int expected_loc = aPatch.start2 + delta; + String text1 = diff_text1(aPatch.diffs); + int start_loc; + int end_loc = -1; + if (text1.length > Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = match_main(text, + text1.substring(0, Match_MaxBits), expected_loc); + if (start_loc != -1) { + end_loc = match_main(text, + text1.substring(text1.length - Match_MaxBits), + expected_loc + text1.length - Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= aPatch.length2 - aPatch.length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + String text2; + if (end_loc == -1) { + text2 = text.substring(start_loc, + min(start_loc + text1.length, text.length)); + } else { + text2 = text.substring(start_loc, + min(end_loc + Match_MaxBits, text.length)); + } + if (text1 == text2) { + // Perfect match, just shove the replacement text in. + text_buffer.clear(); + text = text_buffer.add(text.substring(0, start_loc)) + .add(diff_text2(aPatch.diffs)) + .add(text.substring(start_loc + text1.length)).toString(); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + final diffs = diff_main(text1, text2, false); + if (text1.length > Match_MaxBits + && diff_levenshtein(diffs) / text1.length + > Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + _diff_cleanupSemanticLossless(diffs); + int index1 = 0; + for (Diff aDiff in aPatch.diffs) { + if (aDiff.operation != DIFF_EQUAL) { + int index2 = diff_xIndex(diffs, index1); + if (aDiff.operation == DIFF_INSERT) { + // Insertion + text_buffer.clear(); + text = text_buffer.add(text.substring(0, start_loc + index2)) + .add(aDiff.text) + .add(text.substring(start_loc + index2)).toString(); + } else if (aDiff.operation == DIFF_DELETE) { + // Deletion + text_buffer.clear(); + text = text_buffer.add(text.substring(0, start_loc + index2)) + .add(text.substring(start_loc + diff_xIndex(diffs, + index1 + aDiff.text.length))).toString(); + } + } + if (aDiff.operation != DIFF_DELETE) { + index1 += aDiff.text.length; + } + } + } + } + } + x++; + } + // Strip the padding off. + text = text.substring(nullPadding.length, text.length - nullPadding.length); + return [text, results]; + } + + /** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * [patches] is a List of Patch objects. + * Returns the padding string added to each side. + */ + String patch_addPadding(List patches) { + final paddingLength = Patch_Margin; + final paddingCodes = []; + for (int x = 1; x <= paddingLength; x++) { + paddingCodes.add(x); + } + String nullPadding = new String.fromCharCodes(paddingCodes); + + // Bump all the patches forward. + for (Patch aPatch in patches) { + aPatch.start1 += paddingLength; + aPatch.start2 += paddingLength; + } + + // Add some padding on start of first diff. + Patch patch = patches[0]; + List diffs = patch.diffs; + if (diffs.isEmpty || diffs[0].operation != DIFF_EQUAL) { + // Add nullPadding equality. + diffs.insertRange(0, 1, new Diff(DIFF_EQUAL, nullPadding)); + patch.start1 -= paddingLength; // Should be 0. + patch.start2 -= paddingLength; // Should be 0. + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs[0].text.length) { + // Grow first equality. + Diff firstDiff = diffs[0]; + int extraLength = paddingLength - firstDiff.text.length; + firstDiff.text = + '${nullPadding.substring(firstDiff.text.length)}${firstDiff.text}'; + patch.start1 -= extraLength; + patch.start2 -= extraLength; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + // Add some padding on end of last diff. + patch = patches.last; + diffs = patch.diffs; + if (diffs.isEmpty || diffs.last.operation != DIFF_EQUAL) { + // Add nullPadding equality. + diffs.addLast(new Diff(DIFF_EQUAL, nullPadding)); + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.last.text.length) { + // Grow last equality. + Diff lastDiff = diffs.last; + int extraLength = paddingLength - lastDiff.text.length; + lastDiff.text = + '${lastDiff.text}${nullPadding.substring(0, extraLength)}'; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + return nullPadding; + } + + /** + * Look through the patches and break up any which are longer than the + * maximum limit of the match algorithm. + * Intended to be called only from within patch_apply. + * [patches] is a List of Patch objects. + */ + patch_splitMax(List patches) { + final patch_size = Match_MaxBits; + for (var x = 0; x < patches.length; x++) { + if (patches[x].length1 <= patch_size) { + continue; + } + Patch bigpatch = patches[x]; + // Remove the big old patch. + patches.removeRange(x--, 1); + int start1 = bigpatch.start1; + int start2 = bigpatch.start2; + String precontext = ''; + while (!bigpatch.diffs.isEmpty) { + // Create one of several smaller patches. + final patch = new Patch(); + bool empty = true; + patch.start1 = start1 - precontext.length; + patch.start2 = start2 - precontext.length; + if (!precontext.isEmpty) { + patch.length1 = patch.length2 = precontext.length; + patch.diffs.add(new Diff(DIFF_EQUAL, precontext)); + } + while (!bigpatch.diffs.isEmpty + && patch.length1 < patch_size - Patch_Margin) { + int diff_type = bigpatch.diffs[0].operation; + String diff_text = bigpatch.diffs[0].text; + if (diff_type == DIFF_INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.length; + start2 += diff_text.length; + patch.diffs.addLast(bigpatch.diffs[0]); + bigpatch.diffs.removeRange(0, 1); + empty = false; + } else if (diff_type == DIFF_DELETE && patch.diffs.length == 1 + && patch.diffs[0].operation == DIFF_EQUAL + && diff_text.length > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.length; + start1 += diff_text.length; + empty = false; + patch.diffs.add(new Diff(diff_type, diff_text)); + bigpatch.diffs.removeRange(0, 1); + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text.substring(0, min(diff_text.length, + patch_size - patch.length1 - Patch_Margin)); + patch.length1 += diff_text.length; + start1 += diff_text.length; + if (diff_type == DIFF_EQUAL) { + patch.length2 += diff_text.length; + start2 += diff_text.length; + } else { + empty = false; + } + patch.diffs.add(new Diff(diff_type, diff_text)); + if (diff_text == bigpatch.diffs[0].text) { + bigpatch.diffs.removeRange(0, 1); + } else { + bigpatch.diffs[0].text = bigpatch.diffs[0].text + .substring(diff_text.length); + } + } + } + // Compute the head context for the next patch. + precontext = diff_text2(patch.diffs); + precontext = precontext.substring(max(0, precontext.length + - Patch_Margin)); + // Append the end context for this patch. + String postcontext; + if (diff_text1(bigpatch.diffs).length > Patch_Margin) { + postcontext = diff_text1(bigpatch.diffs).substring(0, Patch_Margin); + } else { + postcontext = diff_text1(bigpatch.diffs); + } + if (!postcontext.isEmpty) { + patch.length1 += postcontext.length; + patch.length2 += postcontext.length; + if (!patch.diffs.isEmpty + && patch.diffs.last.operation == DIFF_EQUAL) { + patch.diffs.last.text = '${patch.diffs.last.text}$postcontext'; + } else { + patch.diffs.add(new Diff(DIFF_EQUAL, postcontext)); + } + } + if (!empty) { + patches.insertRange(++x, 1, patch); + } + } + } + } + + /** + * Take a list of patches and return a textual representation. + * [patches] is a List of Patch objects. + * Returns a text representation of patches. + */ + String patch_toText(List patches) { + final text = new StringBuffer(); + for (Patch aPatch in patches) { + text.add(aPatch); + } + return text.toString(); + } + + /** + * Parse a textual representation of patches and return a List of Patch + * objects. + * [textline] is a text representation of patches. + * Returns a List of Patch objects. + * Throws ArgumentError if invalid input. + */ + List patch_fromText(String textline) { + final patches = []; + if (textline.isEmpty) { + return patches; + } + final text = textline.split('\n'); + int textPointer = 0; + final patchHeader + = new RegExp('^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@\$'); + while (textPointer < text.length) { + Match m = patchHeader.firstMatch(text[textPointer]); + if (m == null) { + throw new ArgumentError( + 'Invalid patch string: ${text[textPointer]}'); + } + final patch = new Patch(); + patches.add(patch); + patch.start1 = int.parse(m.group(1)); + if (m.group(2).isEmpty) { + patch.start1--; + patch.length1 = 1; + } else if (m.group(2) == '0') { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = int.parse(m.group(2)); + } + + patch.start2 = int.parse(m.group(3)); + if (m.group(4).isEmpty) { + patch.start2--; + patch.length2 = 1; + } else if (m.group(4) == '0') { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = int.parse(m.group(4)); + } + textPointer++; + + while (textPointer < text.length) { + if (!text[textPointer].isEmpty) { + final sign = text[textPointer][0]; + String line; + try { + line = decodeUri(text[textPointer].substring(1)); + } on ArgumentError catch (e) { + // Malformed URI sequence. + throw new ArgumentError( + 'Illegal escape in patch_fromText: $line'); + } + if (sign == '-') { + // Deletion. + patch.diffs.add(new Diff(DIFF_DELETE, line)); + } else if (sign == '+') { + // Insertion. + patch.diffs.add(new Diff(DIFF_INSERT, line)); + } else if (sign == ' ') { + // Minor equality. + patch.diffs.add(new Diff(DIFF_EQUAL, line)); + } else if (sign == '@') { + // Start of next patch. + break; + } else { + // WTF? + throw new ArgumentError( + 'Invalid patch mode "$sign" in: $line'); + } + } + textPointer++; + } + } + return patches; + } +} diff --git a/dart/DiffClass.dart b/dart/DiffClass.dart new file mode 100644 index 0000000..4c8a8f3 --- /dev/null +++ b/dart/DiffClass.dart @@ -0,0 +1,57 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +part of DiffMatchPatch; + +/** + * Class representing one diff operation. + */ +class Diff { + /** + * One of: DIFF_INSERT, DIFF_DELETE or DIFF_EQUAL. + */ + int operation; + /** + * The text associated with this diff operation. + */ + String text; + + /** + * Constructor. Initializes the diff with the provided values. + * [operation] is one of DIFF_INSERT, DIFF_DELETE or DIFF_EQUAL. + * [text] is the text being applied. + */ + Diff(this.operation, this.text); + + /** + * Display a human-readable version of this Diff. + * Returns a text version. + */ + String toString() { + String prettyText = this.text.replaceAll('\n', '\u00b6'); + return 'Diff(${this.operation},"$prettyText")'; + } + + /** + * Is this Diff equivalent to another Diff? + * [other] is another Diff to compare against. + * Returns true or false. + */ + bool operator ==(Diff other) => + operation == other.operation && text == other.text; +} diff --git a/dart/DiffMatchPatch.dart b/dart/DiffMatchPatch.dart new file mode 100644 index 0000000..4383359 --- /dev/null +++ b/dart/DiffMatchPatch.dart @@ -0,0 +1,26 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +library DiffMatchPatch; + +import 'dart:math'; +import 'dart:uri'; + +part 'DMPClass.dart'; +part 'DiffClass.dart'; +part 'PatchClass.dart'; diff --git a/dart/DiffMatchPatchTest.dart b/dart/DiffMatchPatchTest.dart new file mode 100644 index 0000000..fbf630d --- /dev/null +++ b/dart/DiffMatchPatchTest.dart @@ -0,0 +1,871 @@ +/** + * Diff Match and Patch -- Test Harness + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Can't import DiffMatchPatch library since the private functions would be +// unavailable. Instead, import all the source files. +import 'dart:math'; +import 'dart:uri'; +part 'DMPClass.dart'; +part 'DiffClass.dart'; +part 'PatchClass.dart'; + +List _diff_rebuildtexts(diffs) { + // Construct the two texts which made up the diff originally. + final text1 = new StringBuffer(); + final text2 = new StringBuffer(); + for (int x = 0; x < diffs.length; x++) { + if (diffs[x].operation != DIFF_INSERT) { + text1.add(diffs[x].text); + } + if (diffs[x].operation != DIFF_DELETE) { + text2.add(diffs[x].text); + } + } + return [text1.toString(), text2.toString()]; +} + +DiffMatchPatch dmp; + +// DIFF TEST FUNCTIONS + + +void testDiffCommonPrefix() { + // Detect any common prefix. + Expect.equals(0, dmp.diff_commonPrefix('abc', 'xyz'), 'diff_commonPrefix: Null case.'); + + Expect.equals(4, dmp.diff_commonPrefix('1234abcdef', '1234xyz'), 'diff_commonPrefix: Non-null case.'); + + Expect.equals(4, dmp.diff_commonPrefix('1234', '1234xyz'), 'diff_commonPrefix: Whole case.'); +} + +void testDiffCommonSuffix() { + // Detect any common suffix. + Expect.equals(0, dmp.diff_commonSuffix('abc', 'xyz'), 'diff_commonSuffix: Null case.'); + + Expect.equals(4, dmp.diff_commonSuffix('abcdef1234', 'xyz1234'), 'diff_commonSuffix: Non-null case.'); + + Expect.equals(4, dmp.diff_commonSuffix('1234', 'xyz1234'), 'diff_commonSuffix: Whole case.'); +} + +void testDiffCommonOverlap() { + // Detect any suffix/prefix overlap. + Expect.equals(0, dmp._diff_commonOverlap('', 'abcd'), 'diff_commonOverlap: Null case.'); + + Expect.equals(3, dmp._diff_commonOverlap('abc', 'abcd'), 'diff_commonOverlap: Whole case.'); + + Expect.equals(0, dmp._diff_commonOverlap('123456', 'abcd'), 'diff_commonOverlap: No overlap.'); + + Expect.equals(3, dmp._diff_commonOverlap('123456xxx', 'xxxabcd'), 'diff_commonOverlap: Overlap.'); + + // Some overly clever languages (C#) may treat ligatures as equal to their + // component letters. E.g. U+FB01 == 'fi' + Expect.equals(0, dmp._diff_commonOverlap('fi', '\ufb01i'), 'diff_commonOverlap: Unicode.'); +} + +void testDiffHalfmatch() { + // Detect a halfmatch. + dmp.Diff_Timeout = 1.0; + Expect.isNull(dmp._diff_halfMatch('1234567890', 'abcdef'), 'diff_halfMatch: No match #1.'); + + Expect.isNull(dmp._diff_halfMatch('12345', '23'), 'diff_halfMatch: No match #2.'); + + Expect.listEquals(['12', '90', 'a', 'z', '345678'], dmp._diff_halfMatch('1234567890', 'a345678z'), 'diff_halfMatch: Single Match #1.'); + + Expect.listEquals(['a', 'z', '12', '90', '345678'], dmp._diff_halfMatch('a345678z', '1234567890'), 'diff_halfMatch: Single Match #2.'); + + Expect.listEquals(['abc', 'z', '1234', '0', '56789'], dmp._diff_halfMatch('abc56789z', '1234567890'), 'diff_halfMatch: Single Match #3.'); + + Expect.listEquals(['a', 'xyz', '1', '7890', '23456'], dmp._diff_halfMatch('a23456xyz', '1234567890'), 'diff_halfMatch: Single Match #4.'); + + Expect.listEquals(['12123', '123121', 'a', 'z', '1234123451234'], dmp._diff_halfMatch('121231234123451234123121', 'a1234123451234z'), 'diff_halfMatch: Multiple Matches #1.'); + + Expect.listEquals(['', '-=-=-=-=-=', 'x', '', 'x-=-=-=-=-=-=-='], dmp._diff_halfMatch('x-=-=-=-=-=-=-=-=-=-=-=-=', 'xx-=-=-=-=-=-=-='), 'diff_halfMatch: Multiple Matches #2.'); + + Expect.listEquals(['-=-=-=-=-=', '', '', 'y', '-=-=-=-=-=-=-=y'], dmp._diff_halfMatch('-=-=-=-=-=-=-=-=-=-=-=-=y', '-=-=-=-=-=-=-=yy'), 'diff_halfMatch: Multiple Matches #3.'); + + // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy + Expect.listEquals(['qHillo', 'w', 'x', 'Hulloy', 'HelloHe'], dmp._diff_halfMatch('qHilloHelloHew', 'xHelloHeHulloy'), 'diff_halfMatch: Non-optimal halfmatch.'); + + dmp.Diff_Timeout = 0.0; + Expect.isNull(dmp._diff_halfMatch('qHilloHelloHew', 'xHelloHeHulloy'), 'diff_halfMatch: Optimal no halfmatch.'); +} + +void testDiffLinesToChars() { + void assertLinesToCharsResultEquals(Map a, Map b, String error_msg) { + Expect.equals(a['chars1'], b['chars1'], error_msg); + Expect.equals(a['chars2'], b['chars2'], error_msg); + Expect.listEquals(a['lineArray'], b['lineArray'], error_msg); + } + + // Convert lines down to characters. + assertLinesToCharsResultEquals({'chars1': '\u0001\u0002\u0001', 'chars2': '\u0002\u0001\u0002', 'lineArray': ['', 'alpha\n', 'beta\n']}, dmp._diff_linesToChars('alpha\nbeta\nalpha\n', 'beta\nalpha\nbeta\n'), 'diff_linesToChars: Shared lines.'); + + assertLinesToCharsResultEquals({'chars1': '', 'chars2': '\u0001\u0002\u0003\u0003', 'lineArray': ['', 'alpha\r\n', 'beta\r\n', '\r\n']}, dmp._diff_linesToChars('', 'alpha\r\nbeta\r\n\r\n\r\n'), 'diff_linesToChars: Empty string and blank lines.'); + + assertLinesToCharsResultEquals({'chars1': '\u0001', 'chars2': '\u0002', 'lineArray': ['', 'a', 'b']}, dmp._diff_linesToChars('a', 'b'), 'diff_linesToChars: No linebreaks.'); + + // More than 256 to reveal any 8-bit limitations. + int n = 300; + List lineList = []; + StringBuffer charList = new StringBuffer(); + for (int x = 1; x < n + 1; x++) { + lineList.add('$x\n'); + charList.add(new String.fromCharCodes([x])); + } + Expect.equals(n, lineList.length); + String lines = Strings.join(lineList, ''); + String chars = charList.toString(); + Expect.equals(n, chars.length); + lineList.insertRange(0, 1, ''); + assertLinesToCharsResultEquals({'chars1': chars, 'chars2': '', 'lineArray': lineList}, dmp._diff_linesToChars(lines, ''), 'diff_linesToChars: More than 256.'); +} + +void testDiffCharsToLines() { + // First check that Diff equality works. + Expect.isTrue(new Diff(DIFF_EQUAL, 'a') == new Diff(DIFF_EQUAL, 'a'), 'diff_charsToLines: Equality #1.'); + + Expect.equals(new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_EQUAL, 'a'), 'diff_charsToLines: Equality #2.'); + + // Convert chars up to lines. + List diffs = [new Diff(DIFF_EQUAL, '\u0001\u0002\u0001'), new Diff(DIFF_INSERT, '\u0002\u0001\u0002')]; + dmp._diff_charsToLines(diffs, ['', 'alpha\n', 'beta\n']); + Expect.listEquals([new Diff(DIFF_EQUAL, 'alpha\nbeta\nalpha\n'), new Diff(DIFF_INSERT, 'beta\nalpha\nbeta\n')], diffs, 'diff_charsToLines: Shared lines.'); + + // More than 256 to reveal any 8-bit limitations. + int n = 300; + List lineList = []; + StringBuffer charList = new StringBuffer(); + for (int x = 1; x < n + 1; x++) { + lineList.add('$x\n'); + charList.add(new String.fromCharCodes([x])); + } + Expect.equals(n, lineList.length); + String lines = Strings.join(lineList, ''); + String chars = charList.toString(); + Expect.equals(n, chars.length); + lineList.insertRange(0, 1, ''); + diffs = [new Diff(DIFF_DELETE, chars)]; + dmp._diff_charsToLines(diffs, lineList); + Expect.listEquals([new Diff(DIFF_DELETE, lines)], diffs, 'diff_charsToLines: More than 256.'); +} + +void testDiffCleanupMerge() { + // Cleanup a messy diff. + List diffs = []; + dmp.diff_cleanupMerge(diffs); + Expect.listEquals([], diffs, 'diff_cleanupMerge: Null case.'); + + diffs = [new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_DELETE, 'b'), new Diff(DIFF_INSERT, 'c')]; + dmp.diff_cleanupMerge(diffs); + Expect.listEquals([new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_DELETE, 'b'), new Diff(DIFF_INSERT, 'c')], diffs, 'diff_cleanupMerge: No change case.'); + + diffs = [new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_EQUAL, 'b'), new Diff(DIFF_EQUAL, 'c')]; + dmp.diff_cleanupMerge(diffs); + Expect.listEquals([new Diff(DIFF_EQUAL, 'abc')], diffs, 'diff_cleanupMerge: Merge equalities.'); + + diffs = [new Diff(DIFF_DELETE, 'a'), new Diff(DIFF_DELETE, 'b'), new Diff(DIFF_DELETE, 'c')]; + dmp.diff_cleanupMerge(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'abc')], diffs, 'diff_cleanupMerge: Merge deletions.'); + + diffs = [new Diff(DIFF_INSERT, 'a'), new Diff(DIFF_INSERT, 'b'), new Diff(DIFF_INSERT, 'c')]; + dmp.diff_cleanupMerge(diffs); + Expect.listEquals([new Diff(DIFF_INSERT, 'abc')], diffs, 'diff_cleanupMerge: Merge insertions.'); + + diffs = [new Diff(DIFF_DELETE, 'a'), new Diff(DIFF_INSERT, 'b'), new Diff(DIFF_DELETE, 'c'), new Diff(DIFF_INSERT, 'd'), new Diff(DIFF_EQUAL, 'e'), new Diff(DIFF_EQUAL, 'f')]; + dmp.diff_cleanupMerge(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'ac'), new Diff(DIFF_INSERT, 'bd'), new Diff(DIFF_EQUAL, 'ef')], diffs, 'diff_cleanupMerge: Merge interweave.'); + + diffs = [new Diff(DIFF_DELETE, 'a'), new Diff(DIFF_INSERT, 'abc'), new Diff(DIFF_DELETE, 'dc')]; + dmp.diff_cleanupMerge(diffs); + Expect.listEquals([new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_DELETE, 'd'), new Diff(DIFF_INSERT, 'b'), new Diff(DIFF_EQUAL, 'c')], diffs, 'diff_cleanupMerge: Prefix and suffix detection.'); + + diffs = [new Diff(DIFF_EQUAL, 'x'), new Diff(DIFF_DELETE, 'a'), new Diff(DIFF_INSERT, 'abc'), new Diff(DIFF_DELETE, 'dc'), new Diff(DIFF_EQUAL, 'y')]; + dmp.diff_cleanupMerge(diffs); + Expect.listEquals([new Diff(DIFF_EQUAL, 'xa'), new Diff(DIFF_DELETE, 'd'), new Diff(DIFF_INSERT, 'b'), new Diff(DIFF_EQUAL, 'cy')], diffs, 'diff_cleanupMerge: Prefix and suffix detection with equalities.'); + + diffs = [new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_INSERT, 'ba'), new Diff(DIFF_EQUAL, 'c')]; + dmp.diff_cleanupMerge(diffs); + Expect.listEquals([new Diff(DIFF_INSERT, 'ab'), new Diff(DIFF_EQUAL, 'ac')], diffs, 'diff_cleanupMerge: Slide edit left.'); + + diffs = [new Diff(DIFF_EQUAL, 'c'), new Diff(DIFF_INSERT, 'ab'), new Diff(DIFF_EQUAL, 'a')]; + dmp.diff_cleanupMerge(diffs); + Expect.listEquals([new Diff(DIFF_EQUAL, 'ca'), new Diff(DIFF_INSERT, 'ba')], diffs, 'diff_cleanupMerge: Slide edit right.'); + + diffs = [new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_DELETE, 'b'), new Diff(DIFF_EQUAL, 'c'), new Diff(DIFF_DELETE, 'ac'), new Diff(DIFF_EQUAL, 'x')]; + dmp.diff_cleanupMerge(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'abc'), new Diff(DIFF_EQUAL, 'acx')], diffs, 'diff_cleanupMerge: Slide edit left recursive.'); + + diffs = [new Diff(DIFF_EQUAL, 'x'), new Diff(DIFF_DELETE, 'ca'), new Diff(DIFF_EQUAL, 'c'), new Diff(DIFF_DELETE, 'b'), new Diff(DIFF_EQUAL, 'a')]; + dmp.diff_cleanupMerge(diffs); + Expect.listEquals([new Diff(DIFF_EQUAL, 'xca'), new Diff(DIFF_DELETE, 'cba')], diffs, 'diff_cleanupMerge: Slide edit right recursive.'); +} + +void testDiffCleanupSemanticLossless() { + // Slide diffs to match logical boundaries. + List diffs = []; + dmp._diff_cleanupSemanticLossless(diffs); + Expect.listEquals([], diffs, 'diff_cleanupSemanticLossless: Null case.'); + + diffs = [new Diff(DIFF_EQUAL, 'AAA\r\n\r\nBBB'), new Diff(DIFF_INSERT, '\r\nDDD\r\n\r\nBBB'), new Diff(DIFF_EQUAL, '\r\nEEE')]; + dmp._diff_cleanupSemanticLossless(diffs); + Expect.listEquals([new Diff(DIFF_EQUAL, 'AAA\r\n\r\n'), new Diff(DIFF_INSERT, 'BBB\r\nDDD\r\n\r\n'), new Diff(DIFF_EQUAL, 'BBB\r\nEEE')], diffs, 'diff_cleanupSemanticLossless: Blank lines.'); + + diffs = [new Diff(DIFF_EQUAL, 'AAA\r\nBBB'), new Diff(DIFF_INSERT, ' DDD\r\nBBB'), new Diff(DIFF_EQUAL, ' EEE')]; + dmp._diff_cleanupSemanticLossless(diffs); + Expect.listEquals([new Diff(DIFF_EQUAL, 'AAA\r\n'), new Diff(DIFF_INSERT, 'BBB DDD\r\n'), new Diff(DIFF_EQUAL, 'BBB EEE')], diffs, 'diff_cleanupSemanticLossless: Line boundaries.'); + + diffs = [new Diff(DIFF_EQUAL, 'The c'), new Diff(DIFF_INSERT, 'ow and the c'), new Diff(DIFF_EQUAL, 'at.')]; + dmp._diff_cleanupSemanticLossless(diffs); + Expect.listEquals([new Diff(DIFF_EQUAL, 'The '), new Diff(DIFF_INSERT, 'cow and the '), new Diff(DIFF_EQUAL, 'cat.')], diffs, 'diff_cleanupSemanticLossless: Word boundaries.'); + + diffs = [new Diff(DIFF_EQUAL, 'The-c'), new Diff(DIFF_INSERT, 'ow-and-the-c'), new Diff(DIFF_EQUAL, 'at.')]; + dmp._diff_cleanupSemanticLossless(diffs); + Expect.listEquals([new Diff(DIFF_EQUAL, 'The-'), new Diff(DIFF_INSERT, 'cow-and-the-'), new Diff(DIFF_EQUAL, 'cat.')], diffs, 'diff_cleanupSemanticLossless: Alphanumeric boundaries.'); + + diffs = [new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_DELETE, 'a'), new Diff(DIFF_EQUAL, 'ax')]; + dmp._diff_cleanupSemanticLossless(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'a'), new Diff(DIFF_EQUAL, 'aax')], diffs, 'diff_cleanupSemanticLossless: Hitting the start.'); + + diffs = [new Diff(DIFF_EQUAL, 'xa'), new Diff(DIFF_DELETE, 'a'), new Diff(DIFF_EQUAL, 'a')]; + dmp._diff_cleanupSemanticLossless(diffs); + Expect.listEquals([new Diff(DIFF_EQUAL, 'xaa'), new Diff(DIFF_DELETE, 'a')], diffs, 'diff_cleanupSemanticLossless: Hitting the end.'); + + diffs = [new Diff(DIFF_EQUAL, 'The xxx. The '), new Diff(DIFF_INSERT, 'zzz. The '), new Diff(DIFF_EQUAL, 'yyy.')]; + dmp._diff_cleanupSemanticLossless(diffs); + Expect.listEquals([new Diff(DIFF_EQUAL, 'The xxx.'), new Diff(DIFF_INSERT, ' The zzz.'), new Diff(DIFF_EQUAL, ' The yyy.')], diffs, 'diff_cleanupSemanticLossless: Sentence boundaries.'); +} + +void testDiffCleanupSemantic() { + // Cleanup semantically trivial equalities. + List diffs = []; + dmp.diff_cleanupSemantic(diffs); + Expect.listEquals([], diffs, 'diff_cleanupSemantic: Null case.'); + + diffs = [new Diff(DIFF_DELETE, 'ab'), new Diff(DIFF_INSERT, 'cd'), new Diff(DIFF_EQUAL, '12'), new Diff(DIFF_DELETE, 'e')]; + dmp.diff_cleanupSemantic(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'ab'), new Diff(DIFF_INSERT, 'cd'), new Diff(DIFF_EQUAL, '12'), new Diff(DIFF_DELETE, 'e')], diffs, 'diff_cleanupSemantic: No elimination #1.'); + + diffs = [new Diff(DIFF_DELETE, 'abc'), new Diff(DIFF_INSERT, 'ABC'), new Diff(DIFF_EQUAL, '1234'), new Diff(DIFF_DELETE, 'wxyz')]; + dmp.diff_cleanupSemantic(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'abc'), new Diff(DIFF_INSERT, 'ABC'), new Diff(DIFF_EQUAL, '1234'), new Diff(DIFF_DELETE, 'wxyz')], diffs, 'diff_cleanupSemantic: No elimination #2.'); + + diffs = [new Diff(DIFF_DELETE, 'a'), new Diff(DIFF_EQUAL, 'b'), new Diff(DIFF_DELETE, 'c')]; + dmp.diff_cleanupSemantic(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'abc'), new Diff(DIFF_INSERT, 'b')], diffs, 'diff_cleanupSemantic: Simple elimination.'); + + diffs = [new Diff(DIFF_DELETE, 'ab'), new Diff(DIFF_EQUAL, 'cd'), new Diff(DIFF_DELETE, 'e'), new Diff(DIFF_EQUAL, 'f'), new Diff(DIFF_INSERT, 'g')]; + dmp.diff_cleanupSemantic(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'abcdef'), new Diff(DIFF_INSERT, 'cdfg')], diffs, 'diff_cleanupSemantic: Backpass elimination.'); + + diffs = [new Diff(DIFF_INSERT, '1'), new Diff(DIFF_EQUAL, 'A'), new Diff(DIFF_DELETE, 'B'), new Diff(DIFF_INSERT, '2'), new Diff(DIFF_EQUAL, '_'), new Diff(DIFF_INSERT, '1'), new Diff(DIFF_EQUAL, 'A'), new Diff(DIFF_DELETE, 'B'), new Diff(DIFF_INSERT, '2')]; + dmp.diff_cleanupSemantic(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'AB_AB'), new Diff(DIFF_INSERT, '1A2_1A2')], diffs, 'diff_cleanupSemantic: Multiple elimination.'); + + diffs = [new Diff(DIFF_EQUAL, 'The c'), new Diff(DIFF_DELETE, 'ow and the c'), new Diff(DIFF_EQUAL, 'at.')]; + dmp.diff_cleanupSemantic(diffs); + Expect.listEquals([new Diff(DIFF_EQUAL, 'The '), new Diff(DIFF_DELETE, 'cow and the '), new Diff(DIFF_EQUAL, 'cat.')], diffs, 'diff_cleanupSemantic: Word boundaries.'); + + diffs = [new Diff(DIFF_DELETE, 'abcxx'), new Diff(DIFF_INSERT, 'xxdef')]; + dmp.diff_cleanupSemantic(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'abcxx'), new Diff(DIFF_INSERT, 'xxdef')], diffs, 'diff_cleanupSemantic: No overlap elimination.'); + + diffs = [new Diff(DIFF_DELETE, 'abcxxx'), new Diff(DIFF_INSERT, 'xxxdef')]; + dmp.diff_cleanupSemantic(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'abc'), new Diff(DIFF_EQUAL, 'xxx'), new Diff(DIFF_INSERT, 'def')], diffs, 'diff_cleanupSemantic: Overlap elimination.'); + + diffs = [new Diff(DIFF_DELETE, 'xxxabc'), new Diff(DIFF_INSERT, 'defxxx')]; + dmp.diff_cleanupSemantic(diffs); + Expect.listEquals([new Diff(DIFF_INSERT, 'def'), new Diff(DIFF_EQUAL, 'xxx'), new Diff(DIFF_DELETE, 'abc')], diffs, 'diff_cleanupSemantic: Reverse overlap elimination.'); + + diffs = [new Diff(DIFF_DELETE, 'abcd1212'), new Diff(DIFF_INSERT, '1212efghi'), new Diff(DIFF_EQUAL, '----'), new Diff(DIFF_DELETE, 'A3'), new Diff(DIFF_INSERT, '3BC')]; + dmp.diff_cleanupSemantic(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'abcd'), new Diff(DIFF_EQUAL, '1212'), new Diff(DIFF_INSERT, 'efghi'), new Diff(DIFF_EQUAL, '----'), new Diff(DIFF_DELETE, 'A'), new Diff(DIFF_EQUAL, '3'), new Diff(DIFF_INSERT, 'BC')], diffs, 'diff_cleanupSemantic: Two overlap eliminations.'); +} + +void testDiffCleanupEfficiency() { + // Cleanup operationally trivial equalities. + dmp.Diff_EditCost = 4; + List diffs = []; + dmp.diff_cleanupEfficiency(diffs); + Expect.listEquals([], diffs, 'diff_cleanupEfficiency: Null case.'); + + diffs = [new Diff(DIFF_DELETE, 'ab'), new Diff(DIFF_INSERT, '12'), new Diff(DIFF_EQUAL, 'wxyz'), new Diff(DIFF_DELETE, 'cd'), new Diff(DIFF_INSERT, '34')]; + dmp.diff_cleanupEfficiency(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'ab'), new Diff(DIFF_INSERT, '12'), new Diff(DIFF_EQUAL, 'wxyz'), new Diff(DIFF_DELETE, 'cd'), new Diff(DIFF_INSERT, '34')], diffs, 'diff_cleanupEfficiency: No elimination.'); + + diffs = [new Diff(DIFF_DELETE, 'ab'), new Diff(DIFF_INSERT, '12'), new Diff(DIFF_EQUAL, 'xyz'), new Diff(DIFF_DELETE, 'cd'), new Diff(DIFF_INSERT, '34')]; + dmp.diff_cleanupEfficiency(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'abxyzcd'), new Diff(DIFF_INSERT, '12xyz34')], diffs, 'diff_cleanupEfficiency: Four-edit elimination.'); + + diffs = [new Diff(DIFF_INSERT, '12'), new Diff(DIFF_EQUAL, 'x'), new Diff(DIFF_DELETE, 'cd'), new Diff(DIFF_INSERT, '34')]; + dmp.diff_cleanupEfficiency(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'xcd'), new Diff(DIFF_INSERT, '12x34')], diffs, 'diff_cleanupEfficiency: Three-edit elimination.'); + + diffs = [new Diff(DIFF_DELETE, 'ab'), new Diff(DIFF_INSERT, '12'), new Diff(DIFF_EQUAL, 'xy'), new Diff(DIFF_INSERT, '34'), new Diff(DIFF_EQUAL, 'z'), new Diff(DIFF_DELETE, 'cd'), new Diff(DIFF_INSERT, '56')]; + dmp.diff_cleanupEfficiency(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'abxyzcd'), new Diff(DIFF_INSERT, '12xy34z56')], diffs, 'diff_cleanupEfficiency: Backpass elimination.'); + + dmp.Diff_EditCost = 5; + diffs = [new Diff(DIFF_DELETE, 'ab'), new Diff(DIFF_INSERT, '12'), new Diff(DIFF_EQUAL, 'wxyz'), new Diff(DIFF_DELETE, 'cd'), new Diff(DIFF_INSERT, '34')]; + dmp.diff_cleanupEfficiency(diffs); + Expect.listEquals([new Diff(DIFF_DELETE, 'abwxyzcd'), new Diff(DIFF_INSERT, '12wxyz34')], diffs, 'diff_cleanupEfficiency: High cost elimination.'); + dmp.Diff_EditCost = 4; +} + +void testDiffPrettyHtml() { + // Pretty print. + List diffs = [new Diff(DIFF_EQUAL, 'a\n'), new Diff(DIFF_DELETE, 'b'), new Diff(DIFF_INSERT, 'c&d')]; + Expect.equals('
<B>b</B>c&d', dmp.diff_prettyHtml(diffs), 'diff_prettyHtml:'); +} + +void testDiffText() { + // Compute the source and destination texts. + List diffs = [new Diff(DIFF_EQUAL, 'jump'), new Diff(DIFF_DELETE, 's'), new Diff(DIFF_INSERT, 'ed'), new Diff(DIFF_EQUAL, ' over '), new Diff(DIFF_DELETE, 'the'), new Diff(DIFF_INSERT, 'a'), new Diff(DIFF_EQUAL, ' lazy')]; + Expect.equals('jumps over the lazy', dmp.diff_text1(diffs), 'diff_text1:'); + Expect.equals('jumped over a lazy', dmp.diff_text2(diffs), 'diff_text2:'); +} + +void testDiffDelta() { + // Convert a diff into delta string. + List diffs = [new Diff(DIFF_EQUAL, 'jump'), new Diff(DIFF_DELETE, 's'), new Diff(DIFF_INSERT, 'ed'), new Diff(DIFF_EQUAL, ' over '), new Diff(DIFF_DELETE, 'the'), new Diff(DIFF_INSERT, 'a'), new Diff(DIFF_EQUAL, ' lazy'), new Diff(DIFF_INSERT, 'old dog')]; + String text1 = dmp.diff_text1(diffs); + Expect.equals('jumps over the lazy', text1, 'diff_text1: Base text.'); + + String delta = dmp.diff_toDelta(diffs); + Expect.equals('=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog', delta, 'diff_toDelta:'); + + // Convert delta string into a diff. + Expect.listEquals(diffs, dmp.diff_fromDelta(text1, delta), 'diff_fromDelta: Normal.'); + + // Generates error (19 < 20). + Expect.throws(() => dmp.diff_fromDelta('${text1}x', delta), null, 'diff_fromDelta: Too long.'); + + // Generates error (19 > 18). + Expect.throws(() => dmp.diff_fromDelta(text1.substring(1), delta), null, 'diff_fromDelta: Too short.'); + + // Generates error (%c3%xy invalid Unicode). + Expect.throws(() => dmp.diff_fromDelta('', '+%c3%xy'), null, 'diff_fromDelta: Invalid character.'); + + // Test deltas with special characters. + diffs = [new Diff(DIFF_EQUAL, '\u0680 \x00 \t %'), new Diff(DIFF_DELETE, '\u0681 \x01 \n ^'), new Diff(DIFF_INSERT, '\u0682 \x02 \\ |')]; + text1 = dmp.diff_text1(diffs); + Expect.equals('\u0680 \x00 \t %\u0681 \x01 \n ^', text1, 'diff_text1: Unicode text.'); + + delta = dmp.diff_toDelta(diffs); + Expect.equals('=7\t-7\t+%DA%82 %02 %5C %7C', delta, 'diff_toDelta: Unicode.'); + + Expect.listEquals(diffs, dmp.diff_fromDelta(text1, delta), 'diff_fromDelta: Unicode.'); + + // Verify pool of unchanged characters. + diffs = [new Diff(DIFF_INSERT, 'A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + \$ , # ')]; + String text2 = dmp.diff_text2(diffs); + Expect.equals('A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + \$ , # ', text2, 'diff_text2: Unchanged characters.'); + + delta = dmp.diff_toDelta(diffs); + Expect.equals('+A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + \$ , # ', delta, 'diff_toDelta: Unchanged characters.'); + + // Convert delta string into a diff. + Expect.listEquals(diffs, dmp.diff_fromDelta('', delta), 'diff_fromDelta: Unchanged characters.'); +} + +void testDiffXIndex() { + // Translate a location in text1 to text2. + List diffs = [new Diff(DIFF_DELETE, 'a'), new Diff(DIFF_INSERT, '1234'), new Diff(DIFF_EQUAL, 'xyz')]; + Expect.equals(5, dmp.diff_xIndex(diffs, 2), 'diff_xIndex: Translation on equality.'); + + diffs = [new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_DELETE, '1234'), new Diff(DIFF_EQUAL, 'xyz')]; + Expect.equals(1, dmp.diff_xIndex(diffs, 3), 'diff_xIndex: Translation on deletion.'); +} + +void testDiffLevenshtein() { + List diffs = [new Diff(DIFF_DELETE, 'abc'), new Diff(DIFF_INSERT, '1234'), new Diff(DIFF_EQUAL, 'xyz')]; + Expect.equals(4, dmp.diff_levenshtein(diffs), 'Levenshtein with trailing equality.'); + + diffs = [new Diff(DIFF_EQUAL, 'xyz'), new Diff(DIFF_DELETE, 'abc'), new Diff(DIFF_INSERT, '1234')]; + Expect.equals(4, dmp.diff_levenshtein(diffs), 'Levenshtein with leading equality.'); + + diffs = [new Diff(DIFF_DELETE, 'abc'), new Diff(DIFF_EQUAL, 'xyz'), new Diff(DIFF_INSERT, '1234')]; + Expect.equals(7, dmp.diff_levenshtein(diffs), 'Levenshtein with middle equality.'); +} + +void testDiffBisect() { + // Normal. + String a = 'cat'; + String b = 'map'; + // Since the resulting diff hasn't been normalized, it would be ok if + // the insertion and deletion pairs are swapped. + // If the order changes, tweak this test as required. + List diffs = [new Diff(DIFF_DELETE, 'c'), new Diff(DIFF_INSERT, 'm'), new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_DELETE, 't'), new Diff(DIFF_INSERT, 'p')]; + // One year should be sufficient. + Date deadline = new Date.now().add(new Duration(days : 365)); + Expect.listEquals(diffs, dmp._diff_bisect(a, b, deadline), 'diff_bisect: Normal.'); + + // Timeout. + diffs = [new Diff(DIFF_DELETE, 'cat'), new Diff(DIFF_INSERT, 'map')]; + // Set deadline to one year ago. + deadline = new Date.now().subtract(new Duration(days : 365)); + Expect.listEquals(diffs, dmp._diff_bisect(a, b, deadline), 'diff_bisect: Timeout.'); +} + +void testDiffMain() { + // Perform a trivial diff. + List diffs = []; + Expect.listEquals(diffs, dmp.diff_main('', '', false), 'diff_main: Null case.'); + + diffs = [new Diff(DIFF_EQUAL, 'abc')]; + Expect.listEquals(diffs, dmp.diff_main('abc', 'abc', false), 'diff_main: Equality.'); + + diffs = [new Diff(DIFF_EQUAL, 'ab'), new Diff(DIFF_INSERT, '123'), new Diff(DIFF_EQUAL, 'c')]; + Expect.listEquals(diffs, dmp.diff_main('abc', 'ab123c', false), 'diff_main: Simple insertion.'); + + diffs = [new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_DELETE, '123'), new Diff(DIFF_EQUAL, 'bc')]; + Expect.listEquals(diffs, dmp.diff_main('a123bc', 'abc', false), 'diff_main: Simple deletion.'); + + diffs = [new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_INSERT, '123'), new Diff(DIFF_EQUAL, 'b'), new Diff(DIFF_INSERT, '456'), new Diff(DIFF_EQUAL, 'c')]; + Expect.listEquals(diffs, dmp.diff_main('abc', 'a123b456c', false), 'diff_main: Two insertions.'); + + diffs = [new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_DELETE, '123'), new Diff(DIFF_EQUAL, 'b'), new Diff(DIFF_DELETE, '456'), new Diff(DIFF_EQUAL, 'c')]; + Expect.listEquals(diffs, dmp.diff_main('a123b456c', 'abc', false), 'diff_main: Two deletions.'); + + // Perform a real diff. + // Switch off the timeout. + dmp.Diff_Timeout = 0.0; + diffs = [new Diff(DIFF_DELETE, 'a'), new Diff(DIFF_INSERT, 'b')]; + Expect.listEquals(diffs, dmp.diff_main('a', 'b', false), 'diff_main: Simple case #1.'); + + diffs = [new Diff(DIFF_DELETE, 'Apple'), new Diff(DIFF_INSERT, 'Banana'), new Diff(DIFF_EQUAL, 's are a'), new Diff(DIFF_INSERT, 'lso'), new Diff(DIFF_EQUAL, ' fruit.')]; + Expect.listEquals(diffs, dmp.diff_main('Apples are a fruit.', 'Bananas are also fruit.', false), 'diff_main: Simple case #2.'); + + diffs = [new Diff(DIFF_DELETE, 'a'), new Diff(DIFF_INSERT, '\u0680'), new Diff(DIFF_EQUAL, 'x'), new Diff(DIFF_DELETE, '\t'), new Diff(DIFF_INSERT, '\000')]; + Expect.listEquals(diffs, dmp.diff_main('ax\t', '\u0680x\000', false), 'diff_main: Simple case #3.'); + + diffs = [new Diff(DIFF_DELETE, '1'), new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_DELETE, 'y'), new Diff(DIFF_EQUAL, 'b'), new Diff(DIFF_DELETE, '2'), new Diff(DIFF_INSERT, 'xab')]; + Expect.listEquals(diffs, dmp.diff_main('1ayb2', 'abxab', false), 'diff_main: Overlap #1.'); + + diffs = [new Diff(DIFF_INSERT, 'xaxcx'), new Diff(DIFF_EQUAL, 'abc'), new Diff(DIFF_DELETE, 'y')]; + Expect.listEquals(diffs, dmp.diff_main('abcy', 'xaxcxabc', false), 'diff_main: Overlap #2.'); + + diffs = [new Diff(DIFF_DELETE, 'ABCD'), new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_DELETE, '='), new Diff(DIFF_INSERT, '-'), new Diff(DIFF_EQUAL, 'bcd'), new Diff(DIFF_DELETE, '='), new Diff(DIFF_INSERT, '-'), new Diff(DIFF_EQUAL, 'efghijklmnopqrs'), new Diff(DIFF_DELETE, 'EFGHIJKLMNOefg')]; + Expect.listEquals(diffs, dmp.diff_main('ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg', 'a-bcd-efghijklmnopqrs', false), 'diff_main: Overlap #3.'); + + diffs = [new Diff(DIFF_INSERT, ' '), new Diff(DIFF_EQUAL, 'a'), new Diff(DIFF_INSERT, 'nd'), new Diff(DIFF_EQUAL, ' [[Pennsylvania]]'), new Diff(DIFF_DELETE, ' and [[New')]; + Expect.listEquals(diffs, dmp.diff_main('a [[Pennsylvania]] and [[New', ' and [[Pennsylvania]]', false), 'diff_main: Large equality.'); + + dmp.Diff_Timeout = 0.1; // 100ms + String a = '`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n'; + String b = 'I am the very model of a modern major general,\nI\'ve information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n'; + // Increase the text lengths by 1024 times to ensure a timeout. + for (int x = 0; x < 10; x++) { + a = '$a$a'; + b = '$b$b'; + } + Date startTime = new Date.now(); + dmp.diff_main(a, b); + Date endTime = new Date.now(); + double elapsedSeconds = endTime.difference(startTime).inMilliseconds / 1000; + // Test that we took at least the timeout period. + Expect.isTrue(dmp.Diff_Timeout <= elapsedSeconds, 'diff_main: Timeout min.'); + // Test that we didn't take forever (be forgiving). + // Theoretically this test could fail very occasionally if the + // OS task swaps or locks up for a second at the wrong moment. + // ************* + // Dart Note: Currently (2011) Dart's performance is out of control, so this + // diff takes 3.5 seconds on a 0.1 second timeout. Commented out. + // ************* + // Expect.isTrue(dmp.Diff_Timeout * 2 > elapsedSeconds, 'diff_main: Timeout max.'); + dmp.Diff_Timeout = 0.0; + + // Test the linemode speedup. + // Must be long to pass the 100 char cutoff. + a = '1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n'; + b = 'abcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\n'; + Expect.listEquals(dmp.diff_main(a, b, true), dmp.diff_main(a, b, false), 'diff_main: Simple line-mode.'); + + a = '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'; + b = 'abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij'; + Expect.listEquals(dmp.diff_main(a, b, true), dmp.diff_main(a, b, false), 'diff_main: Single line-mode.'); + + a = '1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n'; + b = 'abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n'; + List texts_linemode = _diff_rebuildtexts(dmp.diff_main(a, b, true)); + List texts_textmode = _diff_rebuildtexts(dmp.diff_main(a, b, false)); + Expect.listEquals(texts_textmode, texts_linemode, 'diff_main: Overlap line-mode.'); + + // Test null inputs. + Expect.throws(() => dmp.diff_main(null, null), null, 'diff_main: Null inputs.'); +} + + +// MATCH TEST FUNCTIONS + +void testMatchAlphabet() { + void assertMapEquals(Map a, Map b, String error_msg) { + Expect.setEquals(a.keys, b.keys, error_msg); + for (var x in a.keys) { + Expect.equals(a[x], b[x], "$error_msg [Key: $x]"); + } + } + + // Initialise the bitmasks for Bitap. + Map bitmask = {'a': 4, 'b': 2, 'c': 1}; + assertMapEquals(bitmask, dmp._match_alphabet('abc'), 'match_alphabet: Unique.'); + + bitmask = {'a': 37, 'b': 18, 'c': 8}; + assertMapEquals(bitmask, dmp._match_alphabet('abcaba'), 'match_alphabet: Duplicates.'); +} + +void testMatchBitap() { + // Bitap algorithm. + dmp.Match_Distance = 100; + dmp.Match_Threshold = 0.5; + Expect.equals(5, dmp._match_bitap('abcdefghijk', 'fgh', 5), 'match_bitap: Exact match #1.'); + + Expect.equals(5, dmp._match_bitap('abcdefghijk', 'fgh', 0), 'match_bitap: Exact match #2.'); + + Expect.equals(4, dmp._match_bitap('abcdefghijk', 'efxhi', 0), 'match_bitap: Fuzzy match #1.'); + + Expect.equals(2, dmp._match_bitap('abcdefghijk', 'cdefxyhijk', 5), 'match_bitap: Fuzzy match #2.'); + + Expect.equals(-1, dmp._match_bitap('abcdefghijk', 'bxy', 1), 'match_bitap: Fuzzy match #3.'); + + Expect.equals(2, dmp._match_bitap('123456789xx0', '3456789x0', 2), 'match_bitap: Overflow.'); + + Expect.equals(0, dmp._match_bitap('abcdef', 'xxabc', 4), 'match_bitap: Before start match.'); + + Expect.equals(3, dmp._match_bitap('abcdef', 'defyy', 4), 'match_bitap: Beyond end match.'); + + Expect.equals(0, dmp._match_bitap('abcdef', 'xabcdefy', 0), 'match_bitap: Oversized pattern.'); + + dmp.Match_Threshold = 0.4; + Expect.equals(4, dmp._match_bitap('abcdefghijk', 'efxyhi', 1), 'match_bitap: Threshold #1.'); + + dmp.Match_Threshold = 0.3; + Expect.equals(-1, dmp._match_bitap('abcdefghijk', 'efxyhi', 1), 'match_bitap: Threshold #2.'); + + dmp.Match_Threshold = 0.0; + Expect.equals(1, dmp._match_bitap('abcdefghijk', 'bcdef', 1), 'match_bitap: Threshold #3.'); + + dmp.Match_Threshold = 0.5; + Expect.equals(0, dmp._match_bitap('abcdexyzabcde', 'abccde', 3), 'match_bitap: Multiple select #1.'); + + Expect.equals(8, dmp._match_bitap('abcdexyzabcde', 'abccde', 5), 'match_bitap: Multiple select #2.'); + + dmp.Match_Distance = 10; // Strict location. + Expect.equals(-1, dmp._match_bitap('abcdefghijklmnopqrstuvwxyz', 'abcdefg', 24), 'match_bitap: Distance test #1.'); + + Expect.equals(0, dmp._match_bitap('abcdefghijklmnopqrstuvwxyz', 'abcdxxefg', 1), 'match_bitap: Distance test #2.'); + + dmp.Match_Distance = 1000; // Loose location. + Expect.equals(0, dmp._match_bitap('abcdefghijklmnopqrstuvwxyz', 'abcdefg', 24), 'match_bitap: Distance test #3.'); +} + +void testMatchMain() { + // Full match. + Expect.equals(0, dmp.match_main('abcdef', 'abcdef', 1000), 'match_main: Equality.'); + + Expect.equals(-1, dmp.match_main('', 'abcdef', 1), 'match_main: Null text.'); + + Expect.equals(3, dmp.match_main('abcdef', '', 3), 'match_main: Null pattern.'); + + Expect.equals(3, dmp.match_main('abcdef', 'de', 3), 'match_main: Exact match.'); + + Expect.equals(3, dmp.match_main('abcdef', 'defy', 4), 'match_main: Beyond end match.'); + + Expect.equals(0, dmp.match_main('abcdef', 'abcdefy', 0), 'match_main: Oversized pattern.'); + + dmp.Match_Threshold = 0.7; + Expect.equals(4, dmp.match_main('I am the very model of a modern major general.', ' that berry ', 5), 'match_main: Complex match.'); + dmp.Match_Threshold = 0.5; + + // Test null inputs. + Expect.throws(() => dmp.match_main(null, null, 0), null, 'match_main: Null inputs.'); +} + + +// PATCH TEST FUNCTIONS + + +void testPatchObj() { + // Patch Object. + Patch p = new Patch(); + p.start1 = 20; + p.start2 = 21; + p.length1 = 18; + p.length2 = 17; + p.diffs = [new Diff(DIFF_EQUAL, 'jump'), new Diff(DIFF_DELETE, 's'), new Diff(DIFF_INSERT, 'ed'), new Diff(DIFF_EQUAL, ' over '), new Diff(DIFF_DELETE, 'the'), new Diff(DIFF_INSERT, 'a'), new Diff(DIFF_EQUAL, '\nlaz')]; + String strp = '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n'; + Expect.equals(strp, p.toString(), 'Patch: toString.'); +} + +void testPatchFromText() { + Expect.isTrue(dmp.patch_fromText('').isEmpty, 'patch_fromText: #0.'); + + String strp = '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n'; + Expect.equals(strp, dmp.patch_fromText(strp)[0].toString(), 'patch_fromText: #1.'); + + Expect.equals('@@ -1 +1 @@\n-a\n+b\n', dmp.patch_fromText('@@ -1 +1 @@\n-a\n+b\n')[0].toString(), 'patch_fromText: #2.'); + + Expect.equals('@@ -1,3 +0,0 @@\n-abc\n', dmp.patch_fromText('@@ -1,3 +0,0 @@\n-abc\n')[0].toString(), 'patch_fromText: #3.'); + + Expect.equals('@@ -0,0 +1,3 @@\n+abc\n', dmp.patch_fromText('@@ -0,0 +1,3 @@\n+abc\n')[0].toString(), 'patch_fromText: #4.'); + + // Generates error. + Expect.throws(() => dmp.patch_fromText('Bad\nPatch\n'), null, 'patch_fromText: #5.'); +} + +void testPatchToText() { + String strp = '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n'; + List patches; + patches = dmp.patch_fromText(strp); + Expect.equals(strp, dmp.patch_toText(patches), 'patch_toText: Single.'); + + strp = '@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n'; + patches = dmp.patch_fromText(strp); + Expect.equals(strp, dmp.patch_toText(patches), 'patch_toText: Dual.'); +} + +void testPatchAddContext() { + dmp.Patch_Margin = 4; + Patch p; + p = dmp.patch_fromText('@@ -21,4 +21,10 @@\n-jump\n+somersault\n')[0]; + dmp._patch_addContext(p, 'The quick brown fox jumps over the lazy dog.'); + Expect.equals('@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n', p.toString(), 'patch_addContext: Simple case.'); + + p = dmp.patch_fromText('@@ -21,4 +21,10 @@\n-jump\n+somersault\n')[0]; + dmp._patch_addContext(p, 'The quick brown fox jumps.'); + Expect.equals('@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n', p.toString(), 'patch_addContext: Not enough trailing context.'); + + p = dmp.patch_fromText('@@ -3 +3,2 @@\n-e\n+at\n')[0]; + dmp._patch_addContext(p, 'The quick brown fox jumps.'); + Expect.equals('@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n', p.toString(), 'patch_addContext: Not enough leading context.'); + + p = dmp.patch_fromText('@@ -3 +3,2 @@\n-e\n+at\n')[0]; + dmp._patch_addContext(p, 'The quick brown fox jumps. The quick brown fox crashes.'); + Expect.equals('@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n', p.toString(), 'patch_addContext: Ambiguity.'); +} + +void testPatchMake() { + List patches; + patches = dmp.patch_make('', ''); + Expect.equals('', dmp.patch_toText(patches), 'patch_make: Null case.'); + + String text1 = 'The quick brown fox jumps over the lazy dog.'; + String text2 = 'That quick brown fox jumped over a lazy dog.'; + String expectedPatch = '@@ -1,8 +1,7 @@\n Th\n-at\n+e\n qui\n@@ -21,17 +21,18 @@\n jump\n-ed\n+s\n over \n-a\n+the\n laz\n'; + // The second patch must be '-21,17 +21,18', not '-22,17 +21,18' due to rolling context. + patches = dmp.patch_make(text2, text1); + Expect.equals(expectedPatch, dmp.patch_toText(patches), 'patch_make: Text2+Text1 inputs.'); + + expectedPatch = '@@ -1,11 +1,12 @@\n Th\n-e\n+at\n quick b\n@@ -22,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n'; + patches = dmp.patch_make(text1, text2); + Expect.equals(expectedPatch, dmp.patch_toText(patches), 'patch_make: Text1+Text2 inputs.'); + + List diffs = dmp.diff_main(text1, text2, false); + patches = dmp.patch_make(diffs); + Expect.equals(expectedPatch, dmp.patch_toText(patches), 'patch_make: Diff input.'); + + patches = dmp.patch_make(text1, diffs); + Expect.equals(expectedPatch, dmp.patch_toText(patches), 'patch_make: Text1+Diff inputs.'); + + patches = dmp.patch_make(text1, text2, diffs); + Expect.equals(expectedPatch, dmp.patch_toText(patches), 'patch_make: Text1+Text2+Diff inputs (deprecated).'); + + patches = dmp.patch_make('`1234567890-=[]\\;\',./', '~!@#\$%^&*()_+{}|:"<>?'); + Expect.equals('@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;\',./\n+~!@#\$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n', dmp.patch_toText(patches), 'patch_toText: Character encoding.'); + + diffs = [new Diff(DIFF_DELETE, '`1234567890-=[]\\;\',./'), new Diff(DIFF_INSERT, '~!@#\$%^&*()_+{}|:"<>?')]; + Expect.listEquals(diffs, dmp.patch_fromText('@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;\',./\n+~!@#\$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n')[0].diffs, 'patch_fromText: Character decoding.'); + + final sb = new StringBuffer(); + for (int x = 0; x < 100; x++) { + sb.add('abcdef'); + } + text1 = sb.toString(); + text2 = '${text1}123'; + expectedPatch = '@@ -573,28 +573,31 @@\n cdefabcdefabcdefabcdefabcdef\n+123\n'; + patches = dmp.patch_make(text1, text2); + Expect.equals(expectedPatch, dmp.patch_toText(patches), 'patch_make: Long string with repeats.'); + + // Test null inputs. + Expect.throws(() => dmp.patch_make(null), null, 'patch_make: Null inputs.'); +} + +void testPatchSplitMax() { + // Assumes that Match_MaxBits is 32. + List patches; + patches = dmp.patch_make('abcdefghijklmnopqrstuvwxyz01234567890', 'XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0'); + dmp.patch_splitMax(patches); + Expect.equals('@@ -1,32 +1,46 @@\n+X\n ab\n+X\n cd\n+X\n ef\n+X\n gh\n+X\n ij\n+X\n kl\n+X\n mn\n+X\n op\n+X\n qr\n+X\n st\n+X\n uv\n+X\n wx\n+X\n yz\n+X\n 012345\n@@ -25,13 +39,18 @@\n zX01\n+X\n 23\n+X\n 45\n+X\n 67\n+X\n 89\n+X\n 0\n', dmp.patch_toText(patches), 'patch_splitMax: #1.'); + + patches = dmp.patch_make('abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz', 'abcdefuvwxyz'); + String oldToText = dmp.patch_toText(patches); + dmp.patch_splitMax(patches); + Expect.equals(oldToText, dmp.patch_toText(patches), 'patch_splitMax: #2.'); + + patches = dmp.patch_make('1234567890123456789012345678901234567890123456789012345678901234567890', 'abc'); + dmp.patch_splitMax(patches); + Expect.equals('@@ -1,32 +1,4 @@\n-1234567890123456789012345678\n 9012\n@@ -29,32 +1,4 @@\n-9012345678901234567890123456\n 7890\n@@ -57,14 +1,3 @@\n-78901234567890\n+abc\n', dmp.patch_toText(patches), 'patch_splitMax: #3.'); + + patches = dmp.patch_make('abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1', 'abcdefghij , h : 1 , t : 1 abcdefghij , h : 1 , t : 1 abcdefghij , h : 0 , t : 1'); + dmp.patch_splitMax(patches); + Expect.equals('@@ -2,32 +2,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n@@ -29,32 +29,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n', dmp.patch_toText(patches), 'patch_splitMax: #4.'); +} + +void testPatchAddPadding() { + List patches; + patches = dmp.patch_make('', 'test'); + Expect.equals('@@ -0,0 +1,4 @@\n+test\n', dmp.patch_toText(patches), 'patch_addPadding: Both edges full.'); + dmp.patch_addPadding(patches); + Expect.equals('@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n', dmp.patch_toText(patches), 'patch_addPadding: Both edges full.'); + + patches = dmp.patch_make('XY', 'XtestY'); + Expect.equals('@@ -1,2 +1,6 @@\n X\n+test\n Y\n', dmp.patch_toText(patches), 'patch_addPadding: Both edges partial.'); + dmp.patch_addPadding(patches); + Expect.equals('@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n', dmp.patch_toText(patches), 'patch_addPadding: Both edges partial.'); + + patches = dmp.patch_make('XXXXYYYY', 'XXXXtestYYYY'); + Expect.equals('@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n', dmp.patch_toText(patches), 'patch_addPadding: Both edges none.'); + dmp.patch_addPadding(patches); + Expect.equals('@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n', dmp.patch_toText(patches), 'patch_addPadding: Both edges none.'); +} + +void testPatchApply() { + dmp.Match_Distance = 1000; + dmp.Match_Threshold = 0.5; + dmp.Patch_DeleteThreshold = 0.5; + List patches; + patches = dmp.patch_make('', ''); + List results = dmp.patch_apply(patches, 'Hello world.'); + List boolArray = results[1]; + String resultStr = '${results[0]}\t${boolArray.length}'; + Expect.equals('Hello world.\t0', resultStr, 'patch_apply: Null case.'); + + patches = dmp.patch_make('The quick brown fox jumps over the lazy dog.', 'That quick brown fox jumped over a lazy dog.'); + results = dmp.patch_apply(patches, 'The quick brown fox jumps over the lazy dog.'); + boolArray = results[1]; + resultStr = '${results[0]}\t${boolArray[0]}\t${boolArray[1]}'; + Expect.equals('That quick brown fox jumped over a lazy dog.\ttrue\ttrue', resultStr, 'patch_apply: Exact match.'); + + results = dmp.patch_apply(patches, 'The quick red rabbit jumps over the tired tiger.'); + boolArray = results[1]; + resultStr = '${results[0]}\t${boolArray[0]}\t${boolArray[1]}'; + Expect.equals('That quick red rabbit jumped over a tired tiger.\ttrue\ttrue', resultStr, 'patch_apply: Partial match.'); + + results = dmp.patch_apply(patches, 'I am the very model of a modern major general.'); + boolArray = results[1]; + resultStr = '${results[0]}\t${boolArray[0]}\t${boolArray[1]}'; + Expect.equals('I am the very model of a modern major general.\tfalse\tfalse', resultStr, 'patch_apply: Failed match.'); + + patches = dmp.patch_make('x1234567890123456789012345678901234567890123456789012345678901234567890y', 'xabcy'); + results = dmp.patch_apply(patches, 'x123456789012345678901234567890-----++++++++++-----123456789012345678901234567890y'); + boolArray = results[1]; + resultStr = '${results[0]}\t${boolArray[0]}\t${boolArray[1]}'; + Expect.equals('xabcy\ttrue\ttrue', resultStr, 'patch_apply: Big delete, small change.'); + + patches = dmp.patch_make('x1234567890123456789012345678901234567890123456789012345678901234567890y', 'xabcy'); + results = dmp.patch_apply(patches, 'x12345678901234567890---------------++++++++++---------------12345678901234567890y'); + boolArray = results[1]; + resultStr = '${results[0]}\t${boolArray[0]}\t${boolArray[1]}'; + Expect.equals('xabc12345678901234567890---------------++++++++++---------------12345678901234567890y\tfalse\ttrue', resultStr, 'patch_apply: Big delete, big change 1.'); + + dmp.Patch_DeleteThreshold = 0.6; + patches = dmp.patch_make('x1234567890123456789012345678901234567890123456789012345678901234567890y', 'xabcy'); + results = dmp.patch_apply(patches, 'x12345678901234567890---------------++++++++++---------------12345678901234567890y'); + boolArray = results[1]; + resultStr = '${results[0]}\t${boolArray[0]}\t${boolArray[1]}'; + Expect.equals('xabcy\ttrue\ttrue', resultStr, 'patch_apply: Big delete, big change 2.'); + dmp.Patch_DeleteThreshold = 0.5; + + // Compensate for failed patch. + dmp.Match_Threshold = 0.0; + dmp.Match_Distance = 0; + patches = dmp.patch_make('abcdefghijklmnopqrstuvwxyz--------------------1234567890', 'abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------1234567YYYYYYYYYY890'); + results = dmp.patch_apply(patches, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890'); + boolArray = results[1]; + resultStr = '${results[0]}\t${boolArray[0]}\t${boolArray[1]}'; + Expect.equals('ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890\tfalse\ttrue', resultStr, 'patch_apply: Compensate for failed patch.'); + dmp.Match_Threshold = 0.5; + dmp.Match_Distance = 1000; + + patches = dmp.patch_make('', 'test'); + String patchStr = dmp.patch_toText(patches); + dmp.patch_apply(patches, ''); + Expect.equals(patchStr, dmp.patch_toText(patches), 'patch_apply: No side effects.'); + + patches = dmp.patch_make('The quick brown fox jumps over the lazy dog.', 'Woof'); + patchStr = dmp.patch_toText(patches); + dmp.patch_apply(patches, 'The quick brown fox jumps over the lazy dog.'); + Expect.equals(patchStr, dmp.patch_toText(patches), 'patch_apply: No side effects with major delete.'); + + patches = dmp.patch_make('', 'test'); + results = dmp.patch_apply(patches, ''); + boolArray = results[1]; + resultStr = '${results[0]}\t${boolArray[0]}'; + Expect.equals('test\ttrue', resultStr, 'patch_apply: Edge exact match.'); + + patches = dmp.patch_make('XY', 'XtestY'); + results = dmp.patch_apply(patches, 'XY'); + boolArray = results[1]; + resultStr = '${results[0]}\t${boolArray[0]}'; + Expect.equals('XtestY\ttrue', resultStr, 'patch_apply: Near edge exact match.'); + + patches = dmp.patch_make('y', 'y123'); + results = dmp.patch_apply(patches, 'x'); + boolArray = results[1]; + resultStr = '${results[0]}\t${boolArray[0]}'; + Expect.equals('x123\ttrue', resultStr, 'patch_apply: Edge partial match.'); +} + +// Run each test. +// TODO: Use the Dart unit test framework (once it is published). +main() { + dmp = new DiffMatchPatch(); + + testDiffCommonPrefix(); + testDiffCommonSuffix(); + testDiffCommonOverlap(); + testDiffHalfmatch(); + testDiffLinesToChars(); + testDiffCharsToLines(); + testDiffCleanupMerge(); + testDiffCleanupSemanticLossless(); + testDiffCleanupSemantic(); + testDiffCleanupEfficiency(); + testDiffPrettyHtml(); + testDiffText(); + testDiffDelta(); + testDiffXIndex(); + testDiffLevenshtein(); + testDiffBisect(); + testDiffMain(); + + testMatchAlphabet(); + testMatchBitap(); + testMatchMain(); + + testPatchObj(); + testPatchFromText(); + testPatchToText(); + testPatchAddContext(); + testPatchMake(); + testPatchSplitMax(); + testPatchAddPadding(); + testPatchApply(); + + print('All tests passed.'); +} diff --git a/dart/PatchClass.dart b/dart/PatchClass.dart new file mode 100644 index 0000000..9b9d404 --- /dev/null +++ b/dart/PatchClass.dart @@ -0,0 +1,78 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +part of DiffMatchPatch; + +/** + * Class representing one patch operation. + */ +class Patch { + List diffs; + int start1; + int start2; + int length1 = 0; + int length2 = 0; + + /** + * Constructor. Initializes with an empty list of diffs. + */ + Patch() { + this.diffs = []; + } + + /** + * Emmulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indicies are printed as 1-based, not 0-based. + * Returns the GNU diff string. + */ + String toString() { + String coords1, coords2; + if (this.length1 == 0) { + coords1 = '${this.start1},0'; + } else if (this.length1 == 1) { + coords1 = (this.start1 + 1).toString(); + } else { + coords1 = '${this.start1 + 1},${this.length1}'; + } + if (this.length2 == 0) { + coords2 = '${this.start2},0'; + } else if (this.length2 == 1) { + coords2 = (this.start2 + 1).toString(); + } else { + coords2 = '${this.start2 + 1},${this.length2}'; + } + final text = new StringBuffer('@@ -$coords1 +$coords2 @@\n'); + // Escape the body of the patch with %xx notation. + for (Diff aDiff in this.diffs) { + switch (aDiff.operation) { + case DIFF_INSERT: + text.add('+'); + break; + case DIFF_DELETE: + text.add('-'); + break; + case DIFF_EQUAL: + text.add(' '); + break; + } + text.add(encodeUri(aDiff.text)).add('\n'); + } + return text.toString().replaceAll('%20', ' '); + } +} diff --git a/dart/README.txt b/dart/README.txt new file mode 100644 index 0000000..2f05cc1 --- /dev/null +++ b/dart/README.txt @@ -0,0 +1,13 @@ +Warning: + +The Dart language is currently (as of December 2011) a Technology Preview. +Until Dart has its first official release, this port of Diff Match Patch may +change from version to version to take advantage of new Dart features. + +Therefore if you use this Diff Match Patch library there may be API changes +between versions and you may need to make minor updates to your code. + +For example, if Dart adds enums, then DIFF_INSERT/DIFF_DELETE/DIFF_EQUAL will +become an enum. + +-- Neil \ No newline at end of file diff --git a/demos/demo_diff.html b/demos/demo_diff.html new file mode 100644 index 0000000..97a11ce --- /dev/null +++ b/demos/demo_diff.html @@ -0,0 +1,87 @@ + + + + Diff, Match and Patch: Demo of Diff + + + + +

Diff, Match and Patch

+

Demo of Diff

+ +

Diff takes two texts and finds the differences. This implementation works on a character by character basis. +The result of any diff may contain 'chaff', irrelevant small commonalities which complicate the output. +A post-diff cleanup algorithm factors out these trivial commonalities.

+ + + +
+ + + +
+

Text Version 1:

+
+

Text Version 2:

+
+ +

Diff timeout:

+

seconds
+If the mapping phase of the diff computation takes longer than this, then the computation +is truncated and the best solution to date is returned. While guaranteed to be correct, +it may not be optimal. A timeout of '0' allows for unlimited computation.

+ +

Post-diff cleanup:

+
+
+
+
Increase human readability by factoring out commonalities which are likely to be +coincidental.
+
+, +edit cost: +
Increase computational efficiency by factoring out short commonalities which are +not worth the overhead. The larger the edit cost, the more agressive the cleanup.
+
+
+
Raw output.
+
+ +

+
+ +
+ +
+Back to Diff, Match and Patch + + + diff --git a/demos/demo_match.html b/demos/demo_match.html new file mode 100644 index 0000000..249467a --- /dev/null +++ b/demos/demo_match.html @@ -0,0 +1,92 @@ + + + + Diff, Match and Patch: Demo of Match + + + + +

Diff, Match and Patch

+

Demo of Match

+ +

Match looks for a pattern within a larger text. +This implementation of match is fuzzy, meaning it can find a match even if the +pattern contains errors and doesn't exactly match what is found in the text. +This implementation also accepts an expected location, near which the match should be found. +The candidate matches are scored based on a) the number of spelling differences between the +pattern and the text and b) the distance between the candidate match and the expected location. +The match distance parameter sets the relative importance of these two metrics.

+ +
+

Text:

+ + +

Fuzzy pattern:

+


+Aproximate pattern to search for in the text. Due to limitations of the Bitap algorithm, the pattern has a limited length.

+ +

Fuzzy location:

+


+Aproximately where in the text is the pattern expected to be found?

+ +

Match distance:

+


+Determines how close the match must be to the fuzzy location (specified above). An exact letter match which is 'distance' characters away from the fuzzy location would +score as a complete mismatch. A distance of '0' requires the match be at the exact location specified, a threshold of '1000' +would require a perfect match to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.

+ +

Match threshold:

+


+At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match (of both letters and location), a threshold of '1.0' would match anything.

+ + +
+ +
+ +
+ + + +
+Back to Diff, Match and Patch + + + diff --git a/demos/demo_patch.html b/demos/demo_patch.html new file mode 100644 index 0000000..845808a --- /dev/null +++ b/demos/demo_patch.html @@ -0,0 +1,121 @@ + + + + Diff, Match and Patch: Demo of Patch + + + + +

Diff, Match and Patch

+

Demo of Patch

+ +

Two texts can be diffed against each other, generating a list of patches. +These patches can then be applied against a third text. If the third text has edits of its own, this version of patch +will apply its changes on a best-effort basis, reporting which patches succeeded and which failed.

+ +

In this scenario Shakespeare wrote Hamlet in Early Modern English, the source document. Then two derivative +works were created. One is Hamlet updated to Modern English. The other is a Star Trek parody in Early Modern English. +This demonstrantion creates a list of patches between the source and the Modern English version. Then it +applies those patches onto the Star Trek parody, thus creating a Star Trek parody in +Modern English.

+ + + +
+

Shakespeare's copy:

+ + + +
Old Version:
New Version:
+

+
+ +
+ +

Trekkie's copy:

+ + + +
Old Version:
New Version:
+ +
+ +
    +
    + +
    +Back to Diff, Match and Patch + + + + diff --git a/java/name/fraser/neil/plaintext/diff_match_patch.java b/java/name/fraser/neil/plaintext/diff_match_patch.java new file mode 100644 index 0000000..f4ea2a4 --- /dev/null +++ b/java/name/fraser/neil/plaintext/diff_match_patch.java @@ -0,0 +1,2470 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package name.fraser.neil.plaintext; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Stack; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/* + * Functions for diff, match and patch. + * Computes the difference between two texts to create a patch. + * Applies the patch onto another text, allowing for errors. + * + * @author fraser@google.com (Neil Fraser) + */ + +/** + * Class containing the diff, match and patch methods. + * Also contains the behaviour settings. + */ +public class diff_match_patch { + + // Defaults. + // Set these on your diff_match_patch instance to override the defaults. + + /** + * Number of seconds to map a diff before giving up (0 for infinity). + */ + public float Diff_Timeout = 1.0f; + /** + * Cost of an empty edit operation in terms of edit characters. + */ + public short Diff_EditCost = 4; + /** + * At what point is no match declared (0.0 = perfection, 1.0 = very loose). + */ + public float Match_Threshold = 0.5f; + /** + * How far to search for a match (0 = exact location, 1000+ = broad match). + * A match this many characters away from the expected location will add + * 1.0 to the score (0.0 is a perfect match). + */ + public int Match_Distance = 1000; + /** + * When deleting a large block of text (over ~64 characters), how close do + * the contents have to be to match the expected contents. (0.0 = perfection, + * 1.0 = very loose). Note that Match_Threshold controls how closely the + * end points of a delete need to match. + */ + public float Patch_DeleteThreshold = 0.5f; + /** + * Chunk size for context length. + */ + public short Patch_Margin = 4; + + /** + * The number of bits in an int. + */ + private short Match_MaxBits = 32; + + /** + * Internal class for returning results from diff_linesToChars(). + * Other less paranoid languages just use a three-element array. + */ + protected static class LinesToCharsResult { + protected String chars1; + protected String chars2; + protected List lineArray; + + protected LinesToCharsResult(String chars1, String chars2, + List lineArray) { + this.chars1 = chars1; + this.chars2 = chars2; + this.lineArray = lineArray; + } + } + + + // DIFF FUNCTIONS + + + /** + * The data structure representing a diff is a Linked list of Diff objects: + * {Diff(Operation.DELETE, "Hello"), Diff(Operation.INSERT, "Goodbye"), + * Diff(Operation.EQUAL, " world.")} + * which means: delete "Hello", add "Goodbye" and keep " world." + */ + public enum Operation { + DELETE, INSERT, EQUAL + } + + /** + * Find the differences between two texts. + * Run a faster, slightly less optimal diff. + * This method allows the 'checklines' of diff_main() to be optional. + * Most of the time checklines is wanted, so default to true. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @return Linked List of Diff objects. + */ + public LinkedList diff_main(String text1, String text2) { + return diff_main(text1, text2, true); + } + + /** + * Find the differences between two texts. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @return Linked List of Diff objects. + */ + public LinkedList diff_main(String text1, String text2, + boolean checklines) { + // Set a deadline by which time the diff must be complete. + long deadline; + if (Diff_Timeout <= 0) { + deadline = Long.MAX_VALUE; + } else { + deadline = System.currentTimeMillis() + (long) (Diff_Timeout * 1000); + } + return diff_main(text1, text2, checklines, deadline); + } + + /** + * Find the differences between two texts. Simplifies the problem by + * stripping any common prefix or suffix off the texts before diffing. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. Used + * internally for recursive calls. Users should set DiffTimeout instead. + * @return Linked List of Diff objects. + */ + private LinkedList diff_main(String text1, String text2, + boolean checklines, long deadline) { + // Check for null inputs. + if (text1 == null || text2 == null) { + throw new IllegalArgumentException("Null inputs. (diff_main)"); + } + + // Check for equality (speedup). + LinkedList diffs; + if (text1.equals(text2)) { + diffs = new LinkedList(); + if (text1.length() != 0) { + diffs.add(new Diff(Operation.EQUAL, text1)); + } + return diffs; + } + + // Trim off common prefix (speedup). + int commonlength = diff_commonPrefix(text1, text2); + String commonprefix = text1.substring(0, commonlength); + text1 = text1.substring(commonlength); + text2 = text2.substring(commonlength); + + // Trim off common suffix (speedup). + commonlength = diff_commonSuffix(text1, text2); + String commonsuffix = text1.substring(text1.length() - commonlength); + text1 = text1.substring(0, text1.length() - commonlength); + text2 = text2.substring(0, text2.length() - commonlength); + + // Compute the diff on the middle block. + diffs = diff_compute(text1, text2, checklines, deadline); + + // Restore the prefix and suffix. + if (commonprefix.length() != 0) { + diffs.addFirst(new Diff(Operation.EQUAL, commonprefix)); + } + if (commonsuffix.length() != 0) { + diffs.addLast(new Diff(Operation.EQUAL, commonsuffix)); + } + + diff_cleanupMerge(diffs); + return diffs; + } + + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. + * @return Linked List of Diff objects. + */ + private LinkedList diff_compute(String text1, String text2, + boolean checklines, long deadline) { + LinkedList diffs = new LinkedList(); + + if (text1.length() == 0) { + // Just add some text (speedup). + diffs.add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + if (text2.length() == 0) { + // Just delete some text (speedup). + diffs.add(new Diff(Operation.DELETE, text1)); + return diffs; + } + + String longtext = text1.length() > text2.length() ? text1 : text2; + String shorttext = text1.length() > text2.length() ? text2 : text1; + int i = longtext.indexOf(shorttext); + if (i != -1) { + // Shorter text is inside the longer text (speedup). + Operation op = (text1.length() > text2.length()) ? + Operation.DELETE : Operation.INSERT; + diffs.add(new Diff(op, longtext.substring(0, i))); + diffs.add(new Diff(Operation.EQUAL, shorttext)); + diffs.add(new Diff(op, longtext.substring(i + shorttext.length()))); + return diffs; + } + + if (shorttext.length() == 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + diffs.add(new Diff(Operation.DELETE, text1)); + diffs.add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + // Check to see if the problem can be split in two. + String[] hm = diff_halfMatch(text1, text2); + if (hm != null) { + // A half-match was found, sort out the return data. + String text1_a = hm[0]; + String text1_b = hm[1]; + String text2_a = hm[2]; + String text2_b = hm[3]; + String mid_common = hm[4]; + // Send both pairs off for separate processing. + LinkedList diffs_a = diff_main(text1_a, text2_a, + checklines, deadline); + LinkedList diffs_b = diff_main(text1_b, text2_b, + checklines, deadline); + // Merge the results. + diffs = diffs_a; + diffs.add(new Diff(Operation.EQUAL, mid_common)); + diffs.addAll(diffs_b); + return diffs; + } + + if (checklines && text1.length() > 100 && text2.length() > 100) { + return diff_lineMode(text1, text2, deadline); + } + + return diff_bisect(text1, text2, deadline); + } + + /** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time when the diff should be complete by. + * @return Linked List of Diff objects. + */ + private LinkedList diff_lineMode(String text1, String text2, + long deadline) { + // Scan the text on a line-by-line basis first. + LinesToCharsResult b = diff_linesToChars(text1, text2); + text1 = b.chars1; + text2 = b.chars2; + List linearray = b.lineArray; + + LinkedList diffs = diff_main(text1, text2, false, deadline); + + // Convert the diff back to original text. + diff_charsToLines(diffs, linearray); + // Eliminate freak matches (e.g. blank lines) + diff_cleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.add(new Diff(Operation.EQUAL, "")); + int count_delete = 0; + int count_insert = 0; + String text_delete = ""; + String text_insert = ""; + ListIterator pointer = diffs.listIterator(); + Diff thisDiff = pointer.next(); + while (thisDiff != null) { + switch (thisDiff.operation) { + case INSERT: + count_insert++; + text_insert += thisDiff.text; + break; + case DELETE: + count_delete++; + text_delete += thisDiff.text; + break; + case EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + pointer.previous(); + for (int j = 0; j < count_delete + count_insert; j++) { + pointer.previous(); + pointer.remove(); + } + for (Diff newDiff : diff_main(text_delete, text_insert, false, + deadline)) { + pointer.add(newDiff); + } + } + count_insert = 0; + count_delete = 0; + text_delete = ""; + text_insert = ""; + break; + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + diffs.removeLast(); // Remove the dummy entry at the end. + + return diffs; + } + + /** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time at which to bail if not yet complete. + * @return LinkedList of Diff objects. + */ + protected LinkedList diff_bisect(String text1, String text2, + long deadline) { + // Cache the text lengths to prevent multiple calls. + int text1_length = text1.length(); + int text2_length = text2.length(); + int max_d = (text1_length + text2_length + 1) / 2; + int v_offset = max_d; + int v_length = 2 * max_d; + int[] v1 = new int[v_length]; + int[] v2 = new int[v_length]; + for (int x = 0; x < v_length; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[v_offset + 1] = 0; + v2[v_offset + 1] = 0; + int delta = text1_length - text2_length; + // If the total number of characters is odd, then the front path will + // collide with the reverse path. + boolean front = (delta % 2 != 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + int k1start = 0; + int k1end = 0; + int k2start = 0; + int k2end = 0; + for (int d = 0; d < max_d; d++) { + // Bail out if deadline is reached. + if (System.currentTimeMillis() > deadline) { + break; + } + + // Walk the front path one step. + for (int k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + int k1_offset = v_offset + k1; + int x1; + if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { + x1 = v1[k1_offset + 1]; + } else { + x1 = v1[k1_offset - 1] + 1; + } + int y1 = x1 - k1; + while (x1 < text1_length && y1 < text2_length + && text1.charAt(x1) == text2.charAt(y1)) { + x1++; + y1++; + } + v1[k1_offset] = x1; + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + int k2_offset = v_offset + delta - k1; + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { + // Mirror x2 onto top-left coordinate system. + int x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return diff_bisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + + // Walk the reverse path one step. + for (int k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + int k2_offset = v_offset + k2; + int x2; + if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { + x2 = v2[k2_offset + 1]; + } else { + x2 = v2[k2_offset - 1] + 1; + } + int y2 = x2 - k2; + while (x2 < text1_length && y2 < text2_length + && text1.charAt(text1_length - x2 - 1) + == text2.charAt(text2_length - y2 - 1)) { + x2++; + y2++; + } + v2[k2_offset] = x2; + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + int k1_offset = v_offset + delta - k2; + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { + int x1 = v1[k1_offset]; + int y1 = v_offset + x1 - k1_offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2; + if (x1 >= x2) { + // Overlap detected. + return diff_bisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + LinkedList diffs = new LinkedList(); + diffs.add(new Diff(Operation.DELETE, text1)); + diffs.add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + /** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param x Index of split point in text1. + * @param y Index of split point in text2. + * @param deadline Time at which to bail if not yet complete. + * @return LinkedList of Diff objects. + */ + private LinkedList diff_bisectSplit(String text1, String text2, + int x, int y, long deadline) { + String text1a = text1.substring(0, x); + String text2a = text2.substring(0, y); + String text1b = text1.substring(x); + String text2b = text2.substring(y); + + // Compute both diffs serially. + LinkedList diffs = diff_main(text1a, text2a, false, deadline); + LinkedList diffsb = diff_main(text1b, text2b, false, deadline); + + diffs.addAll(diffsb); + return diffs; + } + + /** + * Split two texts into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text1 First string. + * @param text2 Second string. + * @return An object containing the encoded text1, the encoded text2 and + * the List of unique strings. The zeroth element of the List of + * unique strings is intentionally blank. + */ + protected LinesToCharsResult diff_linesToChars(String text1, String text2) { + List lineArray = new ArrayList(); + Map lineHash = new HashMap(); + // e.g. linearray[4] == "Hello\n" + // e.g. linehash.get("Hello\n") == 4 + + // "\x00" is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray.add(""); + + String chars1 = diff_linesToCharsMunge(text1, lineArray, lineHash); + String chars2 = diff_linesToCharsMunge(text2, lineArray, lineHash); + return new LinesToCharsResult(chars1, chars2, lineArray); + } + + /** + * Split a text into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text String to encode. + * @param lineArray List of unique strings. + * @param lineHash Map of strings to indices. + * @return Encoded string. + */ + private String diff_linesToCharsMunge(String text, List lineArray, + Map lineHash) { + int lineStart = 0; + int lineEnd = -1; + String line; + StringBuilder chars = new StringBuilder(); + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + while (lineEnd < text.length() - 1) { + lineEnd = text.indexOf('\n', lineStart); + if (lineEnd == -1) { + lineEnd = text.length() - 1; + } + line = text.substring(lineStart, lineEnd + 1); + lineStart = lineEnd + 1; + + if (lineHash.containsKey(line)) { + chars.append(String.valueOf((char) (int) lineHash.get(line))); + } else { + lineArray.add(line); + lineHash.put(line, lineArray.size() - 1); + chars.append(String.valueOf((char) (lineArray.size() - 1))); + } + } + return chars.toString(); + } + + /** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * @param diffs LinkedList of Diff objects. + * @param lineArray List of unique strings. + */ + protected void diff_charsToLines(LinkedList diffs, + List lineArray) { + StringBuilder text; + for (Diff diff : diffs) { + text = new StringBuilder(); + for (int y = 0; y < diff.text.length(); y++) { + text.append(lineArray.get(diff.text.charAt(y))); + } + diff.text = text.toString(); + } + } + + /** + * Determine the common prefix of two strings + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the start of each string. + */ + public int diff_commonPrefix(String text1, String text2) { + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + int n = Math.min(text1.length(), text2.length()); + for (int i = 0; i < n; i++) { + if (text1.charAt(i) != text2.charAt(i)) { + return i; + } + } + return n; + } + + /** + * Determine the common suffix of two strings + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of each string. + */ + public int diff_commonSuffix(String text1, String text2) { + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + int text1_length = text1.length(); + int text2_length = text2.length(); + int n = Math.min(text1_length, text2_length); + for (int i = 1; i <= n; i++) { + if (text1.charAt(text1_length - i) != text2.charAt(text2_length - i)) { + return i - 1; + } + } + return n; + } + + /** + * Determine if the suffix of one string is the prefix of another. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of the first + * string and the start of the second string. + */ + protected int diff_commonOverlap(String text1, String text2) { + // Cache the text lengths to prevent multiple calls. + int text1_length = text1.length(); + int text2_length = text2.length(); + // Eliminate the null case. + if (text1_length == 0 || text2_length == 0) { + return 0; + } + // Truncate the longer string. + if (text1_length > text2_length) { + text1 = text1.substring(text1_length - text2_length); + } else if (text1_length < text2_length) { + text2 = text2.substring(0, text1_length); + } + int text_length = Math.min(text1_length, text2_length); + // Quick check for the worst case. + if (text1.equals(text2)) { + return text_length; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: http://neil.fraser.name/news/2010/11/04/ + int best = 0; + int length = 1; + while (true) { + String pattern = text1.substring(text_length - length); + int found = text2.indexOf(pattern); + if (found == -1) { + return best; + } + length += found; + if (found == 0 || text1.substring(text_length - length).equals( + text2.substring(0, length))) { + best = length; + length++; + } + } + } + + /** + * Do the two texts share a substring which is at least half the length of + * the longer text? + * This speedup can produce non-minimal diffs. + * @param text1 First string. + * @param text2 Second string. + * @return Five element String array, containing the prefix of text1, the + * suffix of text1, the prefix of text2, the suffix of text2 and the + * common middle. Or null if there was no match. + */ + protected String[] diff_halfMatch(String text1, String text2) { + if (Diff_Timeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return null; + } + String longtext = text1.length() > text2.length() ? text1 : text2; + String shorttext = text1.length() > text2.length() ? text2 : text1; + if (longtext.length() < 4 || shorttext.length() * 2 < longtext.length()) { + return null; // Pointless. + } + + // First check if the second quarter is the seed for a half-match. + String[] hm1 = diff_halfMatchI(longtext, shorttext, + (longtext.length() + 3) / 4); + // Check again based on the third quarter. + String[] hm2 = diff_halfMatchI(longtext, shorttext, + (longtext.length() + 1) / 2); + String[] hm; + if (hm1 == null && hm2 == null) { + return null; + } else if (hm2 == null) { + hm = hm1; + } else if (hm1 == null) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].length() > hm2[4].length() ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + if (text1.length() > text2.length()) { + return hm; + //return new String[]{hm[0], hm[1], hm[2], hm[3], hm[4]}; + } else { + return new String[]{hm[2], hm[3], hm[0], hm[1], hm[4]}; + } + } + + /** + * Does a substring of shorttext exist within longtext such that the + * substring is at least half the length of longtext? + * @param longtext Longer string. + * @param shorttext Shorter string. + * @param i Start index of quarter length substring within longtext. + * @return Five element String array, containing the prefix of longtext, the + * suffix of longtext, the prefix of shorttext, the suffix of shorttext + * and the common middle. Or null if there was no match. + */ + private String[] diff_halfMatchI(String longtext, String shorttext, int i) { + // Start with a 1/4 length substring at position i as a seed. + String seed = longtext.substring(i, i + longtext.length() / 4); + int j = -1; + String best_common = ""; + String best_longtext_a = "", best_longtext_b = ""; + String best_shorttext_a = "", best_shorttext_b = ""; + while ((j = shorttext.indexOf(seed, j + 1)) != -1) { + int prefixLength = diff_commonPrefix(longtext.substring(i), + shorttext.substring(j)); + int suffixLength = diff_commonSuffix(longtext.substring(0, i), + shorttext.substring(0, j)); + if (best_common.length() < suffixLength + prefixLength) { + best_common = shorttext.substring(j - suffixLength, j) + + shorttext.substring(j, j + prefixLength); + best_longtext_a = longtext.substring(0, i - suffixLength); + best_longtext_b = longtext.substring(i + prefixLength); + best_shorttext_a = shorttext.substring(0, j - suffixLength); + best_shorttext_b = shorttext.substring(j + prefixLength); + } + } + if (best_common.length() * 2 >= longtext.length()) { + return new String[]{best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common}; + } else { + return null; + } + } + + /** + * Reduce the number of edits by eliminating semantically trivial equalities. + * @param diffs LinkedList of Diff objects. + */ + public void diff_cleanupSemantic(LinkedList diffs) { + if (diffs.isEmpty()) { + return; + } + boolean changes = false; + Stack equalities = new Stack(); // Stack of qualities. + String lastequality = null; // Always equal to equalities.lastElement().text + ListIterator pointer = diffs.listIterator(); + // Number of characters that changed prior to the equality. + int length_insertions1 = 0; + int length_deletions1 = 0; + // Number of characters that changed after the equality. + int length_insertions2 = 0; + int length_deletions2 = 0; + Diff thisDiff = pointer.next(); + while (thisDiff != null) { + if (thisDiff.operation == Operation.EQUAL) { + // Equality found. + equalities.push(thisDiff); + length_insertions1 = length_insertions2; + length_deletions1 = length_deletions2; + length_insertions2 = 0; + length_deletions2 = 0; + lastequality = thisDiff.text; + } else { + // An insertion or deletion. + if (thisDiff.operation == Operation.INSERT) { + length_insertions2 += thisDiff.text.length(); + } else { + length_deletions2 += thisDiff.text.length(); + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastequality != null && (lastequality.length() + <= Math.max(length_insertions1, length_deletions1)) + && (lastequality.length() + <= Math.max(length_insertions2, length_deletions2))) { + //System.out.println("Splitting: '" + lastequality + "'"); + // Walk back to offending equality. + while (thisDiff != equalities.lastElement()) { + thisDiff = pointer.previous(); + } + pointer.next(); + + // Replace equality with a delete. + pointer.set(new Diff(Operation.DELETE, lastequality)); + // Insert a corresponding an insert. + pointer.add(new Diff(Operation.INSERT, lastequality)); + + equalities.pop(); // Throw away the equality we just deleted. + if (!equalities.empty()) { + // Throw away the previous equality (it needs to be reevaluated). + equalities.pop(); + } + if (equalities.empty()) { + // There are no previous equalities, walk back to the start. + while (pointer.hasPrevious()) { + pointer.previous(); + } + } else { + // There is a safe equality we can fall back to. + thisDiff = equalities.lastElement(); + while (thisDiff != pointer.previous()) { + // Intentionally empty loop. + } + } + + length_insertions1 = 0; // Reset the counters. + length_insertions2 = 0; + length_deletions1 = 0; + length_deletions2 = 0; + lastequality = null; + changes = true; + } + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + + // Normalize the diff. + if (changes) { + diff_cleanupMerge(diffs); + } + diff_cleanupSemanticLossless(diffs); + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = diffs.listIterator(); + Diff prevDiff = null; + thisDiff = null; + if (pointer.hasNext()) { + prevDiff = pointer.next(); + if (pointer.hasNext()) { + thisDiff = pointer.next(); + } + } + while (thisDiff != null) { + if (prevDiff.operation == Operation.DELETE && + thisDiff.operation == Operation.INSERT) { + String deletion = prevDiff.text; + String insertion = thisDiff.text; + int overlap_length1 = this.diff_commonOverlap(deletion, insertion); + int overlap_length2 = this.diff_commonOverlap(insertion, deletion); + if (overlap_length1 >= overlap_length2) { + if (overlap_length1 >= deletion.length() / 2.0 || + overlap_length1 >= insertion.length() / 2.0) { + // Overlap found. Insert an equality and trim the surrounding edits. + pointer.previous(); + pointer.add(new Diff(Operation.EQUAL, + insertion.substring(0, overlap_length1))); + prevDiff.text = + deletion.substring(0, deletion.length() - overlap_length1); + thisDiff.text = insertion.substring(overlap_length1); + // pointer.add inserts the element before the cursor, so there is + // no need to step past the new element. + } + } else { + if (overlap_length2 >= deletion.length() / 2.0 || + overlap_length2 >= insertion.length() / 2.0) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + pointer.previous(); + pointer.add(new Diff(Operation.EQUAL, + deletion.substring(0, overlap_length2))); + prevDiff.operation = Operation.INSERT; + prevDiff.text = + insertion.substring(0, insertion.length() - overlap_length2); + thisDiff.operation = Operation.DELETE; + thisDiff.text = deletion.substring(overlap_length2); + // pointer.add inserts the element before the cursor, so there is + // no need to step past the new element. + } + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + prevDiff = thisDiff; + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + } + + /** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The cat came. -> The cat came. + * @param diffs LinkedList of Diff objects. + */ + public void diff_cleanupSemanticLossless(LinkedList diffs) { + String equality1, edit, equality2; + String commonString; + int commonOffset; + int score, bestScore; + String bestEquality1, bestEdit, bestEquality2; + // Create a new iterator at the start. + ListIterator pointer = diffs.listIterator(); + Diff prevDiff = pointer.hasNext() ? pointer.next() : null; + Diff thisDiff = pointer.hasNext() ? pointer.next() : null; + Diff nextDiff = pointer.hasNext() ? pointer.next() : null; + // Intentionally ignore the first and last element (don't need checking). + while (nextDiff != null) { + if (prevDiff.operation == Operation.EQUAL && + nextDiff.operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + equality1 = prevDiff.text; + edit = thisDiff.text; + equality2 = nextDiff.text; + + // First, shift the edit as far left as possible. + commonOffset = diff_commonSuffix(equality1, edit); + if (commonOffset != 0) { + commonString = edit.substring(edit.length() - commonOffset); + equality1 = equality1.substring(0, equality1.length() - commonOffset); + edit = commonString + edit.substring(0, edit.length() - commonOffset); + equality2 = commonString + equality2; + } + + // Second, step character by character right, looking for the best fit. + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + bestScore = diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2); + while (edit.length() != 0 && equality2.length() != 0 + && edit.charAt(0) == equality2.charAt(0)) { + equality1 += edit.charAt(0); + edit = edit.substring(1) + equality2.charAt(0); + equality2 = equality2.substring(1); + score = diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2); + // The >= encourages trailing rather than leading whitespace on edits. + if (score >= bestScore) { + bestScore = score; + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + } + } + + if (!prevDiff.text.equals(bestEquality1)) { + // We have an improvement, save it back to the diff. + if (bestEquality1.length() != 0) { + prevDiff.text = bestEquality1; + } else { + pointer.previous(); // Walk past nextDiff. + pointer.previous(); // Walk past thisDiff. + pointer.previous(); // Walk past prevDiff. + pointer.remove(); // Delete prevDiff. + pointer.next(); // Walk past thisDiff. + pointer.next(); // Walk past nextDiff. + } + thisDiff.text = bestEdit; + if (bestEquality2.length() != 0) { + nextDiff.text = bestEquality2; + } else { + pointer.remove(); // Delete nextDiff. + nextDiff = thisDiff; + thisDiff = prevDiff; + } + } + } + prevDiff = thisDiff; + thisDiff = nextDiff; + nextDiff = pointer.hasNext() ? pointer.next() : null; + } + } + + /** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 6 (best) to 0 (worst). + * @param one First string. + * @param two Second string. + * @return The score. + */ + private int diff_cleanupSemanticScore(String one, String two) { + if (one.length() == 0 || two.length() == 0) { + // Edges are the best. + return 6; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + char char1 = one.charAt(one.length() - 1); + char char2 = two.charAt(0); + boolean nonAlphaNumeric1 = !Character.isLetterOrDigit(char1); + boolean nonAlphaNumeric2 = !Character.isLetterOrDigit(char2); + boolean whitespace1 = nonAlphaNumeric1 && Character.isWhitespace(char1); + boolean whitespace2 = nonAlphaNumeric2 && Character.isWhitespace(char2); + boolean lineBreak1 = whitespace1 + && Character.getType(char1) == Character.CONTROL; + boolean lineBreak2 = whitespace2 + && Character.getType(char2) == Character.CONTROL; + boolean blankLine1 = lineBreak1 && BLANKLINEEND.matcher(one).find(); + boolean blankLine2 = lineBreak2 && BLANKLINESTART.matcher(two).find(); + + if (blankLine1 || blankLine2) { + // Five points for blank lines. + return 5; + } else if (lineBreak1 || lineBreak2) { + // Four points for line breaks. + return 4; + } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { + // Three points for end of sentences. + return 3; + } else if (whitespace1 || whitespace2) { + // Two points for whitespace. + return 2; + } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { + // One point for non-alphanumeric. + return 1; + } + return 0; + } + + // Define some regex patterns for matching boundaries. + private Pattern BLANKLINEEND + = Pattern.compile("\\n\\r?\\n\\Z", Pattern.DOTALL); + private Pattern BLANKLINESTART + = Pattern.compile("\\A\\r?\\n\\r?\\n", Pattern.DOTALL); + + /** + * Reduce the number of edits by eliminating operationally trivial equalities. + * @param diffs LinkedList of Diff objects. + */ + public void diff_cleanupEfficiency(LinkedList diffs) { + if (diffs.isEmpty()) { + return; + } + boolean changes = false; + Stack equalities = new Stack(); // Stack of equalities. + String lastequality = null; // Always equal to equalities.lastElement().text + ListIterator pointer = diffs.listIterator(); + // Is there an insertion operation before the last equality. + boolean pre_ins = false; + // Is there a deletion operation before the last equality. + boolean pre_del = false; + // Is there an insertion operation after the last equality. + boolean post_ins = false; + // Is there a deletion operation after the last equality. + boolean post_del = false; + Diff thisDiff = pointer.next(); + Diff safeDiff = thisDiff; // The last Diff that is known to be unsplitable. + while (thisDiff != null) { + if (thisDiff.operation == Operation.EQUAL) { + // Equality found. + if (thisDiff.text.length() < Diff_EditCost && (post_ins || post_del)) { + // Candidate found. + equalities.push(thisDiff); + pre_ins = post_ins; + pre_del = post_del; + lastequality = thisDiff.text; + } else { + // Not a candidate, and can never become one. + equalities.clear(); + lastequality = null; + safeDiff = thisDiff; + } + post_ins = post_del = false; + } else { + // An insertion or deletion. + if (thisDiff.operation == Operation.DELETE) { + post_del = true; + } else { + post_ins = true; + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if (lastequality != null + && ((pre_ins && pre_del && post_ins && post_del) + || ((lastequality.length() < Diff_EditCost / 2) + && ((pre_ins ? 1 : 0) + (pre_del ? 1 : 0) + + (post_ins ? 1 : 0) + (post_del ? 1 : 0)) == 3))) { + //System.out.println("Splitting: '" + lastequality + "'"); + // Walk back to offending equality. + while (thisDiff != equalities.lastElement()) { + thisDiff = pointer.previous(); + } + pointer.next(); + + // Replace equality with a delete. + pointer.set(new Diff(Operation.DELETE, lastequality)); + // Insert a corresponding an insert. + pointer.add(thisDiff = new Diff(Operation.INSERT, lastequality)); + + equalities.pop(); // Throw away the equality we just deleted. + lastequality = null; + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_ins = post_del = true; + equalities.clear(); + safeDiff = thisDiff; + } else { + if (!equalities.empty()) { + // Throw away the previous equality (it needs to be reevaluated). + equalities.pop(); + } + if (equalities.empty()) { + // There are no previous questionable equalities, + // walk back to the last known safe diff. + thisDiff = safeDiff; + } else { + // There is an equality we can fall back to. + thisDiff = equalities.lastElement(); + } + while (thisDiff != pointer.previous()) { + // Intentionally empty loop. + } + post_ins = post_del = false; + } + + changes = true; + } + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + + if (changes) { + diff_cleanupMerge(diffs); + } + } + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param diffs LinkedList of Diff objects. + */ + public void diff_cleanupMerge(LinkedList diffs) { + diffs.add(new Diff(Operation.EQUAL, "")); // Add a dummy entry at the end. + ListIterator pointer = diffs.listIterator(); + int count_delete = 0; + int count_insert = 0; + String text_delete = ""; + String text_insert = ""; + Diff thisDiff = pointer.next(); + Diff prevEqual = null; + int commonlength; + while (thisDiff != null) { + switch (thisDiff.operation) { + case INSERT: + count_insert++; + text_insert += thisDiff.text; + prevEqual = null; + break; + case DELETE: + count_delete++; + text_delete += thisDiff.text; + prevEqual = null; + break; + case EQUAL: + if (count_delete + count_insert > 1) { + boolean both_types = count_delete != 0 && count_insert != 0; + // Delete the offending records. + pointer.previous(); // Reverse direction. + while (count_delete-- > 0) { + pointer.previous(); + pointer.remove(); + } + while (count_insert-- > 0) { + pointer.previous(); + pointer.remove(); + } + if (both_types) { + // Factor out any common prefixies. + commonlength = diff_commonPrefix(text_insert, text_delete); + if (commonlength != 0) { + if (pointer.hasPrevious()) { + thisDiff = pointer.previous(); + assert thisDiff.operation == Operation.EQUAL + : "Previous diff should have been an equality."; + thisDiff.text += text_insert.substring(0, commonlength); + pointer.next(); + } else { + pointer.add(new Diff(Operation.EQUAL, + text_insert.substring(0, commonlength))); + } + text_insert = text_insert.substring(commonlength); + text_delete = text_delete.substring(commonlength); + } + // Factor out any common suffixies. + commonlength = diff_commonSuffix(text_insert, text_delete); + if (commonlength != 0) { + thisDiff = pointer.next(); + thisDiff.text = text_insert.substring(text_insert.length() + - commonlength) + thisDiff.text; + text_insert = text_insert.substring(0, text_insert.length() + - commonlength); + text_delete = text_delete.substring(0, text_delete.length() + - commonlength); + pointer.previous(); + } + } + // Insert the merged records. + if (text_delete.length() != 0) { + pointer.add(new Diff(Operation.DELETE, text_delete)); + } + if (text_insert.length() != 0) { + pointer.add(new Diff(Operation.INSERT, text_insert)); + } + // Step forward to the equality. + thisDiff = pointer.hasNext() ? pointer.next() : null; + } else if (prevEqual != null) { + // Merge this equality with the previous one. + prevEqual.text += thisDiff.text; + pointer.remove(); + thisDiff = pointer.previous(); + pointer.next(); // Forward direction + } + count_insert = 0; + count_delete = 0; + text_delete = ""; + text_insert = ""; + prevEqual = thisDiff; + break; + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + if (diffs.getLast().text.length() == 0) { + diffs.removeLast(); // Remove the dummy entry at the end. + } + + /* + * Second pass: look for single edits surrounded on both sides by equalities + * which can be shifted sideways to eliminate an equality. + * e.g: ABAC -> ABAC + */ + boolean changes = false; + // Create a new iterator at the start. + // (As opposed to walking the current one back.) + pointer = diffs.listIterator(); + Diff prevDiff = pointer.hasNext() ? pointer.next() : null; + thisDiff = pointer.hasNext() ? pointer.next() : null; + Diff nextDiff = pointer.hasNext() ? pointer.next() : null; + // Intentionally ignore the first and last element (don't need checking). + while (nextDiff != null) { + if (prevDiff.operation == Operation.EQUAL && + nextDiff.operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + if (thisDiff.text.endsWith(prevDiff.text)) { + // Shift the edit over the previous equality. + thisDiff.text = prevDiff.text + + thisDiff.text.substring(0, thisDiff.text.length() + - prevDiff.text.length()); + nextDiff.text = prevDiff.text + nextDiff.text; + pointer.previous(); // Walk past nextDiff. + pointer.previous(); // Walk past thisDiff. + pointer.previous(); // Walk past prevDiff. + pointer.remove(); // Delete prevDiff. + pointer.next(); // Walk past thisDiff. + thisDiff = pointer.next(); // Walk past nextDiff. + nextDiff = pointer.hasNext() ? pointer.next() : null; + changes = true; + } else if (thisDiff.text.startsWith(nextDiff.text)) { + // Shift the edit over the next equality. + prevDiff.text += nextDiff.text; + thisDiff.text = thisDiff.text.substring(nextDiff.text.length()) + + nextDiff.text; + pointer.remove(); // Delete nextDiff. + nextDiff = pointer.hasNext() ? pointer.next() : null; + changes = true; + } + } + prevDiff = thisDiff; + thisDiff = nextDiff; + nextDiff = pointer.hasNext() ? pointer.next() : null; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + diff_cleanupMerge(diffs); + } + } + + /** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. "The cat" vs "The big cat", 1->1, 5->8 + * @param diffs LinkedList of Diff objects. + * @param loc Location within text1. + * @return Location within text2. + */ + public int diff_xIndex(LinkedList diffs, int loc) { + int chars1 = 0; + int chars2 = 0; + int last_chars1 = 0; + int last_chars2 = 0; + Diff lastDiff = null; + for (Diff aDiff : diffs) { + if (aDiff.operation != Operation.INSERT) { + // Equality or deletion. + chars1 += aDiff.text.length(); + } + if (aDiff.operation != Operation.DELETE) { + // Equality or insertion. + chars2 += aDiff.text.length(); + } + if (chars1 > loc) { + // Overshot the location. + lastDiff = aDiff; + break; + } + last_chars1 = chars1; + last_chars2 = chars2; + } + if (lastDiff != null && lastDiff.operation == Operation.DELETE) { + // The location was deleted. + return last_chars2; + } + // Add the remaining character length. + return last_chars2 + (loc - last_chars1); + } + + /** + * Convert a Diff list into a pretty HTML report. + * @param diffs LinkedList of Diff objects. + * @return HTML representation. + */ + public String diff_prettyHtml(LinkedList diffs) { + StringBuilder html = new StringBuilder(); + for (Diff aDiff : diffs) { + String text = aDiff.text.replace("&", "&").replace("<", "<") + .replace(">", ">").replace("\n", "¶
    "); + switch (aDiff.operation) { + case INSERT: + html.append("").append(text) + .append(""); + break; + case DELETE: + html.append("").append(text) + .append(""); + break; + case EQUAL: + html.append("").append(text).append(""); + break; + } + } + return html.toString(); + } + + /** + * Compute and return the source text (all equalities and deletions). + * @param diffs LinkedList of Diff objects. + * @return Source text. + */ + public String diff_text1(LinkedList diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + if (aDiff.operation != Operation.INSERT) { + text.append(aDiff.text); + } + } + return text.toString(); + } + + /** + * Compute and return the destination text (all equalities and insertions). + * @param diffs LinkedList of Diff objects. + * @return Destination text. + */ + public String diff_text2(LinkedList diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + if (aDiff.operation != Operation.DELETE) { + text.append(aDiff.text); + } + } + return text.toString(); + } + + /** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * @param diffs LinkedList of Diff objects. + * @return Number of changes. + */ + public int diff_levenshtein(LinkedList diffs) { + int levenshtein = 0; + int insertions = 0; + int deletions = 0; + for (Diff aDiff : diffs) { + switch (aDiff.operation) { + case INSERT: + insertions += aDiff.text.length(); + break; + case DELETE: + deletions += aDiff.text.length(); + break; + case EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += Math.max(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += Math.max(insertions, deletions); + return levenshtein; + } + + /** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx notation. + * @param diffs Array of Diff objects. + * @return Delta text. + */ + public String diff_toDelta(LinkedList diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + switch (aDiff.operation) { + case INSERT: + try { + text.append("+").append(URLEncoder.encode(aDiff.text, "UTF-8") + .replace('+', ' ')).append("\t"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } + break; + case DELETE: + text.append("-").append(aDiff.text.length()).append("\t"); + break; + case EQUAL: + text.append("=").append(aDiff.text.length()).append("\t"); + break; + } + } + String delta = text.toString(); + if (delta.length() != 0) { + // Strip off trailing tab character. + delta = delta.substring(0, delta.length() - 1); + delta = unescapeForEncodeUriCompatability(delta); + } + return delta; + } + + /** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, compute the full diff. + * @param text1 Source string for the diff. + * @param delta Delta text. + * @return Array of Diff objects or null if invalid. + * @throws IllegalArgumentException If invalid input. + */ + public LinkedList diff_fromDelta(String text1, String delta) + throws IllegalArgumentException { + LinkedList diffs = new LinkedList(); + int pointer = 0; // Cursor in text1 + String[] tokens = delta.split("\t"); + for (String token : tokens) { + if (token.length() == 0) { + // Blank tokens are ok (from a trailing \t). + continue; + } + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + String param = token.substring(1); + switch (token.charAt(0)) { + case '+': + // decode would change all "+" to " " + param = param.replace("+", "%2B"); + try { + param = URLDecoder.decode(param, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } catch (IllegalArgumentException e) { + // Malformed URI sequence. + throw new IllegalArgumentException( + "Illegal escape in diff_fromDelta: " + param, e); + } + diffs.add(new Diff(Operation.INSERT, param)); + break; + case '-': + // Fall through. + case '=': + int n; + try { + n = Integer.parseInt(param); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Invalid number in diff_fromDelta: " + param, e); + } + if (n < 0) { + throw new IllegalArgumentException( + "Negative number in diff_fromDelta: " + param); + } + String text; + try { + text = text1.substring(pointer, pointer += n); + } catch (StringIndexOutOfBoundsException e) { + throw new IllegalArgumentException("Delta length (" + pointer + + ") larger than source text length (" + text1.length() + + ").", e); + } + if (token.charAt(0) == '=') { + diffs.add(new Diff(Operation.EQUAL, text)); + } else { + diffs.add(new Diff(Operation.DELETE, text)); + } + break; + default: + // Anything else is an error. + throw new IllegalArgumentException( + "Invalid diff operation in diff_fromDelta: " + token.charAt(0)); + } + } + if (pointer != text1.length()) { + throw new IllegalArgumentException("Delta length (" + pointer + + ") smaller than source text length (" + text1.length() + ")."); + } + return diffs; + } + + + // MATCH FUNCTIONS + + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + public int match_main(String text, String pattern, int loc) { + // Check for null inputs. + if (text == null || pattern == null) { + throw new IllegalArgumentException("Null inputs. (match_main)"); + } + + loc = Math.max(0, Math.min(loc, text.length())); + if (text.equals(pattern)) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (text.length() == 0) { + // Nothing to match. + return -1; + } else if (loc + pattern.length() <= text.length() + && text.substring(loc, loc + pattern.length()).equals(pattern)) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return loc; + } else { + // Do a fuzzy compare. + return match_bitap(text, pattern, loc); + } + } + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + protected int match_bitap(String text, String pattern, int loc) { + assert (Match_MaxBits == 0 || pattern.length() <= Match_MaxBits) + : "Pattern too long for this application."; + + // Initialise the alphabet. + Map s = match_alphabet(pattern); + + // Highest score beyond which we give up. + double score_threshold = Match_Threshold; + // Is there a nearby exact match? (speedup) + int best_loc = text.indexOf(pattern, loc); + if (best_loc != -1) { + score_threshold = Math.min(match_bitapScore(0, best_loc, loc, pattern), + score_threshold); + // What about in the other direction? (speedup) + best_loc = text.lastIndexOf(pattern, loc + pattern.length()); + if (best_loc != -1) { + score_threshold = Math.min(match_bitapScore(0, best_loc, loc, pattern), + score_threshold); + } + } + + // Initialise the bit arrays. + int matchmask = 1 << (pattern.length() - 1); + best_loc = -1; + + int bin_min, bin_mid; + int bin_max = pattern.length() + text.length(); + // Empty initialization added to appease Java compiler. + int[] last_rd = new int[0]; + for (int d = 0; d < pattern.length(); d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at + // this error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (match_bitapScore(d, loc + bin_mid, loc, pattern) + <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = (bin_max - bin_min) / 2 + bin_min; + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + int start = Math.max(1, loc - bin_mid + 1); + int finish = Math.min(loc + bin_mid, text.length()) + pattern.length(); + + int[] rd = new int[finish + 2]; + rd[finish + 1] = (1 << d) - 1; + for (int j = finish; j >= start; j--) { + int charMatch; + if (text.length() <= j - 1 || !s.containsKey(text.charAt(j - 1))) { + // Out of range. + charMatch = 0; + } else { + charMatch = s.get(text.charAt(j - 1)); + } + if (d == 0) { + // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { + // Subsequent passes: fuzzy match. + rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) + | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]; + } + if ((rd[j] & matchmask) != 0) { + double score = match_bitapScore(d, j - 1, loc, pattern); + // This match will almost certainly be better than any existing + // match. But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = Math.max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + if (match_bitapScore(d + 1, loc, loc, pattern) > score_threshold) { + // No hope for a (better) match at greater error levels. + break; + } + last_rd = rd; + } + return best_loc; + } + + /** + * Compute and return the score for a match with e errors and x location. + * @param e Number of errors in match. + * @param x Location of match. + * @param loc Expected location of match. + * @param pattern Pattern being sought. + * @return Overall score for match (0.0 = good, 1.0 = bad). + */ + private double match_bitapScore(int e, int x, int loc, String pattern) { + float accuracy = (float) e / pattern.length(); + int proximity = Math.abs(loc - x); + if (Match_Distance == 0) { + // Dodge divide by zero error. + return proximity == 0 ? accuracy : 1.0; + } + return accuracy + (proximity / (float) Match_Distance); + } + + /** + * Initialise the alphabet for the Bitap algorithm. + * @param pattern The text to encode. + * @return Hash of character locations. + */ + protected Map match_alphabet(String pattern) { + Map s = new HashMap(); + char[] char_pattern = pattern.toCharArray(); + for (char c : char_pattern) { + s.put(c, 0); + } + int i = 0; + for (char c : char_pattern) { + s.put(c, s.get(c) | (1 << (pattern.length() - i - 1))); + i++; + } + return s; + } + + + // PATCH FUNCTIONS + + + /** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * @param patch The patch to grow. + * @param text Source text. + */ + protected void patch_addContext(Patch patch, String text) { + if (text.length() == 0) { + return; + } + String pattern = text.substring(patch.start2, patch.start2 + patch.length1); + int padding = 0; + + // Look for the first and last matches of pattern in text. If two different + // matches are found, increase the pattern length. + while (text.indexOf(pattern) != text.lastIndexOf(pattern) + && pattern.length() < Match_MaxBits - Patch_Margin - Patch_Margin) { + padding += Patch_Margin; + pattern = text.substring(Math.max(0, patch.start2 - padding), + Math.min(text.length(), patch.start2 + patch.length1 + padding)); + } + // Add one chunk for good luck. + padding += Patch_Margin; + + // Add the prefix. + String prefix = text.substring(Math.max(0, patch.start2 - padding), + patch.start2); + if (prefix.length() != 0) { + patch.diffs.addFirst(new Diff(Operation.EQUAL, prefix)); + } + // Add the suffix. + String suffix = text.substring(patch.start2 + patch.length1, + Math.min(text.length(), patch.start2 + patch.length1 + padding)); + if (suffix.length() != 0) { + patch.diffs.addLast(new Diff(Operation.EQUAL, suffix)); + } + + // Roll back the start points. + patch.start1 -= prefix.length(); + patch.start2 -= prefix.length(); + // Extend the lengths. + patch.length1 += prefix.length() + suffix.length(); + patch.length2 += prefix.length() + suffix.length(); + } + + /** + * Compute a list of patches to turn text1 into text2. + * A set of diffs will be computed. + * @param text1 Old text. + * @param text2 New text. + * @return LinkedList of Patch objects. + */ + public LinkedList patch_make(String text1, String text2) { + if (text1 == null || text2 == null) { + throw new IllegalArgumentException("Null inputs. (patch_make)"); + } + // No diffs provided, compute our own. + LinkedList diffs = diff_main(text1, text2, true); + if (diffs.size() > 2) { + diff_cleanupSemantic(diffs); + diff_cleanupEfficiency(diffs); + } + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text1 will be derived from the provided diffs. + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + */ + public LinkedList patch_make(LinkedList diffs) { + if (diffs == null) { + throw new IllegalArgumentException("Null inputs. (patch_make)"); + } + // No origin string provided, compute our own. + String text1 = diff_text1(diffs); + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is ignored, diffs are the delta between text1 and text2. + * @param text1 Old text + * @param text2 Ignored. + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + * @deprecated Prefer patch_make(String text1, LinkedList diffs). + */ + public LinkedList patch_make(String text1, String text2, + LinkedList diffs) { + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is not provided, diffs are the delta between text1 and text2. + * @param text1 Old text. + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + */ + public LinkedList patch_make(String text1, LinkedList diffs) { + if (text1 == null || diffs == null) { + throw new IllegalArgumentException("Null inputs. (patch_make)"); + } + + LinkedList patches = new LinkedList(); + if (diffs.isEmpty()) { + return patches; // Get rid of the null case. + } + Patch patch = new Patch(); + int char_count1 = 0; // Number of characters into the text1 string. + int char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + String prepatch_text = text1; + String postpatch_text = text1; + for (Diff aDiff : diffs) { + if (patch.diffs.isEmpty() && aDiff.operation != Operation.EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (aDiff.operation) { + case INSERT: + patch.diffs.add(aDiff); + patch.length2 += aDiff.text.length(); + postpatch_text = postpatch_text.substring(0, char_count2) + + aDiff.text + postpatch_text.substring(char_count2); + break; + case DELETE: + patch.length1 += aDiff.text.length(); + patch.diffs.add(aDiff); + postpatch_text = postpatch_text.substring(0, char_count2) + + postpatch_text.substring(char_count2 + aDiff.text.length()); + break; + case EQUAL: + if (aDiff.text.length() <= 2 * Patch_Margin + && !patch.diffs.isEmpty() && aDiff != diffs.getLast()) { + // Small equality inside a patch. + patch.diffs.add(aDiff); + patch.length1 += aDiff.text.length(); + patch.length2 += aDiff.text.length(); + } + + if (aDiff.text.length() >= 2 * Patch_Margin) { + // Time for a new patch. + if (!patch.diffs.isEmpty()) { + patch_addContext(patch, prepatch_text); + patches.add(patch); + patch = new Patch(); + // Unlike Unidiff, our patch lists have a rolling context. + // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (aDiff.operation != Operation.INSERT) { + char_count1 += aDiff.text.length(); + } + if (aDiff.operation != Operation.DELETE) { + char_count2 += aDiff.text.length(); + } + } + // Pick up the leftover patch if not empty. + if (!patch.diffs.isEmpty()) { + patch_addContext(patch, prepatch_text); + patches.add(patch); + } + + return patches; + } + + /** + * Given an array of patches, return another array that is identical. + * @param patches Array of Patch objects. + * @return Array of Patch objects. + */ + public LinkedList patch_deepCopy(LinkedList patches) { + LinkedList patchesCopy = new LinkedList(); + for (Patch aPatch : patches) { + Patch patchCopy = new Patch(); + for (Diff aDiff : aPatch.diffs) { + Diff diffCopy = new Diff(aDiff.operation, aDiff.text); + patchCopy.diffs.add(diffCopy); + } + patchCopy.start1 = aPatch.start1; + patchCopy.start2 = aPatch.start2; + patchCopy.length1 = aPatch.length1; + patchCopy.length2 = aPatch.length2; + patchesCopy.add(patchCopy); + } + return patchesCopy; + } + + /** + * Merge a set of patches onto the text. Return a patched text, as well + * as an array of true/false values indicating which patches were applied. + * @param patches Array of Patch objects + * @param text Old text. + * @return Two element Object array, containing the new text and an array of + * boolean values. + */ + public Object[] patch_apply(LinkedList patches, String text) { + if (patches.isEmpty()) { + return new Object[]{text, new boolean[0]}; + } + + // Deep copy the patches so that no changes are made to originals. + patches = patch_deepCopy(patches); + + String nullPadding = patch_addPadding(patches); + text = nullPadding + text + nullPadding; + patch_splitMax(patches); + + int x = 0; + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + int delta = 0; + boolean[] results = new boolean[patches.size()]; + for (Patch aPatch : patches) { + int expected_loc = aPatch.start2 + delta; + String text1 = diff_text1(aPatch.diffs); + int start_loc; + int end_loc = -1; + if (text1.length() > this.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = match_main(text, + text1.substring(0, this.Match_MaxBits), expected_loc); + if (start_loc != -1) { + end_loc = match_main(text, + text1.substring(text1.length() - this.Match_MaxBits), + expected_loc + text1.length() - this.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= aPatch.length2 - aPatch.length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + String text2; + if (end_loc == -1) { + text2 = text.substring(start_loc, + Math.min(start_loc + text1.length(), text.length())); + } else { + text2 = text.substring(start_loc, + Math.min(end_loc + this.Match_MaxBits, text.length())); + } + if (text1.equals(text2)) { + // Perfect match, just shove the replacement text in. + text = text.substring(0, start_loc) + diff_text2(aPatch.diffs) + + text.substring(start_loc + text1.length()); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + LinkedList diffs = diff_main(text1, text2, false); + if (text1.length() > this.Match_MaxBits + && diff_levenshtein(diffs) / (float) text1.length() + > this.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + diff_cleanupSemanticLossless(diffs); + int index1 = 0; + for (Diff aDiff : aPatch.diffs) { + if (aDiff.operation != Operation.EQUAL) { + int index2 = diff_xIndex(diffs, index1); + if (aDiff.operation == Operation.INSERT) { + // Insertion + text = text.substring(0, start_loc + index2) + aDiff.text + + text.substring(start_loc + index2); + } else if (aDiff.operation == Operation.DELETE) { + // Deletion + text = text.substring(0, start_loc + index2) + + text.substring(start_loc + diff_xIndex(diffs, + index1 + aDiff.text.length())); + } + } + if (aDiff.operation != Operation.DELETE) { + index1 += aDiff.text.length(); + } + } + } + } + } + x++; + } + // Strip the padding off. + text = text.substring(nullPadding.length(), text.length() + - nullPadding.length()); + return new Object[]{text, results}; + } + + /** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * @param patches Array of Patch objects. + * @return The padding string added to each side. + */ + public String patch_addPadding(LinkedList patches) { + short paddingLength = this.Patch_Margin; + String nullPadding = ""; + for (short x = 1; x <= paddingLength; x++) { + nullPadding += String.valueOf((char) x); + } + + // Bump all the patches forward. + for (Patch aPatch : patches) { + aPatch.start1 += paddingLength; + aPatch.start2 += paddingLength; + } + + // Add some padding on start of first diff. + Patch patch = patches.getFirst(); + LinkedList diffs = patch.diffs; + if (diffs.isEmpty() || diffs.getFirst().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.addFirst(new Diff(Operation.EQUAL, nullPadding)); + patch.start1 -= paddingLength; // Should be 0. + patch.start2 -= paddingLength; // Should be 0. + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.getFirst().text.length()) { + // Grow first equality. + Diff firstDiff = diffs.getFirst(); + int extraLength = paddingLength - firstDiff.text.length(); + firstDiff.text = nullPadding.substring(firstDiff.text.length()) + + firstDiff.text; + patch.start1 -= extraLength; + patch.start2 -= extraLength; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + // Add some padding on end of last diff. + patch = patches.getLast(); + diffs = patch.diffs; + if (diffs.isEmpty() || diffs.getLast().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.addLast(new Diff(Operation.EQUAL, nullPadding)); + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.getLast().text.length()) { + // Grow last equality. + Diff lastDiff = diffs.getLast(); + int extraLength = paddingLength - lastDiff.text.length(); + lastDiff.text += nullPadding.substring(0, extraLength); + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + return nullPadding; + } + + /** + * Look through the patches and break up any which are longer than the + * maximum limit of the match algorithm. + * Intended to be called only from within patch_apply. + * @param patches LinkedList of Patch objects. + */ + public void patch_splitMax(LinkedList patches) { + short patch_size = Match_MaxBits; + String precontext, postcontext; + Patch patch; + int start1, start2; + boolean empty; + Operation diff_type; + String diff_text; + ListIterator pointer = patches.listIterator(); + Patch bigpatch = pointer.hasNext() ? pointer.next() : null; + while (bigpatch != null) { + if (bigpatch.length1 <= Match_MaxBits) { + bigpatch = pointer.hasNext() ? pointer.next() : null; + continue; + } + // Remove the big old patch. + pointer.remove(); + start1 = bigpatch.start1; + start2 = bigpatch.start2; + precontext = ""; + while (!bigpatch.diffs.isEmpty()) { + // Create one of several smaller patches. + patch = new Patch(); + empty = true; + patch.start1 = start1 - precontext.length(); + patch.start2 = start2 - precontext.length(); + if (precontext.length() != 0) { + patch.length1 = patch.length2 = precontext.length(); + patch.diffs.add(new Diff(Operation.EQUAL, precontext)); + } + while (!bigpatch.diffs.isEmpty() + && patch.length1 < patch_size - Patch_Margin) { + diff_type = bigpatch.diffs.getFirst().operation; + diff_text = bigpatch.diffs.getFirst().text; + if (diff_type == Operation.INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.length(); + start2 += diff_text.length(); + patch.diffs.addLast(bigpatch.diffs.removeFirst()); + empty = false; + } else if (diff_type == Operation.DELETE && patch.diffs.size() == 1 + && patch.diffs.getFirst().operation == Operation.EQUAL + && diff_text.length() > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.length(); + start1 += diff_text.length(); + empty = false; + patch.diffs.add(new Diff(diff_type, diff_text)); + bigpatch.diffs.removeFirst(); + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text.substring(0, Math.min(diff_text.length(), + patch_size - patch.length1 - Patch_Margin)); + patch.length1 += diff_text.length(); + start1 += diff_text.length(); + if (diff_type == Operation.EQUAL) { + patch.length2 += diff_text.length(); + start2 += diff_text.length(); + } else { + empty = false; + } + patch.diffs.add(new Diff(diff_type, diff_text)); + if (diff_text.equals(bigpatch.diffs.getFirst().text)) { + bigpatch.diffs.removeFirst(); + } else { + bigpatch.diffs.getFirst().text = bigpatch.diffs.getFirst().text + .substring(diff_text.length()); + } + } + } + // Compute the head context for the next patch. + precontext = diff_text2(patch.diffs); + precontext = precontext.substring(Math.max(0, precontext.length() + - Patch_Margin)); + // Append the end context for this patch. + if (diff_text1(bigpatch.diffs).length() > Patch_Margin) { + postcontext = diff_text1(bigpatch.diffs).substring(0, Patch_Margin); + } else { + postcontext = diff_text1(bigpatch.diffs); + } + if (postcontext.length() != 0) { + patch.length1 += postcontext.length(); + patch.length2 += postcontext.length(); + if (!patch.diffs.isEmpty() + && patch.diffs.getLast().operation == Operation.EQUAL) { + patch.diffs.getLast().text += postcontext; + } else { + patch.diffs.add(new Diff(Operation.EQUAL, postcontext)); + } + } + if (!empty) { + pointer.add(patch); + } + } + bigpatch = pointer.hasNext() ? pointer.next() : null; + } + } + + /** + * Take a list of patches and return a textual representation. + * @param patches List of Patch objects. + * @return Text representation of patches. + */ + public String patch_toText(List patches) { + StringBuilder text = new StringBuilder(); + for (Patch aPatch : patches) { + text.append(aPatch); + } + return text.toString(); + } + + /** + * Parse a textual representation of patches and return a List of Patch + * objects. + * @param textline Text representation of patches. + * @return List of Patch objects. + * @throws IllegalArgumentException If invalid input. + */ + public List patch_fromText(String textline) + throws IllegalArgumentException { + List patches = new LinkedList(); + if (textline.length() == 0) { + return patches; + } + List textList = Arrays.asList(textline.split("\n")); + LinkedList text = new LinkedList(textList); + Patch patch; + Pattern patchHeader + = Pattern.compile("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$"); + Matcher m; + char sign; + String line; + while (!text.isEmpty()) { + m = patchHeader.matcher(text.getFirst()); + if (!m.matches()) { + throw new IllegalArgumentException( + "Invalid patch string: " + text.getFirst()); + } + patch = new Patch(); + patches.add(patch); + patch.start1 = Integer.parseInt(m.group(1)); + if (m.group(2).length() == 0) { + patch.start1--; + patch.length1 = 1; + } else if (m.group(2).equals("0")) { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = Integer.parseInt(m.group(2)); + } + + patch.start2 = Integer.parseInt(m.group(3)); + if (m.group(4).length() == 0) { + patch.start2--; + patch.length2 = 1; + } else if (m.group(4).equals("0")) { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = Integer.parseInt(m.group(4)); + } + text.removeFirst(); + + while (!text.isEmpty()) { + try { + sign = text.getFirst().charAt(0); + } catch (IndexOutOfBoundsException e) { + // Blank line? Whatever. + text.removeFirst(); + continue; + } + line = text.getFirst().substring(1); + line = line.replace("+", "%2B"); // decode would change all "+" to " " + try { + line = URLDecoder.decode(line, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } catch (IllegalArgumentException e) { + // Malformed URI sequence. + throw new IllegalArgumentException( + "Illegal escape in patch_fromText: " + line, e); + } + if (sign == '-') { + // Deletion. + patch.diffs.add(new Diff(Operation.DELETE, line)); + } else if (sign == '+') { + // Insertion. + patch.diffs.add(new Diff(Operation.INSERT, line)); + } else if (sign == ' ') { + // Minor equality. + patch.diffs.add(new Diff(Operation.EQUAL, line)); + } else if (sign == '@') { + // Start of next patch. + break; + } else { + // WTF? + throw new IllegalArgumentException( + "Invalid patch mode '" + sign + "' in: " + line); + } + text.removeFirst(); + } + } + return patches; + } + + + /** + * Class representing one diff operation. + */ + public static class Diff { + /** + * One of: INSERT, DELETE or EQUAL. + */ + public Operation operation; + /** + * The text associated with this diff operation. + */ + public String text; + + /** + * Constructor. Initializes the diff with the provided values. + * @param operation One of INSERT, DELETE or EQUAL. + * @param text The text being applied. + */ + public Diff(Operation operation, String text) { + // Construct a diff with the specified operation and text. + this.operation = operation; + this.text = text; + } + + /** + * Display a human-readable version of this Diff. + * @return text version. + */ + public String toString() { + String prettyText = this.text.replace('\n', '\u00b6'); + return "Diff(" + this.operation + ",\"" + prettyText + "\")"; + } + + /** + * Create a numeric hash value for a Diff. + * This function is not used by DMP. + * @return Hash value. + */ + @Override + public int hashCode() { + final int prime = 31; + int result = (operation == null) ? 0 : operation.hashCode(); + result += prime * ((text == null) ? 0 : text.hashCode()); + return result; + } + + /** + * Is this Diff equivalent to another Diff? + * @param obj Another Diff to compare against. + * @return true or false. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Diff other = (Diff) obj; + if (operation != other.operation) { + return false; + } + if (text == null) { + if (other.text != null) { + return false; + } + } else if (!text.equals(other.text)) { + return false; + } + return true; + } + } + + + /** + * Class representing one patch operation. + */ + public static class Patch { + public LinkedList diffs; + public int start1; + public int start2; + public int length1; + public int length2; + + /** + * Constructor. Initializes with an empty list of diffs. + */ + public Patch() { + this.diffs = new LinkedList(); + } + + /** + * Emmulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indicies are printed as 1-based, not 0-based. + * @return The GNU diff string. + */ + public String toString() { + String coords1, coords2; + if (this.length1 == 0) { + coords1 = this.start1 + ",0"; + } else if (this.length1 == 1) { + coords1 = Integer.toString(this.start1 + 1); + } else { + coords1 = (this.start1 + 1) + "," + this.length1; + } + if (this.length2 == 0) { + coords2 = this.start2 + ",0"; + } else if (this.length2 == 1) { + coords2 = Integer.toString(this.start2 + 1); + } else { + coords2 = (this.start2 + 1) + "," + this.length2; + } + StringBuilder text = new StringBuilder(); + text.append("@@ -").append(coords1).append(" +").append(coords2) + .append(" @@\n"); + // Escape the body of the patch with %xx notation. + for (Diff aDiff : this.diffs) { + switch (aDiff.operation) { + case INSERT: + text.append('+'); + break; + case DELETE: + text.append('-'); + break; + case EQUAL: + text.append(' '); + break; + } + try { + text.append(URLEncoder.encode(aDiff.text, "UTF-8").replace('+', ' ')) + .append("\n"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } + } + return unescapeForEncodeUriCompatability(text.toString()); + } + } + + /** + * Unescape selected chars for compatability with JavaScript's encodeURI. + * In speed critical applications this could be dropped since the + * receiving application will certainly decode these fine. + * Note that this function is case-sensitive. Thus "%3f" would not be + * unescaped. But this is ok because it is only called with the output of + * URLEncoder.encode which returns uppercase hex. + * + * Example: "%3F" -> "?", "%24" -> "$", etc. + * + * @param str The string to escape. + * @return The escaped string. + */ + private static String unescapeForEncodeUriCompatability(String str) { + return str.replace("%21", "!").replace("%7E", "~") + .replace("%27", "'").replace("%28", "(").replace("%29", ")") + .replace("%3B", ";").replace("%2F", "/").replace("%3F", "?") + .replace("%3A", ":").replace("%40", "@").replace("%26", "&") + .replace("%3D", "=").replace("%2B", "+").replace("%24", "$") + .replace("%2C", ",").replace("%23", "#"); + } +} diff --git a/java/name/fraser/neil/plaintext/diff_match_patch_test.java b/java/name/fraser/neil/plaintext/diff_match_patch_test.java new file mode 100644 index 0000000..85f1d0a --- /dev/null +++ b/java/name/fraser/neil/plaintext/diff_match_patch_test.java @@ -0,0 +1,910 @@ +/* + * Diff Match and Patch -- Test harness + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package name.fraser.neil.plaintext; + +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import name.fraser.neil.plaintext.diff_match_patch.Diff; +import name.fraser.neil.plaintext.diff_match_patch.LinesToCharsResult; +import name.fraser.neil.plaintext.diff_match_patch.Patch; + +public class diff_match_patch_test extends TestCase { + + private diff_match_patch dmp; + private diff_match_patch.Operation DELETE = diff_match_patch.Operation.DELETE; + private diff_match_patch.Operation EQUAL = diff_match_patch.Operation.EQUAL; + private diff_match_patch.Operation INSERT = diff_match_patch.Operation.INSERT; + + protected void setUp() { + // Create an instance of the diff_match_patch object. + dmp = new diff_match_patch(); + } + + + // DIFF TEST FUNCTIONS + + + public void testDiffCommonPrefix() { + // Detect any common prefix. + assertEquals("diff_commonPrefix: Null case.", 0, dmp.diff_commonPrefix("abc", "xyz")); + + assertEquals("diff_commonPrefix: Non-null case.", 4, dmp.diff_commonPrefix("1234abcdef", "1234xyz")); + + assertEquals("diff_commonPrefix: Whole case.", 4, dmp.diff_commonPrefix("1234", "1234xyz")); + } + + public void testDiffCommonSuffix() { + // Detect any common suffix. + assertEquals("diff_commonSuffix: Null case.", 0, dmp.diff_commonSuffix("abc", "xyz")); + + assertEquals("diff_commonSuffix: Non-null case.", 4, dmp.diff_commonSuffix("abcdef1234", "xyz1234")); + + assertEquals("diff_commonSuffix: Whole case.", 4, dmp.diff_commonSuffix("1234", "xyz1234")); + } + + public void testDiffCommonOverlap() { + // Detect any suffix/prefix overlap. + assertEquals("diff_commonOverlap: Null case.", 0, dmp.diff_commonOverlap("", "abcd")); + + assertEquals("diff_commonOverlap: Whole case.", 3, dmp.diff_commonOverlap("abc", "abcd")); + + assertEquals("diff_commonOverlap: No overlap.", 0, dmp.diff_commonOverlap("123456", "abcd")); + + assertEquals("diff_commonOverlap: Overlap.", 3, dmp.diff_commonOverlap("123456xxx", "xxxabcd")); + + // Some overly clever languages (C#) may treat ligatures as equal to their + // component letters. E.g. U+FB01 == 'fi' + assertEquals("diff_commonOverlap: Unicode.", 0, dmp.diff_commonOverlap("fi", "\ufb01i")); + } + + public void testDiffHalfmatch() { + // Detect a halfmatch. + dmp.Diff_Timeout = 1; + assertNull("diff_halfMatch: No match #1.", dmp.diff_halfMatch("1234567890", "abcdef")); + + assertNull("diff_halfMatch: No match #2.", dmp.diff_halfMatch("12345", "23")); + + assertArrayEquals("diff_halfMatch: Single Match #1.", new String[]{"12", "90", "a", "z", "345678"}, dmp.diff_halfMatch("1234567890", "a345678z")); + + assertArrayEquals("diff_halfMatch: Single Match #2.", new String[]{"a", "z", "12", "90", "345678"}, dmp.diff_halfMatch("a345678z", "1234567890")); + + assertArrayEquals("diff_halfMatch: Single Match #3.", new String[]{"abc", "z", "1234", "0", "56789"}, dmp.diff_halfMatch("abc56789z", "1234567890")); + + assertArrayEquals("diff_halfMatch: Single Match #4.", new String[]{"a", "xyz", "1", "7890", "23456"}, dmp.diff_halfMatch("a23456xyz", "1234567890")); + + assertArrayEquals("diff_halfMatch: Multiple Matches #1.", new String[]{"12123", "123121", "a", "z", "1234123451234"}, dmp.diff_halfMatch("121231234123451234123121", "a1234123451234z")); + + assertArrayEquals("diff_halfMatch: Multiple Matches #2.", new String[]{"", "-=-=-=-=-=", "x", "", "x-=-=-=-=-=-=-="}, dmp.diff_halfMatch("x-=-=-=-=-=-=-=-=-=-=-=-=", "xx-=-=-=-=-=-=-=")); + + assertArrayEquals("diff_halfMatch: Multiple Matches #3.", new String[]{"-=-=-=-=-=", "", "", "y", "-=-=-=-=-=-=-=y"}, dmp.diff_halfMatch("-=-=-=-=-=-=-=-=-=-=-=-=y", "-=-=-=-=-=-=-=yy")); + + // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy + assertArrayEquals("diff_halfMatch: Non-optimal halfmatch.", new String[]{"qHillo", "w", "x", "Hulloy", "HelloHe"}, dmp.diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy")); + + dmp.Diff_Timeout = 0; + assertNull("diff_halfMatch: Optimal no halfmatch.", dmp.diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy")); + } + + public void testDiffLinesToChars() { + // Convert lines down to characters. + ArrayList tmpVector = new ArrayList(); + tmpVector.add(""); + tmpVector.add("alpha\n"); + tmpVector.add("beta\n"); + assertLinesToCharsResultEquals("diff_linesToChars: Shared lines.", new LinesToCharsResult("\u0001\u0002\u0001", "\u0002\u0001\u0002", tmpVector), dmp.diff_linesToChars("alpha\nbeta\nalpha\n", "beta\nalpha\nbeta\n")); + + tmpVector.clear(); + tmpVector.add(""); + tmpVector.add("alpha\r\n"); + tmpVector.add("beta\r\n"); + tmpVector.add("\r\n"); + assertLinesToCharsResultEquals("diff_linesToChars: Empty string and blank lines.", new LinesToCharsResult("", "\u0001\u0002\u0003\u0003", tmpVector), dmp.diff_linesToChars("", "alpha\r\nbeta\r\n\r\n\r\n")); + + tmpVector.clear(); + tmpVector.add(""); + tmpVector.add("a"); + tmpVector.add("b"); + assertLinesToCharsResultEquals("diff_linesToChars: No linebreaks.", new LinesToCharsResult("\u0001", "\u0002", tmpVector), dmp.diff_linesToChars("a", "b")); + + // More than 256 to reveal any 8-bit limitations. + int n = 300; + tmpVector.clear(); + StringBuilder lineList = new StringBuilder(); + StringBuilder charList = new StringBuilder(); + for (int x = 1; x < n + 1; x++) { + tmpVector.add(x + "\n"); + lineList.append(x + "\n"); + charList.append(String.valueOf((char) x)); + } + assertEquals(n, tmpVector.size()); + String lines = lineList.toString(); + String chars = charList.toString(); + assertEquals(n, chars.length()); + tmpVector.add(0, ""); + assertLinesToCharsResultEquals("diff_linesToChars: More than 256.", new LinesToCharsResult(chars, "", tmpVector), dmp.diff_linesToChars(lines, "")); + } + + public void testDiffCharsToLines() { + // First check that Diff equality works. + assertTrue("diff_charsToLines: Equality #1.", new Diff(EQUAL, "a").equals(new Diff(EQUAL, "a"))); + + assertEquals("diff_charsToLines: Equality #2.", new Diff(EQUAL, "a"), new Diff(EQUAL, "a")); + + // Convert chars up to lines. + LinkedList diffs = diffList(new Diff(EQUAL, "\u0001\u0002\u0001"), new Diff(INSERT, "\u0002\u0001\u0002")); + ArrayList tmpVector = new ArrayList(); + tmpVector.add(""); + tmpVector.add("alpha\n"); + tmpVector.add("beta\n"); + dmp.diff_charsToLines(diffs, tmpVector); + assertEquals("diff_charsToLines: Shared lines.", diffList(new Diff(EQUAL, "alpha\nbeta\nalpha\n"), new Diff(INSERT, "beta\nalpha\nbeta\n")), diffs); + + // More than 256 to reveal any 8-bit limitations. + int n = 300; + tmpVector.clear(); + StringBuilder lineList = new StringBuilder(); + StringBuilder charList = new StringBuilder(); + for (int x = 1; x < n + 1; x++) { + tmpVector.add(x + "\n"); + lineList.append(x + "\n"); + charList.append(String.valueOf((char) x)); + } + assertEquals(n, tmpVector.size()); + String lines = lineList.toString(); + String chars = charList.toString(); + assertEquals(n, chars.length()); + tmpVector.add(0, ""); + diffs = diffList(new Diff(DELETE, chars)); + dmp.diff_charsToLines(diffs, tmpVector); + assertEquals("diff_charsToLines: More than 256.", diffList(new Diff(DELETE, lines)), diffs); + } + + public void testDiffCleanupMerge() { + // Cleanup a messy diff. + LinkedList diffs = diffList(); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Null case.", diffList(), diffs); + + diffs = diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "b"), new Diff(INSERT, "c")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: No change case.", diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "b"), new Diff(INSERT, "c")), diffs); + + diffs = diffList(new Diff(EQUAL, "a"), new Diff(EQUAL, "b"), new Diff(EQUAL, "c")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Merge equalities.", diffList(new Diff(EQUAL, "abc")), diffs); + + diffs = diffList(new Diff(DELETE, "a"), new Diff(DELETE, "b"), new Diff(DELETE, "c")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Merge deletions.", diffList(new Diff(DELETE, "abc")), diffs); + + diffs = diffList(new Diff(INSERT, "a"), new Diff(INSERT, "b"), new Diff(INSERT, "c")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Merge insertions.", diffList(new Diff(INSERT, "abc")), diffs); + + diffs = diffList(new Diff(DELETE, "a"), new Diff(INSERT, "b"), new Diff(DELETE, "c"), new Diff(INSERT, "d"), new Diff(EQUAL, "e"), new Diff(EQUAL, "f")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Merge interweave.", diffList(new Diff(DELETE, "ac"), new Diff(INSERT, "bd"), new Diff(EQUAL, "ef")), diffs); + + diffs = diffList(new Diff(DELETE, "a"), new Diff(INSERT, "abc"), new Diff(DELETE, "dc")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Prefix and suffix detection.", diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "d"), new Diff(INSERT, "b"), new Diff(EQUAL, "c")), diffs); + + diffs = diffList(new Diff(EQUAL, "x"), new Diff(DELETE, "a"), new Diff(INSERT, "abc"), new Diff(DELETE, "dc"), new Diff(EQUAL, "y")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Prefix and suffix detection with equalities.", diffList(new Diff(EQUAL, "xa"), new Diff(DELETE, "d"), new Diff(INSERT, "b"), new Diff(EQUAL, "cy")), diffs); + + diffs = diffList(new Diff(EQUAL, "a"), new Diff(INSERT, "ba"), new Diff(EQUAL, "c")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Slide edit left.", diffList(new Diff(INSERT, "ab"), new Diff(EQUAL, "ac")), diffs); + + diffs = diffList(new Diff(EQUAL, "c"), new Diff(INSERT, "ab"), new Diff(EQUAL, "a")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Slide edit right.", diffList(new Diff(EQUAL, "ca"), new Diff(INSERT, "ba")), diffs); + + diffs = diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "b"), new Diff(EQUAL, "c"), new Diff(DELETE, "ac"), new Diff(EQUAL, "x")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Slide edit left recursive.", diffList(new Diff(DELETE, "abc"), new Diff(EQUAL, "acx")), diffs); + + diffs = diffList(new Diff(EQUAL, "x"), new Diff(DELETE, "ca"), new Diff(EQUAL, "c"), new Diff(DELETE, "b"), new Diff(EQUAL, "a")); + dmp.diff_cleanupMerge(diffs); + assertEquals("diff_cleanupMerge: Slide edit right recursive.", diffList(new Diff(EQUAL, "xca"), new Diff(DELETE, "cba")), diffs); + } + + public void testDiffCleanupSemanticLossless() { + // Slide diffs to match logical boundaries. + LinkedList diffs = diffList(); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemanticLossless: Null case.", diffList(), diffs); + + diffs = diffList(new Diff(EQUAL, "AAA\r\n\r\nBBB"), new Diff(INSERT, "\r\nDDD\r\n\r\nBBB"), new Diff(EQUAL, "\r\nEEE")); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemanticLossless: Blank lines.", diffList(new Diff(EQUAL, "AAA\r\n\r\n"), new Diff(INSERT, "BBB\r\nDDD\r\n\r\n"), new Diff(EQUAL, "BBB\r\nEEE")), diffs); + + diffs = diffList(new Diff(EQUAL, "AAA\r\nBBB"), new Diff(INSERT, " DDD\r\nBBB"), new Diff(EQUAL, " EEE")); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemanticLossless: Line boundaries.", diffList(new Diff(EQUAL, "AAA\r\n"), new Diff(INSERT, "BBB DDD\r\n"), new Diff(EQUAL, "BBB EEE")), diffs); + + diffs = diffList(new Diff(EQUAL, "The c"), new Diff(INSERT, "ow and the c"), new Diff(EQUAL, "at.")); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemanticLossless: Word boundaries.", diffList(new Diff(EQUAL, "The "), new Diff(INSERT, "cow and the "), new Diff(EQUAL, "cat.")), diffs); + + diffs = diffList(new Diff(EQUAL, "The-c"), new Diff(INSERT, "ow-and-the-c"), new Diff(EQUAL, "at.")); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemanticLossless: Alphanumeric boundaries.", diffList(new Diff(EQUAL, "The-"), new Diff(INSERT, "cow-and-the-"), new Diff(EQUAL, "cat.")), diffs); + + diffs = diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "a"), new Diff(EQUAL, "ax")); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemanticLossless: Hitting the start.", diffList(new Diff(DELETE, "a"), new Diff(EQUAL, "aax")), diffs); + + diffs = diffList(new Diff(EQUAL, "xa"), new Diff(DELETE, "a"), new Diff(EQUAL, "a")); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemanticLossless: Hitting the end.", diffList(new Diff(EQUAL, "xaa"), new Diff(DELETE, "a")), diffs); + + diffs = diffList(new Diff(EQUAL, "The xxx. The "), new Diff(INSERT, "zzz. The "), new Diff(EQUAL, "yyy.")); + dmp.diff_cleanupSemanticLossless(diffs); + assertEquals("diff_cleanupSemanticLossless: Sentence boundaries.", diffList(new Diff(EQUAL, "The xxx."), new Diff(INSERT, " The zzz."), new Diff(EQUAL, " The yyy.")), diffs); + } + + public void testDiffCleanupSemantic() { + // Cleanup semantically trivial equalities. + LinkedList diffs = diffList(); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Null case.", diffList(), diffs); + + diffs = diffList(new Diff(DELETE, "ab"), new Diff(INSERT, "cd"), new Diff(EQUAL, "12"), new Diff(DELETE, "e")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: No elimination #1.", diffList(new Diff(DELETE, "ab"), new Diff(INSERT, "cd"), new Diff(EQUAL, "12"), new Diff(DELETE, "e")), diffs); + + diffs = diffList(new Diff(DELETE, "abc"), new Diff(INSERT, "ABC"), new Diff(EQUAL, "1234"), new Diff(DELETE, "wxyz")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: No elimination #2.", diffList(new Diff(DELETE, "abc"), new Diff(INSERT, "ABC"), new Diff(EQUAL, "1234"), new Diff(DELETE, "wxyz")), diffs); + + diffs = diffList(new Diff(DELETE, "a"), new Diff(EQUAL, "b"), new Diff(DELETE, "c")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Simple elimination.", diffList(new Diff(DELETE, "abc"), new Diff(INSERT, "b")), diffs); + + diffs = diffList(new Diff(DELETE, "ab"), new Diff(EQUAL, "cd"), new Diff(DELETE, "e"), new Diff(EQUAL, "f"), new Diff(INSERT, "g")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Backpass elimination.", diffList(new Diff(DELETE, "abcdef"), new Diff(INSERT, "cdfg")), diffs); + + diffs = diffList(new Diff(INSERT, "1"), new Diff(EQUAL, "A"), new Diff(DELETE, "B"), new Diff(INSERT, "2"), new Diff(EQUAL, "_"), new Diff(INSERT, "1"), new Diff(EQUAL, "A"), new Diff(DELETE, "B"), new Diff(INSERT, "2")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Multiple elimination.", diffList(new Diff(DELETE, "AB_AB"), new Diff(INSERT, "1A2_1A2")), diffs); + + diffs = diffList(new Diff(EQUAL, "The c"), new Diff(DELETE, "ow and the c"), new Diff(EQUAL, "at.")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Word boundaries.", diffList(new Diff(EQUAL, "The "), new Diff(DELETE, "cow and the "), new Diff(EQUAL, "cat.")), diffs); + + diffs = diffList(new Diff(DELETE, "abcxx"), new Diff(INSERT, "xxdef")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: No overlap elimination.", diffList(new Diff(DELETE, "abcxx"), new Diff(INSERT, "xxdef")), diffs); + + diffs = diffList(new Diff(DELETE, "abcxxx"), new Diff(INSERT, "xxxdef")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Overlap elimination.", diffList(new Diff(DELETE, "abc"), new Diff(EQUAL, "xxx"), new Diff(INSERT, "def")), diffs); + + diffs = diffList(new Diff(DELETE, "xxxabc"), new Diff(INSERT, "defxxx")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Reverse overlap elimination.", diffList(new Diff(INSERT, "def"), new Diff(EQUAL, "xxx"), new Diff(DELETE, "abc")), diffs); + + diffs = diffList(new Diff(DELETE, "abcd1212"), new Diff(INSERT, "1212efghi"), new Diff(EQUAL, "----"), new Diff(DELETE, "A3"), new Diff(INSERT, "3BC")); + dmp.diff_cleanupSemantic(diffs); + assertEquals("diff_cleanupSemantic: Two overlap eliminations.", diffList(new Diff(DELETE, "abcd"), new Diff(EQUAL, "1212"), new Diff(INSERT, "efghi"), new Diff(EQUAL, "----"), new Diff(DELETE, "A"), new Diff(EQUAL, "3"), new Diff(INSERT, "BC")), diffs); + } + + public void testDiffCleanupEfficiency() { + // Cleanup operationally trivial equalities. + dmp.Diff_EditCost = 4; + LinkedList diffs = diffList(); + dmp.diff_cleanupEfficiency(diffs); + assertEquals("diff_cleanupEfficiency: Null case.", diffList(), diffs); + + diffs = diffList(new Diff(DELETE, "ab"), new Diff(INSERT, "12"), new Diff(EQUAL, "wxyz"), new Diff(DELETE, "cd"), new Diff(INSERT, "34")); + dmp.diff_cleanupEfficiency(diffs); + assertEquals("diff_cleanupEfficiency: No elimination.", diffList(new Diff(DELETE, "ab"), new Diff(INSERT, "12"), new Diff(EQUAL, "wxyz"), new Diff(DELETE, "cd"), new Diff(INSERT, "34")), diffs); + + diffs = diffList(new Diff(DELETE, "ab"), new Diff(INSERT, "12"), new Diff(EQUAL, "xyz"), new Diff(DELETE, "cd"), new Diff(INSERT, "34")); + dmp.diff_cleanupEfficiency(diffs); + assertEquals("diff_cleanupEfficiency: Four-edit elimination.", diffList(new Diff(DELETE, "abxyzcd"), new Diff(INSERT, "12xyz34")), diffs); + + diffs = diffList(new Diff(INSERT, "12"), new Diff(EQUAL, "x"), new Diff(DELETE, "cd"), new Diff(INSERT, "34")); + dmp.diff_cleanupEfficiency(diffs); + assertEquals("diff_cleanupEfficiency: Three-edit elimination.", diffList(new Diff(DELETE, "xcd"), new Diff(INSERT, "12x34")), diffs); + + diffs = diffList(new Diff(DELETE, "ab"), new Diff(INSERT, "12"), new Diff(EQUAL, "xy"), new Diff(INSERT, "34"), new Diff(EQUAL, "z"), new Diff(DELETE, "cd"), new Diff(INSERT, "56")); + dmp.diff_cleanupEfficiency(diffs); + assertEquals("diff_cleanupEfficiency: Backpass elimination.", diffList(new Diff(DELETE, "abxyzcd"), new Diff(INSERT, "12xy34z56")), diffs); + + dmp.Diff_EditCost = 5; + diffs = diffList(new Diff(DELETE, "ab"), new Diff(INSERT, "12"), new Diff(EQUAL, "wxyz"), new Diff(DELETE, "cd"), new Diff(INSERT, "34")); + dmp.diff_cleanupEfficiency(diffs); + assertEquals("diff_cleanupEfficiency: High cost elimination.", diffList(new Diff(DELETE, "abwxyzcd"), new Diff(INSERT, "12wxyz34")), diffs); + dmp.Diff_EditCost = 4; + } + + public void testDiffPrettyHtml() { + // Pretty print. + LinkedList diffs = diffList(new Diff(EQUAL, "a\n"), new Diff(DELETE, "b"), new Diff(INSERT, "c&d")); + assertEquals("diff_prettyHtml:", "
    <B>b</B>c&d", dmp.diff_prettyHtml(diffs)); + } + + public void testDiffText() { + // Compute the source and destination texts. + LinkedList diffs = diffList(new Diff(EQUAL, "jump"), new Diff(DELETE, "s"), new Diff(INSERT, "ed"), new Diff(EQUAL, " over "), new Diff(DELETE, "the"), new Diff(INSERT, "a"), new Diff(EQUAL, " lazy")); + assertEquals("diff_text1:", "jumps over the lazy", dmp.diff_text1(diffs)); + assertEquals("diff_text2:", "jumped over a lazy", dmp.diff_text2(diffs)); + } + + public void testDiffDelta() { + // Convert a diff into delta string. + LinkedList diffs = diffList(new Diff(EQUAL, "jump"), new Diff(DELETE, "s"), new Diff(INSERT, "ed"), new Diff(EQUAL, " over "), new Diff(DELETE, "the"), new Diff(INSERT, "a"), new Diff(EQUAL, " lazy"), new Diff(INSERT, "old dog")); + String text1 = dmp.diff_text1(diffs); + assertEquals("diff_text1: Base text.", "jumps over the lazy", text1); + + String delta = dmp.diff_toDelta(diffs); + assertEquals("diff_toDelta:", "=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog", delta); + + // Convert delta string into a diff. + assertEquals("diff_fromDelta: Normal.", diffs, dmp.diff_fromDelta(text1, delta)); + + // Generates error (19 < 20). + try { + dmp.diff_fromDelta(text1 + "x", delta); + fail("diff_fromDelta: Too long."); + } catch (IllegalArgumentException ex) { + // Exception expected. + } + + // Generates error (19 > 18). + try { + dmp.diff_fromDelta(text1.substring(1), delta); + fail("diff_fromDelta: Too short."); + } catch (IllegalArgumentException ex) { + // Exception expected. + } + + // Generates error (%c3%xy invalid Unicode). + try { + dmp.diff_fromDelta("", "+%c3%xy"); + fail("diff_fromDelta: Invalid character."); + } catch (IllegalArgumentException ex) { + // Exception expected. + } + + // Test deltas with special characters. + diffs = diffList(new Diff(EQUAL, "\u0680 \000 \t %"), new Diff(DELETE, "\u0681 \001 \n ^"), new Diff(INSERT, "\u0682 \002 \\ |")); + text1 = dmp.diff_text1(diffs); + assertEquals("diff_text1: Unicode text.", "\u0680 \000 \t %\u0681 \001 \n ^", text1); + + delta = dmp.diff_toDelta(diffs); + assertEquals("diff_toDelta: Unicode.", "=7\t-7\t+%DA%82 %02 %5C %7C", delta); + + assertEquals("diff_fromDelta: Unicode.", diffs, dmp.diff_fromDelta(text1, delta)); + + // Verify pool of unchanged characters. + diffs = diffList(new Diff(INSERT, "A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ")); + String text2 = dmp.diff_text2(diffs); + assertEquals("diff_text2: Unchanged characters.", "A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", text2); + + delta = dmp.diff_toDelta(diffs); + assertEquals("diff_toDelta: Unchanged characters.", "+A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", delta); + + // Convert delta string into a diff. + assertEquals("diff_fromDelta: Unchanged characters.", diffs, dmp.diff_fromDelta("", delta)); + } + + public void testDiffXIndex() { + // Translate a location in text1 to text2. + LinkedList diffs = diffList(new Diff(DELETE, "a"), new Diff(INSERT, "1234"), new Diff(EQUAL, "xyz")); + assertEquals("diff_xIndex: Translation on equality.", 5, dmp.diff_xIndex(diffs, 2)); + + diffs = diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "1234"), new Diff(EQUAL, "xyz")); + assertEquals("diff_xIndex: Translation on deletion.", 1, dmp.diff_xIndex(diffs, 3)); + } + + public void testDiffLevenshtein() { + LinkedList diffs = diffList(new Diff(DELETE, "abc"), new Diff(INSERT, "1234"), new Diff(EQUAL, "xyz")); + assertEquals("Levenshtein with trailing equality.", 4, dmp.diff_levenshtein(diffs)); + + diffs = diffList(new Diff(EQUAL, "xyz"), new Diff(DELETE, "abc"), new Diff(INSERT, "1234")); + assertEquals("Levenshtein with leading equality.", 4, dmp.diff_levenshtein(diffs)); + + diffs = diffList(new Diff(DELETE, "abc"), new Diff(EQUAL, "xyz"), new Diff(INSERT, "1234")); + assertEquals("Levenshtein with middle equality.", 7, dmp.diff_levenshtein(diffs)); + } + + public void testDiffBisect() { + // Normal. + String a = "cat"; + String b = "map"; + // Since the resulting diff hasn't been normalized, it would be ok if + // the insertion and deletion pairs are swapped. + // If the order changes, tweak this test as required. + LinkedList diffs = diffList(new Diff(DELETE, "c"), new Diff(INSERT, "m"), new Diff(EQUAL, "a"), new Diff(DELETE, "t"), new Diff(INSERT, "p")); + assertEquals("diff_bisect: Normal.", diffs, dmp.diff_bisect(a, b, Long.MAX_VALUE)); + + // Timeout. + diffs = diffList(new Diff(DELETE, "cat"), new Diff(INSERT, "map")); + assertEquals("diff_bisect: Timeout.", diffs, dmp.diff_bisect(a, b, 0)); + } + + public void testDiffMain() { + // Perform a trivial diff. + LinkedList diffs = diffList(); + assertEquals("diff_main: Null case.", diffs, dmp.diff_main("", "", false)); + + diffs = diffList(new Diff(EQUAL, "abc")); + assertEquals("diff_main: Equality.", diffs, dmp.diff_main("abc", "abc", false)); + + diffs = diffList(new Diff(EQUAL, "ab"), new Diff(INSERT, "123"), new Diff(EQUAL, "c")); + assertEquals("diff_main: Simple insertion.", diffs, dmp.diff_main("abc", "ab123c", false)); + + diffs = diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "123"), new Diff(EQUAL, "bc")); + assertEquals("diff_main: Simple deletion.", diffs, dmp.diff_main("a123bc", "abc", false)); + + diffs = diffList(new Diff(EQUAL, "a"), new Diff(INSERT, "123"), new Diff(EQUAL, "b"), new Diff(INSERT, "456"), new Diff(EQUAL, "c")); + assertEquals("diff_main: Two insertions.", diffs, dmp.diff_main("abc", "a123b456c", false)); + + diffs = diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "123"), new Diff(EQUAL, "b"), new Diff(DELETE, "456"), new Diff(EQUAL, "c")); + assertEquals("diff_main: Two deletions.", diffs, dmp.diff_main("a123b456c", "abc", false)); + + // Perform a real diff. + // Switch off the timeout. + dmp.Diff_Timeout = 0; + diffs = diffList(new Diff(DELETE, "a"), new Diff(INSERT, "b")); + assertEquals("diff_main: Simple case #1.", diffs, dmp.diff_main("a", "b", false)); + + diffs = diffList(new Diff(DELETE, "Apple"), new Diff(INSERT, "Banana"), new Diff(EQUAL, "s are a"), new Diff(INSERT, "lso"), new Diff(EQUAL, " fruit.")); + assertEquals("diff_main: Simple case #2.", diffs, dmp.diff_main("Apples are a fruit.", "Bananas are also fruit.", false)); + + diffs = diffList(new Diff(DELETE, "a"), new Diff(INSERT, "\u0680"), new Diff(EQUAL, "x"), new Diff(DELETE, "\t"), new Diff(INSERT, "\000")); + assertEquals("diff_main: Simple case #3.", diffs, dmp.diff_main("ax\t", "\u0680x\000", false)); + + diffs = diffList(new Diff(DELETE, "1"), new Diff(EQUAL, "a"), new Diff(DELETE, "y"), new Diff(EQUAL, "b"), new Diff(DELETE, "2"), new Diff(INSERT, "xab")); + assertEquals("diff_main: Overlap #1.", diffs, dmp.diff_main("1ayb2", "abxab", false)); + + diffs = diffList(new Diff(INSERT, "xaxcx"), new Diff(EQUAL, "abc"), new Diff(DELETE, "y")); + assertEquals("diff_main: Overlap #2.", diffs, dmp.diff_main("abcy", "xaxcxabc", false)); + + diffs = diffList(new Diff(DELETE, "ABCD"), new Diff(EQUAL, "a"), new Diff(DELETE, "="), new Diff(INSERT, "-"), new Diff(EQUAL, "bcd"), new Diff(DELETE, "="), new Diff(INSERT, "-"), new Diff(EQUAL, "efghijklmnopqrs"), new Diff(DELETE, "EFGHIJKLMNOefg")); + assertEquals("diff_main: Overlap #3.", diffs, dmp.diff_main("ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg", "a-bcd-efghijklmnopqrs", false)); + + diffs = diffList(new Diff(INSERT, " "), new Diff(EQUAL, "a"), new Diff(INSERT, "nd"), new Diff(EQUAL, " [[Pennsylvania]]"), new Diff(DELETE, " and [[New")); + assertEquals("diff_main: Large equality.", diffs, dmp.diff_main("a [[Pennsylvania]] and [[New", " and [[Pennsylvania]]", false)); + + dmp.Diff_Timeout = 0.1f; // 100ms + String a = "`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n"; + String b = "I am the very model of a modern major general,\nI've information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n"; + // Increase the text lengths by 1024 times to ensure a timeout. + for (int x = 0; x < 10; x++) { + a = a + a; + b = b + b; + } + long startTime = System.currentTimeMillis(); + dmp.diff_main(a, b); + long endTime = System.currentTimeMillis(); + // Test that we took at least the timeout period. + assertTrue("diff_main: Timeout min.", dmp.Diff_Timeout * 1000 <= endTime - startTime); + // Test that we didn't take forever (be forgiving). + // Theoretically this test could fail very occasionally if the + // OS task swaps or locks up for a second at the wrong moment. + assertTrue("diff_main: Timeout max.", dmp.Diff_Timeout * 1000 * 2 > endTime - startTime); + dmp.Diff_Timeout = 0; + + // Test the linemode speedup. + // Must be long to pass the 100 char cutoff. + a = "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n"; + b = "abcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\n"; + assertEquals("diff_main: Simple line-mode.", dmp.diff_main(a, b, true), dmp.diff_main(a, b, false)); + + a = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; + b = "abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij"; + assertEquals("diff_main: Single line-mode.", dmp.diff_main(a, b, true), dmp.diff_main(a, b, false)); + + a = "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n"; + b = "abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n"; + String[] texts_linemode = diff_rebuildtexts(dmp.diff_main(a, b, true)); + String[] texts_textmode = diff_rebuildtexts(dmp.diff_main(a, b, false)); + assertArrayEquals("diff_main: Overlap line-mode.", texts_textmode, texts_linemode); + + // Test null inputs. + try { + dmp.diff_main(null, null); + fail("diff_main: Null inputs."); + } catch (IllegalArgumentException ex) { + // Error expected. + } + } + + + // MATCH TEST FUNCTIONS + + + public void testMatchAlphabet() { + // Initialise the bitmasks for Bitap. + Map bitmask; + bitmask = new HashMap(); + bitmask.put('a', 4); bitmask.put('b', 2); bitmask.put('c', 1); + assertEquals("match_alphabet: Unique.", bitmask, dmp.match_alphabet("abc")); + + bitmask = new HashMap(); + bitmask.put('a', 37); bitmask.put('b', 18); bitmask.put('c', 8); + assertEquals("match_alphabet: Duplicates.", bitmask, dmp.match_alphabet("abcaba")); + } + + public void testMatchBitap() { + // Bitap algorithm. + dmp.Match_Distance = 100; + dmp.Match_Threshold = 0.5f; + assertEquals("match_bitap: Exact match #1.", 5, dmp.match_bitap("abcdefghijk", "fgh", 5)); + + assertEquals("match_bitap: Exact match #2.", 5, dmp.match_bitap("abcdefghijk", "fgh", 0)); + + assertEquals("match_bitap: Fuzzy match #1.", 4, dmp.match_bitap("abcdefghijk", "efxhi", 0)); + + assertEquals("match_bitap: Fuzzy match #2.", 2, dmp.match_bitap("abcdefghijk", "cdefxyhijk", 5)); + + assertEquals("match_bitap: Fuzzy match #3.", -1, dmp.match_bitap("abcdefghijk", "bxy", 1)); + + assertEquals("match_bitap: Overflow.", 2, dmp.match_bitap("123456789xx0", "3456789x0", 2)); + + assertEquals("match_bitap: Before start match.", 0, dmp.match_bitap("abcdef", "xxabc", 4)); + + assertEquals("match_bitap: Beyond end match.", 3, dmp.match_bitap("abcdef", "defyy", 4)); + + assertEquals("match_bitap: Oversized pattern.", 0, dmp.match_bitap("abcdef", "xabcdefy", 0)); + + dmp.Match_Threshold = 0.4f; + assertEquals("match_bitap: Threshold #1.", 4, dmp.match_bitap("abcdefghijk", "efxyhi", 1)); + + dmp.Match_Threshold = 0.3f; + assertEquals("match_bitap: Threshold #2.", -1, dmp.match_bitap("abcdefghijk", "efxyhi", 1)); + + dmp.Match_Threshold = 0.0f; + assertEquals("match_bitap: Threshold #3.", 1, dmp.match_bitap("abcdefghijk", "bcdef", 1)); + + dmp.Match_Threshold = 0.5f; + assertEquals("match_bitap: Multiple select #1.", 0, dmp.match_bitap("abcdexyzabcde", "abccde", 3)); + + assertEquals("match_bitap: Multiple select #2.", 8, dmp.match_bitap("abcdexyzabcde", "abccde", 5)); + + dmp.Match_Distance = 10; // Strict location. + assertEquals("match_bitap: Distance test #1.", -1, dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24)); + + assertEquals("match_bitap: Distance test #2.", 0, dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdxxefg", 1)); + + dmp.Match_Distance = 1000; // Loose location. + assertEquals("match_bitap: Distance test #3.", 0, dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24)); + } + + public void testMatchMain() { + // Full match. + assertEquals("match_main: Equality.", 0, dmp.match_main("abcdef", "abcdef", 1000)); + + assertEquals("match_main: Null text.", -1, dmp.match_main("", "abcdef", 1)); + + assertEquals("match_main: Null pattern.", 3, dmp.match_main("abcdef", "", 3)); + + assertEquals("match_main: Exact match.", 3, dmp.match_main("abcdef", "de", 3)); + + assertEquals("match_main: Beyond end match.", 3, dmp.match_main("abcdef", "defy", 4)); + + assertEquals("match_main: Oversized pattern.", 0, dmp.match_main("abcdef", "abcdefy", 0)); + + dmp.Match_Threshold = 0.7f; + assertEquals("match_main: Complex match.", 4, dmp.match_main("I am the very model of a modern major general.", " that berry ", 5)); + dmp.Match_Threshold = 0.5f; + + // Test null inputs. + try { + dmp.match_main(null, null, 0); + fail("match_main: Null inputs."); + } catch (IllegalArgumentException ex) { + // Error expected. + } + } + + + // PATCH TEST FUNCTIONS + + + public void testPatchObj() { + // Patch Object. + Patch p = new Patch(); + p.start1 = 20; + p.start2 = 21; + p.length1 = 18; + p.length2 = 17; + p.diffs = diffList(new Diff(EQUAL, "jump"), new Diff(DELETE, "s"), new Diff(INSERT, "ed"), new Diff(EQUAL, " over "), new Diff(DELETE, "the"), new Diff(INSERT, "a"), new Diff(EQUAL, "\nlaz")); + String strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n"; + assertEquals("Patch: toString.", strp, p.toString()); + } + + public void testPatchFromText() { + assertTrue("patch_fromText: #0.", dmp.patch_fromText("").isEmpty()); + + String strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n"; + assertEquals("patch_fromText: #1.", strp, dmp.patch_fromText(strp).get(0).toString()); + + assertEquals("patch_fromText: #2.", "@@ -1 +1 @@\n-a\n+b\n", dmp.patch_fromText("@@ -1 +1 @@\n-a\n+b\n").get(0).toString()); + + assertEquals("patch_fromText: #3.", "@@ -1,3 +0,0 @@\n-abc\n", dmp.patch_fromText("@@ -1,3 +0,0 @@\n-abc\n").get(0).toString()); + + assertEquals("patch_fromText: #4.", "@@ -0,0 +1,3 @@\n+abc\n", dmp.patch_fromText("@@ -0,0 +1,3 @@\n+abc\n").get(0).toString()); + + // Generates error. + try { + dmp.patch_fromText("Bad\nPatch\n"); + fail("patch_fromText: #5."); + } catch (IllegalArgumentException ex) { + // Exception expected. + } + } + + public void testPatchToText() { + String strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n"; + List patches; + patches = dmp.patch_fromText(strp); + assertEquals("patch_toText: Single.", strp, dmp.patch_toText(patches)); + + strp = "@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n"; + patches = dmp.patch_fromText(strp); + assertEquals("patch_toText: Dual.", strp, dmp.patch_toText(patches)); + } + + public void testPatchAddContext() { + dmp.Patch_Margin = 4; + Patch p; + p = dmp.patch_fromText("@@ -21,4 +21,10 @@\n-jump\n+somersault\n").get(0); + dmp.patch_addContext(p, "The quick brown fox jumps over the lazy dog."); + assertEquals("patch_addContext: Simple case.", "@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n", p.toString()); + + p = dmp.patch_fromText("@@ -21,4 +21,10 @@\n-jump\n+somersault\n").get(0); + dmp.patch_addContext(p, "The quick brown fox jumps."); + assertEquals("patch_addContext: Not enough trailing context.", "@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n", p.toString()); + + p = dmp.patch_fromText("@@ -3 +3,2 @@\n-e\n+at\n").get(0); + dmp.patch_addContext(p, "The quick brown fox jumps."); + assertEquals("patch_addContext: Not enough leading context.", "@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n", p.toString()); + + p = dmp.patch_fromText("@@ -3 +3,2 @@\n-e\n+at\n").get(0); + dmp.patch_addContext(p, "The quick brown fox jumps. The quick brown fox crashes."); + assertEquals("patch_addContext: Ambiguity.", "@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n", p.toString()); + } + + @SuppressWarnings("deprecation") + public void testPatchMake() { + LinkedList patches; + patches = dmp.patch_make("", ""); + assertEquals("patch_make: Null case.", "", dmp.patch_toText(patches)); + + String text1 = "The quick brown fox jumps over the lazy dog."; + String text2 = "That quick brown fox jumped over a lazy dog."; + String expectedPatch = "@@ -1,8 +1,7 @@\n Th\n-at\n+e\n qui\n@@ -21,17 +21,18 @@\n jump\n-ed\n+s\n over \n-a\n+the\n laz\n"; + // The second patch must be "-21,17 +21,18", not "-22,17 +21,18" due to rolling context. + patches = dmp.patch_make(text2, text1); + assertEquals("patch_make: Text2+Text1 inputs.", expectedPatch, dmp.patch_toText(patches)); + + expectedPatch = "@@ -1,11 +1,12 @@\n Th\n-e\n+at\n quick b\n@@ -22,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n"; + patches = dmp.patch_make(text1, text2); + assertEquals("patch_make: Text1+Text2 inputs.", expectedPatch, dmp.patch_toText(patches)); + + LinkedList diffs = dmp.diff_main(text1, text2, false); + patches = dmp.patch_make(diffs); + assertEquals("patch_make: Diff input.", expectedPatch, dmp.patch_toText(patches)); + + patches = dmp.patch_make(text1, diffs); + assertEquals("patch_make: Text1+Diff inputs.", expectedPatch, dmp.patch_toText(patches)); + + patches = dmp.patch_make(text1, text2, diffs); + assertEquals("patch_make: Text1+Text2+Diff inputs (deprecated).", expectedPatch, dmp.patch_toText(patches)); + + patches = dmp.patch_make("`1234567890-=[]\\;',./", "~!@#$%^&*()_+{}|:\"<>?"); + assertEquals("patch_toText: Character encoding.", "@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n", dmp.patch_toText(patches)); + + diffs = diffList(new Diff(DELETE, "`1234567890-=[]\\;',./"), new Diff(INSERT, "~!@#$%^&*()_+{}|:\"<>?")); + assertEquals("patch_fromText: Character decoding.", diffs, dmp.patch_fromText("@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n").get(0).diffs); + + text1 = ""; + for (int x = 0; x < 100; x++) { + text1 += "abcdef"; + } + text2 = text1 + "123"; + expectedPatch = "@@ -573,28 +573,31 @@\n cdefabcdefabcdefabcdefabcdef\n+123\n"; + patches = dmp.patch_make(text1, text2); + assertEquals("patch_make: Long string with repeats.", expectedPatch, dmp.patch_toText(patches)); + + // Test null inputs. + try { + dmp.patch_make(null); + fail("patch_make: Null inputs."); + } catch (IllegalArgumentException ex) { + // Error expected. + } + } + + public void testPatchSplitMax() { + // Assumes that Match_MaxBits is 32. + LinkedList patches; + patches = dmp.patch_make("abcdefghijklmnopqrstuvwxyz01234567890", "XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0"); + dmp.patch_splitMax(patches); + assertEquals("patch_splitMax: #1.", "@@ -1,32 +1,46 @@\n+X\n ab\n+X\n cd\n+X\n ef\n+X\n gh\n+X\n ij\n+X\n kl\n+X\n mn\n+X\n op\n+X\n qr\n+X\n st\n+X\n uv\n+X\n wx\n+X\n yz\n+X\n 012345\n@@ -25,13 +39,18 @@\n zX01\n+X\n 23\n+X\n 45\n+X\n 67\n+X\n 89\n+X\n 0\n", dmp.patch_toText(patches)); + + patches = dmp.patch_make("abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz", "abcdefuvwxyz"); + String oldToText = dmp.patch_toText(patches); + dmp.patch_splitMax(patches); + assertEquals("patch_splitMax: #2.", oldToText, dmp.patch_toText(patches)); + + patches = dmp.patch_make("1234567890123456789012345678901234567890123456789012345678901234567890", "abc"); + dmp.patch_splitMax(patches); + assertEquals("patch_splitMax: #3.", "@@ -1,32 +1,4 @@\n-1234567890123456789012345678\n 9012\n@@ -29,32 +1,4 @@\n-9012345678901234567890123456\n 7890\n@@ -57,14 +1,3 @@\n-78901234567890\n+abc\n", dmp.patch_toText(patches)); + + patches = dmp.patch_make("abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1", "abcdefghij , h : 1 , t : 1 abcdefghij , h : 1 , t : 1 abcdefghij , h : 0 , t : 1"); + dmp.patch_splitMax(patches); + assertEquals("patch_splitMax: #4.", "@@ -2,32 +2,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n@@ -29,32 +29,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n", dmp.patch_toText(patches)); + } + + public void testPatchAddPadding() { + LinkedList patches; + patches = dmp.patch_make("", "test"); + assertEquals("patch_addPadding: Both edges full.", "@@ -0,0 +1,4 @@\n+test\n", dmp.patch_toText(patches)); + dmp.patch_addPadding(patches); + assertEquals("patch_addPadding: Both edges full.", "@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n", dmp.patch_toText(patches)); + + patches = dmp.patch_make("XY", "XtestY"); + assertEquals("patch_addPadding: Both edges partial.", "@@ -1,2 +1,6 @@\n X\n+test\n Y\n", dmp.patch_toText(patches)); + dmp.patch_addPadding(patches); + assertEquals("patch_addPadding: Both edges partial.", "@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n", dmp.patch_toText(patches)); + + patches = dmp.patch_make("XXXXYYYY", "XXXXtestYYYY"); + assertEquals("patch_addPadding: Both edges none.", "@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n", dmp.patch_toText(patches)); + dmp.patch_addPadding(patches); + assertEquals("patch_addPadding: Both edges none.", "@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n", dmp.patch_toText(patches)); + } + + public void testPatchApply() { + dmp.Match_Distance = 1000; + dmp.Match_Threshold = 0.5f; + dmp.Patch_DeleteThreshold = 0.5f; + LinkedList patches; + patches = dmp.patch_make("", ""); + Object[] results = dmp.patch_apply(patches, "Hello world."); + boolean[] boolArray = (boolean[]) results[1]; + String resultStr = results[0] + "\t" + boolArray.length; + assertEquals("patch_apply: Null case.", "Hello world.\t0", resultStr); + + patches = dmp.patch_make("The quick brown fox jumps over the lazy dog.", "That quick brown fox jumped over a lazy dog."); + results = dmp.patch_apply(patches, "The quick brown fox jumps over the lazy dog."); + boolArray = (boolean[]) results[1]; + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; + assertEquals("patch_apply: Exact match.", "That quick brown fox jumped over a lazy dog.\ttrue\ttrue", resultStr); + + results = dmp.patch_apply(patches, "The quick red rabbit jumps over the tired tiger."); + boolArray = (boolean[]) results[1]; + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; + assertEquals("patch_apply: Partial match.", "That quick red rabbit jumped over a tired tiger.\ttrue\ttrue", resultStr); + + results = dmp.patch_apply(patches, "I am the very model of a modern major general."); + boolArray = (boolean[]) results[1]; + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; + assertEquals("patch_apply: Failed match.", "I am the very model of a modern major general.\tfalse\tfalse", resultStr); + + patches = dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy"); + results = dmp.patch_apply(patches, "x123456789012345678901234567890-----++++++++++-----123456789012345678901234567890y"); + boolArray = (boolean[]) results[1]; + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; + assertEquals("patch_apply: Big delete, small change.", "xabcy\ttrue\ttrue", resultStr); + + patches = dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy"); + results = dmp.patch_apply(patches, "x12345678901234567890---------------++++++++++---------------12345678901234567890y"); + boolArray = (boolean[]) results[1]; + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; + assertEquals("patch_apply: Big delete, big change 1.", "xabc12345678901234567890---------------++++++++++---------------12345678901234567890y\tfalse\ttrue", resultStr); + + dmp.Patch_DeleteThreshold = 0.6f; + patches = dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy"); + results = dmp.patch_apply(patches, "x12345678901234567890---------------++++++++++---------------12345678901234567890y"); + boolArray = (boolean[]) results[1]; + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; + assertEquals("patch_apply: Big delete, big change 2.", "xabcy\ttrue\ttrue", resultStr); + dmp.Patch_DeleteThreshold = 0.5f; + + // Compensate for failed patch. + dmp.Match_Threshold = 0.0f; + dmp.Match_Distance = 0; + patches = dmp.patch_make("abcdefghijklmnopqrstuvwxyz--------------------1234567890", "abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------1234567YYYYYYYYYY890"); + results = dmp.patch_apply(patches, "ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890"); + boolArray = (boolean[]) results[1]; + resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; + assertEquals("patch_apply: Compensate for failed patch.", "ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890\tfalse\ttrue", resultStr); + dmp.Match_Threshold = 0.5f; + dmp.Match_Distance = 1000; + + patches = dmp.patch_make("", "test"); + String patchStr = dmp.patch_toText(patches); + dmp.patch_apply(patches, ""); + assertEquals("patch_apply: No side effects.", patchStr, dmp.patch_toText(patches)); + + patches = dmp.patch_make("The quick brown fox jumps over the lazy dog.", "Woof"); + patchStr = dmp.patch_toText(patches); + dmp.patch_apply(patches, "The quick brown fox jumps over the lazy dog."); + assertEquals("patch_apply: No side effects with major delete.", patchStr, dmp.patch_toText(patches)); + + patches = dmp.patch_make("", "test"); + results = dmp.patch_apply(patches, ""); + boolArray = (boolean[]) results[1]; + resultStr = results[0] + "\t" + boolArray[0]; + assertEquals("patch_apply: Edge exact match.", "test\ttrue", resultStr); + + patches = dmp.patch_make("XY", "XtestY"); + results = dmp.patch_apply(patches, "XY"); + boolArray = (boolean[]) results[1]; + resultStr = results[0] + "\t" + boolArray[0]; + assertEquals("patch_apply: Near edge exact match.", "XtestY\ttrue", resultStr); + + patches = dmp.patch_make("y", "y123"); + results = dmp.patch_apply(patches, "x"); + boolArray = (boolean[]) results[1]; + resultStr = results[0] + "\t" + boolArray[0]; + assertEquals("patch_apply: Edge partial match.", "x123\ttrue", resultStr); + } + + private void assertArrayEquals(String error_msg, Object[] a, Object[] b) { + List list_a = Arrays.asList(a); + List list_b = Arrays.asList(b); + assertEquals(error_msg, list_a, list_b); + } + + private void assertLinesToCharsResultEquals(String error_msg, + LinesToCharsResult a, LinesToCharsResult b) { + assertEquals(error_msg, a.chars1, b.chars1); + assertEquals(error_msg, a.chars2, b.chars2); + assertEquals(error_msg, a.lineArray, b.lineArray); + } + + // Construct the two texts which made up the diff originally. + private static String[] diff_rebuildtexts(LinkedList diffs) { + String[] text = {"", ""}; + for (Diff myDiff : diffs) { + if (myDiff.operation != diff_match_patch.Operation.INSERT) { + text[0] += myDiff.text; + } + if (myDiff.operation != diff_match_patch.Operation.DELETE) { + text[1] += myDiff.text; + } + } + return text; + } + + // Private function for quickly building lists of diffs. + private static LinkedList diffList(Diff... diffs) { + LinkedList myDiffList = new LinkedList(); + for (Diff myDiff : diffs) { + myDiffList.add(myDiff); + } + return myDiffList; + } +} diff --git a/javascript/diff_match_patch.js b/javascript/diff_match_patch.js new file mode 100644 index 0000000..c41b513 --- /dev/null +++ b/javascript/diff_match_patch.js @@ -0,0 +1,49 @@ +(function(){function diff_match_patch(){this.Diff_Timeout=1;this.Diff_EditCost=4;this.Match_Threshold=0.5;this.Match_Distance=1E3;this.Patch_DeleteThreshold=0.5;this.Patch_Margin=4;this.Match_MaxBits=32} +diff_match_patch.prototype.diff_main=function(a,b,c,d){"undefined"==typeof d&&(d=0>=this.Diff_Timeout?Number.MAX_VALUE:(new Date).getTime()+1E3*this.Diff_Timeout);if(null==a||null==b)throw Error("Null input. (diff_main)");if(a==b)return a?[[0,a]]:[];"undefined"==typeof c&&(c=!0);var e=c,f=this.diff_commonPrefix(a,b);c=a.substring(0,f);a=a.substring(f);b=b.substring(f);var f=this.diff_commonSuffix(a,b),g=a.substring(a.length-f);a=a.substring(0,a.length-f);b=b.substring(0,b.length-f);a=this.diff_compute_(a, +b,e,d);c&&a.unshift([0,c]);g&&a.push([0,g]);this.diff_cleanupMerge(a);return a}; +diff_match_patch.prototype.diff_compute_=function(a,b,c,d){if(!a)return[[1,b]];if(!b)return[[-1,a]];var e=a.length>b.length?a:b,f=a.length>b.length?b:a,g=e.indexOf(f);return-1!=g?(c=[[1,e.substring(0,g)],[0,f],[1,e.substring(g+f.length)]],a.length>b.length&&(c[0][0]=c[2][0]=-1),c):1==f.length?[[-1,a],[1,b]]:(e=this.diff_halfMatch_(a,b))?(f=e[0],a=e[1],g=e[2],b=e[3],e=e[4],f=this.diff_main(f,g,c,d),c=this.diff_main(a,b,c,d),f.concat([[0,e]],c)):c&&100c);v++){for(var n=-v+r;n<=v-t;n+=2){var l=g+n,m;m=n==-v||n!=v&&j[l-1]d)t+=2;else if(s>e)r+=2;else if(q&&(l=g+k-n,0<=l&&l= +u)return this.diff_bisectSplit_(a,b,m,s,c)}}for(n=-v+p;n<=v-w;n+=2){l=g+n;u=n==-v||n!=v&&i[l-1]d)w+=2;else if(m>e)p+=2;else if(!q&&(l=g+k-n,0<=l&&(l=u)))return this.diff_bisectSplit_(a,b,m,s,c)}}return[[-1,a],[1,b]]}; +diff_match_patch.prototype.diff_bisectSplit_=function(a,b,c,d,e){var f=a.substring(0,c),g=b.substring(0,d);a=a.substring(c);b=b.substring(d);f=this.diff_main(f,g,!1,e);e=this.diff_main(a,b,!1,e);return f.concat(e)}; +diff_match_patch.prototype.diff_linesToChars_=function(a,b){function c(a){for(var b="",c=0,f=-1,g=d.length;fd?a=a.substring(c-d):c=a.length?[h,j,n,l,g]:null}if(0>=this.Diff_Timeout)return null; +var d=a.length>b.length?a:b,e=a.length>b.length?b:a;if(4>d.length||2*e.lengthd[4].length?g:d:d:g;var j;a.length>b.length?(g=h[0],d=h[1],e=h[2],j=h[3]):(e=h[0],j=h[1],g=h[2],d=h[3]);h=h[4];return[g,d,e,j,h]}; +diff_match_patch.prototype.diff_cleanupSemantic=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=0,h=0,j=0,i=0;f=e){if(d>=b.length/2||d>=c.length/2)a.splice(f,0,[0,c.substring(0,d)]),a[f-1][1]=b.substring(0,b.length-d),a[f+1][1]=c.substring(d),f++}else if(e>=b.length/2||e>=c.length/2)a.splice(f,0,[0,b.substring(0,e)]),a[f-1][0]=1,a[f-1][1]=c.substring(0,c.length-e),a[f+1][0]=-1,a[f+1][1]=b.substring(e),f++;f++}f++}}; +diff_match_patch.prototype.diff_cleanupSemanticLossless=function(a){function b(a,b){if(!a||!b)return 6;var c=a.charAt(a.length-1),d=b.charAt(0),e=c.match(diff_match_patch.nonAlphaNumericRegex_),f=d.match(diff_match_patch.nonAlphaNumericRegex_),g=e&&c.match(diff_match_patch.whitespaceRegex_),h=f&&d.match(diff_match_patch.whitespaceRegex_),c=g&&c.match(diff_match_patch.linebreakRegex_),d=h&&d.match(diff_match_patch.linebreakRegex_),i=c&&a.match(diff_match_patch.blanklineEndRegex_),j=d&&b.match(diff_match_patch.blanklineStartRegex_); +return i||j?5:c||d?4:e&&!g&&h?3:g||h?2:e||f?1:0}for(var c=1;c=i&&(i=k,g=d,h=e,j=f)}a[c-1][1]!=g&&(g?a[c-1][1]=g:(a.splice(c-1,1),c--),a[c][1]= +h,j?a[c+1][1]=j:(a.splice(c+1,1),c--))}c++}};diff_match_patch.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/;diff_match_patch.whitespaceRegex_=/\s/;diff_match_patch.linebreakRegex_=/[\r\n]/;diff_match_patch.blanklineEndRegex_=/\n\r?\n$/;diff_match_patch.blanklineStartRegex_=/^\r?\n\r?\n/; +diff_match_patch.prototype.diff_cleanupEfficiency=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=!1,h=!1,j=!1,i=!1;fb)break;e=c;f=d}return a.length!=g&&-1===a[g][0]?f:f+(b-e)}; +diff_match_patch.prototype.diff_prettyHtml=function(a){for(var b=[],c=/&/g,d=//g,f=/\n/g,g=0;g");switch(h){case 1:b[g]=''+j+"";break;case -1:b[g]=''+j+"";break;case 0:b[g]=""+j+""}}return b.join("")}; +diff_match_patch.prototype.diff_text1=function(a){for(var b=[],c=0;cthis.Match_MaxBits)throw Error("Pattern too long for this browser.");var e=this.match_alphabet_(b),f=this,g=this.Match_Threshold,h=a.indexOf(b,c);-1!=h&&(g=Math.min(d(0,h),g),h=a.lastIndexOf(b,c+b.length),-1!=h&&(g=Math.min(d(0,h),g)));for(var j=1<=i;p--){var w=e[a.charAt(p-1)];k[p]=0===t?(k[p+1]<<1|1)&w:(k[p+1]<<1|1)&w|((r[p+1]|r[p])<<1|1)|r[p+1];if(k[p]&j&&(w=d(t,p-1),w<=g))if(g=w,h=p-1,h>c)i=Math.max(1,2*c-h);else break}if(d(t+1,c)>g)break;r=k}return h}; +diff_match_patch.prototype.match_alphabet_=function(a){for(var b={},c=0;c=2*this.Patch_Margin&& +e&&(this.patch_addContext_(a,h),c.push(a),a=new diff_match_patch.patch_obj,e=0,h=d,f=g)}1!==i&&(f+=k.length);-1!==i&&(g+=k.length)}e&&(this.patch_addContext_(a,h),c.push(a));return c};diff_match_patch.prototype.patch_deepCopy=function(a){for(var b=[],c=0;cthis.Match_MaxBits){if(j=this.match_main(b,h.substring(0,this.Match_MaxBits),g),-1!=j&&(i=this.match_main(b,h.substring(h.length-this.Match_MaxBits),g+h.length-this.Match_MaxBits),-1==i||j>=i))j=-1}else j=this.match_main(b,h,g); +if(-1==j)e[f]=!1,d-=a[f].length2-a[f].length1;else if(e[f]=!0,d=j-g,g=-1==i?b.substring(j,j+h.length):b.substring(j,i+this.Match_MaxBits),h==g)b=b.substring(0,j)+this.diff_text2(a[f].diffs)+b.substring(j+h.length);else if(g=this.diff_main(h,g,!1),h.length>this.Match_MaxBits&&this.diff_levenshtein(g)/h.length>this.Patch_DeleteThreshold)e[f]=!1;else{this.diff_cleanupSemanticLossless(g);for(var h=0,k,i=0;ie[0][1].length){var f=b-e[0][1].length;e[0][1]=c.substring(e[0][1].length)+e[0][1];d.start1-=f;d.start2-=f;d.length1+=f;d.length2+=f}d=a[a.length-1];e=d.diffs;0==e.length||0!=e[e.length-1][0]?(e.push([0, +c]),d.length1+=b,d.length2+=b):b>e[e.length-1][1].length&&(f=b-e[e.length-1][1].length,e[e.length-1][1]+=c.substring(0,f),d.length1+=f,d.length2+=f);return c}; +diff_match_patch.prototype.patch_splitMax=function(a){for(var b=this.Match_MaxBits,c=0;c2*b?(h.length1+=i.length,e+=i.length,j=!1,h.diffs.push([g,i]),d.diffs.shift()):(i=i.substring(0,b-h.length1-this.Patch_Margin),h.length1+=i.length,e+=i.length,0===g?(h.length2+=i.length,f+=i.length):j=!1,h.diffs.push([g,i]),i==d.diffs[0][1]?d.diffs.shift():d.diffs[0][1]=d.diffs[0][1].substring(i.length))}g=this.diff_text2(h.diffs);g=g.substring(g.length-this.Patch_Margin);i=this.diff_text1(d.diffs).substring(0,this.Patch_Margin);""!==i&& +(h.length1+=i.length,h.length2+=i.length,0!==h.diffs.length&&0===h.diffs[h.diffs.length-1][0]?h.diffs[h.diffs.length-1][1]+=i:h.diffs.push([0,i]));j||a.splice(++c,0,h)}}};diff_match_patch.prototype.patch_toText=function(a){for(var b=[],c=0;c + + + + + + + + + + + + + + +

    If debugging errors, start with the first reported error, + subsequent tests often rely on earlier ones.

    + + + + diff --git a/javascript/diff_match_patch_test.js b/javascript/diff_match_patch_test.js new file mode 100644 index 0000000..9652197 --- /dev/null +++ b/javascript/diff_match_patch_test.js @@ -0,0 +1,937 @@ +/** + * Diff Match and Patch -- Test Harness + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +// If expected and actual are the equivalent, pass the test. +function assertEquivalent(msg, expected, actual) { + if (typeof actual == 'undefined') { + // msg is optional. + actual = expected; + expected = msg; + msg = 'Expected: \'' + expected + '\' Actual: \'' + actual + '\''; + } + if (_equivalent(expected, actual)) { + assertEquals(msg, String.toString(expected), String.toString(actual)); + } else { + assertEquals(msg, expected, actual); + } +} + + +// Are a and b the equivalent? -- Recursive. +function _equivalent(a, b) { + if (a == b) { + return true; + } + if (typeof a == 'object' && typeof b == 'object' && a !== null && b !== null) { + if (a.toString() != b.toString()) { + return false; + } + for (var p in a) { + if (!_equivalent(a[p], b[p])) { + return false; + } + } + for (var p in b) { + if (!_equivalent(a[p], b[p])) { + return false; + } + } + return true; + } + return false; +} + + +function diff_rebuildtexts(diffs) { + // Construct the two texts which made up the diff originally. + var text1 = ''; + var text2 = ''; + for (var x = 0; x < diffs.length; x++) { + if (diffs[x][0] != DIFF_INSERT) { + text1 += diffs[x][1]; + } + if (diffs[x][0] != DIFF_DELETE) { + text2 += diffs[x][1]; + } + } + return [text1, text2]; +} + +var dmp = new diff_match_patch(); + + +// DIFF TEST FUNCTIONS + + +function testDiffCommonPrefix() { + // Detect any common prefix. + // Null case. + assertEquals(0, dmp.diff_commonPrefix('abc', 'xyz')); + + // Non-null case. + assertEquals(4, dmp.diff_commonPrefix('1234abcdef', '1234xyz')); + + // Whole case. + assertEquals(4, dmp.diff_commonPrefix('1234', '1234xyz')); +} + +function testDiffCommonSuffix() { + // Detect any common suffix. + // Null case. + assertEquals(0, dmp.diff_commonSuffix('abc', 'xyz')); + + // Non-null case. + assertEquals(4, dmp.diff_commonSuffix('abcdef1234', 'xyz1234')); + + // Whole case. + assertEquals(4, dmp.diff_commonSuffix('1234', 'xyz1234')); +} + +function testDiffCommonOverlap() { + // Detect any suffix/prefix overlap. + // Null case. + assertEquals(0, dmp.diff_commonOverlap_('', 'abcd')); + + // Whole case. + assertEquals(3, dmp.diff_commonOverlap_('abc', 'abcd')); + + // No overlap. + assertEquals(0, dmp.diff_commonOverlap_('123456', 'abcd')); + + // Overlap. + assertEquals(3, dmp.diff_commonOverlap_('123456xxx', 'xxxabcd')); + + // Unicode. + // Some overly clever languages (C#) may treat ligatures as equal to their + // component letters. E.g. U+FB01 == 'fi' + assertEquals(0, dmp.diff_commonOverlap_('fi', '\ufb01i')); +} + +function testDiffHalfMatch() { + // Detect a halfmatch. + dmp.Diff_Timeout = 1; + // No match. + assertEquals(null, dmp.diff_halfMatch_('1234567890', 'abcdef')); + + assertEquals(null, dmp.diff_halfMatch_('12345', '23')); + + // Single Match. + assertEquivalent(['12', '90', 'a', 'z', '345678'], dmp.diff_halfMatch_('1234567890', 'a345678z')); + + assertEquivalent(['a', 'z', '12', '90', '345678'], dmp.diff_halfMatch_('a345678z', '1234567890')); + + assertEquivalent(['abc', 'z', '1234', '0', '56789'], dmp.diff_halfMatch_('abc56789z', '1234567890')); + + assertEquivalent(['a', 'xyz', '1', '7890', '23456'], dmp.diff_halfMatch_('a23456xyz', '1234567890')); + + // Multiple Matches. + assertEquivalent(['12123', '123121', 'a', 'z', '1234123451234'], dmp.diff_halfMatch_('121231234123451234123121', 'a1234123451234z')); + + assertEquivalent(['', '-=-=-=-=-=', 'x', '', 'x-=-=-=-=-=-=-='], dmp.diff_halfMatch_('x-=-=-=-=-=-=-=-=-=-=-=-=', 'xx-=-=-=-=-=-=-=')); + + assertEquivalent(['-=-=-=-=-=', '', '', 'y', '-=-=-=-=-=-=-=y'], dmp.diff_halfMatch_('-=-=-=-=-=-=-=-=-=-=-=-=y', '-=-=-=-=-=-=-=yy')); + + // Non-optimal halfmatch. + // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy + assertEquivalent(['qHillo', 'w', 'x', 'Hulloy', 'HelloHe'], dmp.diff_halfMatch_('qHilloHelloHew', 'xHelloHeHulloy')); + + // Optimal no halfmatch. + dmp.Diff_Timeout = 0; + assertEquals(null, dmp.diff_halfMatch_('qHilloHelloHew', 'xHelloHeHulloy')); +} + +function testDiffLinesToChars() { + function assertLinesToCharsResultEquals(a, b) { + assertEquals(a.chars1, b.chars1); + assertEquals(a.chars2, b.chars2); + assertEquivalent(a.lineArray, b.lineArray); + } + + // Convert lines down to characters. + assertLinesToCharsResultEquals({chars1: '\x01\x02\x01', chars2: '\x02\x01\x02', lineArray: ['', 'alpha\n', 'beta\n']}, dmp.diff_linesToChars_('alpha\nbeta\nalpha\n', 'beta\nalpha\nbeta\n')); + + assertLinesToCharsResultEquals({chars1: '', chars2: '\x01\x02\x03\x03', lineArray: ['', 'alpha\r\n', 'beta\r\n', '\r\n']}, dmp.diff_linesToChars_('', 'alpha\r\nbeta\r\n\r\n\r\n')); + + assertLinesToCharsResultEquals({chars1: '\x01', chars2: '\x02', lineArray: ['', 'a', 'b']}, dmp.diff_linesToChars_('a', 'b')); + + // More than 256 to reveal any 8-bit limitations. + var n = 300; + var lineList = []; + var charList = []; + for (var x = 1; x < n + 1; x++) { + lineList[x - 1] = x + '\n'; + charList[x - 1] = String.fromCharCode(x); + } + assertEquals(n, lineList.length); + var lines = lineList.join(''); + var chars = charList.join(''); + assertEquals(n, chars.length); + lineList.unshift(''); + assertLinesToCharsResultEquals({chars1: chars, chars2: '', lineArray: lineList}, dmp.diff_linesToChars_(lines, '')); +} + +function testDiffCharsToLines() { + // Convert chars up to lines. + var diffs = [[DIFF_EQUAL, '\x01\x02\x01'], [DIFF_INSERT, '\x02\x01\x02']]; + dmp.diff_charsToLines_(diffs, ['', 'alpha\n', 'beta\n']); + assertEquivalent([[DIFF_EQUAL, 'alpha\nbeta\nalpha\n'], [DIFF_INSERT, 'beta\nalpha\nbeta\n']], diffs); + + // More than 256 to reveal any 8-bit limitations. + var n = 300; + var lineList = []; + var charList = []; + for (var x = 1; x < n + 1; x++) { + lineList[x - 1] = x + '\n'; + charList[x - 1] = String.fromCharCode(x); + } + assertEquals(n, lineList.length); + var lines = lineList.join(''); + var chars = charList.join(''); + assertEquals(n, chars.length); + lineList.unshift(''); + var diffs = [[DIFF_DELETE, chars]]; + dmp.diff_charsToLines_(diffs, lineList); + assertEquivalent([[DIFF_DELETE, lines]], diffs); +} + +function testDiffCleanupMerge() { + // Cleanup a messy diff. + // Null case. + var diffs = []; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([], diffs); + + // No change case. + diffs = [[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'b'], [DIFF_INSERT, 'c']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'b'], [DIFF_INSERT, 'c']], diffs); + + // Merge equalities. + diffs = [[DIFF_EQUAL, 'a'], [DIFF_EQUAL, 'b'], [DIFF_EQUAL, 'c']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_EQUAL, 'abc']], diffs); + + // Merge deletions. + diffs = [[DIFF_DELETE, 'a'], [DIFF_DELETE, 'b'], [DIFF_DELETE, 'c']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_DELETE, 'abc']], diffs); + + // Merge insertions. + diffs = [[DIFF_INSERT, 'a'], [DIFF_INSERT, 'b'], [DIFF_INSERT, 'c']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_INSERT, 'abc']], diffs); + + // Merge interweave. + diffs = [[DIFF_DELETE, 'a'], [DIFF_INSERT, 'b'], [DIFF_DELETE, 'c'], [DIFF_INSERT, 'd'], [DIFF_EQUAL, 'e'], [DIFF_EQUAL, 'f']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_DELETE, 'ac'], [DIFF_INSERT, 'bd'], [DIFF_EQUAL, 'ef']], diffs); + + // Prefix and suffix detection. + diffs = [[DIFF_DELETE, 'a'], [DIFF_INSERT, 'abc'], [DIFF_DELETE, 'dc']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'd'], [DIFF_INSERT, 'b'], [DIFF_EQUAL, 'c']], diffs); + + // Prefix and suffix detection with equalities. + diffs = [[DIFF_EQUAL, 'x'], [DIFF_DELETE, 'a'], [DIFF_INSERT, 'abc'], [DIFF_DELETE, 'dc'], [DIFF_EQUAL, 'y']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_EQUAL, 'xa'], [DIFF_DELETE, 'd'], [DIFF_INSERT, 'b'], [DIFF_EQUAL, 'cy']], diffs); + + // Slide edit left. + diffs = [[DIFF_EQUAL, 'a'], [DIFF_INSERT, 'ba'], [DIFF_EQUAL, 'c']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_INSERT, 'ab'], [DIFF_EQUAL, 'ac']], diffs); + + // Slide edit right. + diffs = [[DIFF_EQUAL, 'c'], [DIFF_INSERT, 'ab'], [DIFF_EQUAL, 'a']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_EQUAL, 'ca'], [DIFF_INSERT, 'ba']], diffs); + + // Slide edit left recursive. + diffs = [[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'b'], [DIFF_EQUAL, 'c'], [DIFF_DELETE, 'ac'], [DIFF_EQUAL, 'x']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_DELETE, 'abc'], [DIFF_EQUAL, 'acx']], diffs); + + // Slide edit right recursive. + diffs = [[DIFF_EQUAL, 'x'], [DIFF_DELETE, 'ca'], [DIFF_EQUAL, 'c'], [DIFF_DELETE, 'b'], [DIFF_EQUAL, 'a']]; + dmp.diff_cleanupMerge(diffs); + assertEquivalent([[DIFF_EQUAL, 'xca'], [DIFF_DELETE, 'cba']], diffs); +} + +function testDiffCleanupSemanticLossless() { + // Slide diffs to match logical boundaries. + // Null case. + var diffs = []; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([], diffs); + + // Blank lines. + diffs = [[DIFF_EQUAL, 'AAA\r\n\r\nBBB'], [DIFF_INSERT, '\r\nDDD\r\n\r\nBBB'], [DIFF_EQUAL, '\r\nEEE']]; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([[DIFF_EQUAL, 'AAA\r\n\r\n'], [DIFF_INSERT, 'BBB\r\nDDD\r\n\r\n'], [DIFF_EQUAL, 'BBB\r\nEEE']], diffs); + + // Line boundaries. + diffs = [[DIFF_EQUAL, 'AAA\r\nBBB'], [DIFF_INSERT, ' DDD\r\nBBB'], [DIFF_EQUAL, ' EEE']]; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([[DIFF_EQUAL, 'AAA\r\n'], [DIFF_INSERT, 'BBB DDD\r\n'], [DIFF_EQUAL, 'BBB EEE']], diffs); + + // Word boundaries. + diffs = [[DIFF_EQUAL, 'The c'], [DIFF_INSERT, 'ow and the c'], [DIFF_EQUAL, 'at.']]; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([[DIFF_EQUAL, 'The '], [DIFF_INSERT, 'cow and the '], [DIFF_EQUAL, 'cat.']], diffs); + + // Alphanumeric boundaries. + diffs = [[DIFF_EQUAL, 'The-c'], [DIFF_INSERT, 'ow-and-the-c'], [DIFF_EQUAL, 'at.']]; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([[DIFF_EQUAL, 'The-'], [DIFF_INSERT, 'cow-and-the-'], [DIFF_EQUAL, 'cat.']], diffs); + + // Hitting the start. + diffs = [[DIFF_EQUAL, 'a'], [DIFF_DELETE, 'a'], [DIFF_EQUAL, 'ax']]; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([[DIFF_DELETE, 'a'], [DIFF_EQUAL, 'aax']], diffs); + + // Hitting the end. + diffs = [[DIFF_EQUAL, 'xa'], [DIFF_DELETE, 'a'], [DIFF_EQUAL, 'a']]; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([[DIFF_EQUAL, 'xaa'], [DIFF_DELETE, 'a']], diffs); + + // Sentence boundaries. + diffs = [[DIFF_EQUAL, 'The xxx. The '], [DIFF_INSERT, 'zzz. The '], [DIFF_EQUAL, 'yyy.']]; + dmp.diff_cleanupSemanticLossless(diffs); + assertEquivalent([[DIFF_EQUAL, 'The xxx.'], [DIFF_INSERT, ' The zzz.'], [DIFF_EQUAL, ' The yyy.']], diffs); +} + +function testDiffCleanupSemantic() { + // Cleanup semantically trivial equalities. + // Null case. + var diffs = []; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([], diffs); + + // No elimination #1. + diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, 'cd'], [DIFF_EQUAL, '12'], [DIFF_DELETE, 'e']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'ab'], [DIFF_INSERT, 'cd'], [DIFF_EQUAL, '12'], [DIFF_DELETE, 'e']], diffs); + + // No elimination #2. + diffs = [[DIFF_DELETE, 'abc'], [DIFF_INSERT, 'ABC'], [DIFF_EQUAL, '1234'], [DIFF_DELETE, 'wxyz']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'abc'], [DIFF_INSERT, 'ABC'], [DIFF_EQUAL, '1234'], [DIFF_DELETE, 'wxyz']], diffs); + + // Simple elimination. + diffs = [[DIFF_DELETE, 'a'], [DIFF_EQUAL, 'b'], [DIFF_DELETE, 'c']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'abc'], [DIFF_INSERT, 'b']], diffs); + + // Backpass elimination. + diffs = [[DIFF_DELETE, 'ab'], [DIFF_EQUAL, 'cd'], [DIFF_DELETE, 'e'], [DIFF_EQUAL, 'f'], [DIFF_INSERT, 'g']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'abcdef'], [DIFF_INSERT, 'cdfg']], diffs); + + // Multiple eliminations. + diffs = [[DIFF_INSERT, '1'], [DIFF_EQUAL, 'A'], [DIFF_DELETE, 'B'], [DIFF_INSERT, '2'], [DIFF_EQUAL, '_'], [DIFF_INSERT, '1'], [DIFF_EQUAL, 'A'], [DIFF_DELETE, 'B'], [DIFF_INSERT, '2']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'AB_AB'], [DIFF_INSERT, '1A2_1A2']], diffs); + + // Word boundaries. + diffs = [[DIFF_EQUAL, 'The c'], [DIFF_DELETE, 'ow and the c'], [DIFF_EQUAL, 'at.']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_EQUAL, 'The '], [DIFF_DELETE, 'cow and the '], [DIFF_EQUAL, 'cat.']], diffs); + + // No overlap elimination. + diffs = [[DIFF_DELETE, 'abcxx'], [DIFF_INSERT, 'xxdef']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'abcxx'], [DIFF_INSERT, 'xxdef']], diffs); + + // Overlap elimination. + diffs = [[DIFF_DELETE, 'abcxxx'], [DIFF_INSERT, 'xxxdef']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'abc'], [DIFF_EQUAL, 'xxx'], [DIFF_INSERT, 'def']], diffs); + + // Reverse overlap elimination. + diffs = [[DIFF_DELETE, 'xxxabc'], [DIFF_INSERT, 'defxxx']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_INSERT, 'def'], [DIFF_EQUAL, 'xxx'], [DIFF_DELETE, 'abc']], diffs); + + // Two overlap eliminations. + diffs = [[DIFF_DELETE, 'abcd1212'], [DIFF_INSERT, '1212efghi'], [DIFF_EQUAL, '----'], [DIFF_DELETE, 'A3'], [DIFF_INSERT, '3BC']]; + dmp.diff_cleanupSemantic(diffs); + assertEquivalent([[DIFF_DELETE, 'abcd'], [DIFF_EQUAL, '1212'], [DIFF_INSERT, 'efghi'], [DIFF_EQUAL, '----'], [DIFF_DELETE, 'A'], [DIFF_EQUAL, '3'], [DIFF_INSERT, 'BC']], diffs); +} + +function testDiffCleanupEfficiency() { + // Cleanup operationally trivial equalities. + dmp.Diff_EditCost = 4; + // Null case. + var diffs = []; + dmp.diff_cleanupEfficiency(diffs); + assertEquivalent([], diffs); + + // No elimination. + diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'wxyz'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']]; + dmp.diff_cleanupEfficiency(diffs); + assertEquivalent([[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'wxyz'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']], diffs); + + // Four-edit elimination. + diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'xyz'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']]; + dmp.diff_cleanupEfficiency(diffs); + assertEquivalent([[DIFF_DELETE, 'abxyzcd'], [DIFF_INSERT, '12xyz34']], diffs); + + // Three-edit elimination. + diffs = [[DIFF_INSERT, '12'], [DIFF_EQUAL, 'x'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']]; + dmp.diff_cleanupEfficiency(diffs); + assertEquivalent([[DIFF_DELETE, 'xcd'], [DIFF_INSERT, '12x34']], diffs); + + // Backpass elimination. + diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'xy'], [DIFF_INSERT, '34'], [DIFF_EQUAL, 'z'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '56']]; + dmp.diff_cleanupEfficiency(diffs); + assertEquivalent([[DIFF_DELETE, 'abxyzcd'], [DIFF_INSERT, '12xy34z56']], diffs); + + // High cost elimination. + dmp.Diff_EditCost = 5; + diffs = [[DIFF_DELETE, 'ab'], [DIFF_INSERT, '12'], [DIFF_EQUAL, 'wxyz'], [DIFF_DELETE, 'cd'], [DIFF_INSERT, '34']]; + dmp.diff_cleanupEfficiency(diffs); + assertEquivalent([[DIFF_DELETE, 'abwxyzcd'], [DIFF_INSERT, '12wxyz34']], diffs); + dmp.Diff_EditCost = 4; +} + +function testDiffPrettyHtml() { + // Pretty print. + var diffs = [[DIFF_EQUAL, 'a\n'], [DIFF_DELETE, 'b'], [DIFF_INSERT, 'c&d']]; + assertEquals('
    <B>b</B>c&d', dmp.diff_prettyHtml(diffs)); +} + +function testDiffText() { + // Compute the source and destination texts. + var diffs = [[DIFF_EQUAL, 'jump'], [DIFF_DELETE, 's'], [DIFF_INSERT, 'ed'], [DIFF_EQUAL, ' over '], [DIFF_DELETE, 'the'], [DIFF_INSERT, 'a'], [DIFF_EQUAL, ' lazy']]; + assertEquals('jumps over the lazy', dmp.diff_text1(diffs)); + + assertEquals('jumped over a lazy', dmp.diff_text2(diffs)); +} + +function testDiffDelta() { + // Convert a diff into delta string. + var diffs = [[DIFF_EQUAL, 'jump'], [DIFF_DELETE, 's'], [DIFF_INSERT, 'ed'], [DIFF_EQUAL, ' over '], [DIFF_DELETE, 'the'], [DIFF_INSERT, 'a'], [DIFF_EQUAL, ' lazy'], [DIFF_INSERT, 'old dog']]; + var text1 = dmp.diff_text1(diffs); + assertEquals('jumps over the lazy', text1); + + var delta = dmp.diff_toDelta(diffs); + assertEquals('=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog', delta); + + // Convert delta string into a diff. + assertEquivalent(diffs, dmp.diff_fromDelta(text1, delta)); + + // Generates error (19 != 20). + try { + dmp.diff_fromDelta(text1 + 'x', delta); + assertEquals(Error, null); + } catch (e) { + // Exception expected. + } + + // Generates error (19 != 18). + try { + dmp.diff_fromDelta(text1.substring(1), delta); + assertEquals(Error, null); + } catch (e) { + // Exception expected. + } + + // Generates error (%c3%xy invalid Unicode). + try { + dmp.diff_fromDelta('', '+%c3%xy'); + assertEquals(Error, null); + } catch (e) { + // Exception expected. + } + + // Test deltas with special characters. + diffs = [[DIFF_EQUAL, '\u0680 \x00 \t %'], [DIFF_DELETE, '\u0681 \x01 \n ^'], [DIFF_INSERT, '\u0682 \x02 \\ |']]; + text1 = dmp.diff_text1(diffs); + assertEquals('\u0680 \x00 \t %\u0681 \x01 \n ^', text1); + + delta = dmp.diff_toDelta(diffs); + assertEquals('=7\t-7\t+%DA%82 %02 %5C %7C', delta); + + // Convert delta string into a diff. + assertEquivalent(diffs, dmp.diff_fromDelta(text1, delta)); + + // Verify pool of unchanged characters. + diffs = [[DIFF_INSERT, 'A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ']]; + var text2 = dmp.diff_text2(diffs); + assertEquals('A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ', text2); + + delta = dmp.diff_toDelta(diffs); + assertEquals('+A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ', delta); + + // Convert delta string into a diff. + assertEquivalent(diffs, dmp.diff_fromDelta('', delta)); +} + +function testDiffXIndex() { + // Translate a location in text1 to text2. + // Translation on equality. + assertEquals(5, dmp.diff_xIndex([[DIFF_DELETE, 'a'], [DIFF_INSERT, '1234'], [DIFF_EQUAL, 'xyz']], 2)); + + // Translation on deletion. + assertEquals(1, dmp.diff_xIndex([[DIFF_EQUAL, 'a'], [DIFF_DELETE, '1234'], [DIFF_EQUAL, 'xyz']], 3)); +} + +function testDiffLevenshtein() { + // Levenshtein with trailing equality. + assertEquals(4, dmp.diff_levenshtein([[DIFF_DELETE, 'abc'], [DIFF_INSERT, '1234'], [DIFF_EQUAL, 'xyz']])); + // Levenshtein with leading equality. + assertEquals(4, dmp.diff_levenshtein([[DIFF_EQUAL, 'xyz'], [DIFF_DELETE, 'abc'], [DIFF_INSERT, '1234']])); + // Levenshtein with middle equality. + assertEquals(7, dmp.diff_levenshtein([[DIFF_DELETE, 'abc'], [DIFF_EQUAL, 'xyz'], [DIFF_INSERT, '1234']])); +} + +function testDiffBisect() { + // Normal. + var a = 'cat'; + var b = 'map'; + // Since the resulting diff hasn't been normalized, it would be ok if + // the insertion and deletion pairs are swapped. + // If the order changes, tweak this test as required. + assertEquivalent([[DIFF_DELETE, 'c'], [DIFF_INSERT, 'm'], [DIFF_EQUAL, 'a'], [DIFF_DELETE, 't'], [DIFF_INSERT, 'p']], dmp.diff_bisect_(a, b, Number.MAX_VALUE)); + + // Timeout. + assertEquivalent([[DIFF_DELETE, 'cat'], [DIFF_INSERT, 'map']], dmp.diff_bisect_(a, b, 0)); +} + +function testDiffMain() { + // Perform a trivial diff. + // Null case. + assertEquivalent([], dmp.diff_main('', '', false)); + + // Equality. + assertEquivalent([[DIFF_EQUAL, 'abc']], dmp.diff_main('abc', 'abc', false)); + + // Simple insertion. + assertEquivalent([[DIFF_EQUAL, 'ab'], [DIFF_INSERT, '123'], [DIFF_EQUAL, 'c']], dmp.diff_main('abc', 'ab123c', false)); + + // Simple deletion. + assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_DELETE, '123'], [DIFF_EQUAL, 'bc']], dmp.diff_main('a123bc', 'abc', false)); + + // Two insertions. + assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_INSERT, '123'], [DIFF_EQUAL, 'b'], [DIFF_INSERT, '456'], [DIFF_EQUAL, 'c']], dmp.diff_main('abc', 'a123b456c', false)); + + // Two deletions. + assertEquivalent([[DIFF_EQUAL, 'a'], [DIFF_DELETE, '123'], [DIFF_EQUAL, 'b'], [DIFF_DELETE, '456'], [DIFF_EQUAL, 'c']], dmp.diff_main('a123b456c', 'abc', false)); + + // Perform a real diff. + // Switch off the timeout. + dmp.Diff_Timeout = 0; + // Simple cases. + assertEquivalent([[DIFF_DELETE, 'a'], [DIFF_INSERT, 'b']], dmp.diff_main('a', 'b', false)); + + assertEquivalent([[DIFF_DELETE, 'Apple'], [DIFF_INSERT, 'Banana'], [DIFF_EQUAL, 's are a'], [DIFF_INSERT, 'lso'], [DIFF_EQUAL, ' fruit.']], dmp.diff_main('Apples are a fruit.', 'Bananas are also fruit.', false)); + + assertEquivalent([[DIFF_DELETE, 'a'], [DIFF_INSERT, '\u0680'], [DIFF_EQUAL, 'x'], [DIFF_DELETE, '\t'], [DIFF_INSERT, '\0']], dmp.diff_main('ax\t', '\u0680x\0', false)); + + // Overlaps. + assertEquivalent([[DIFF_DELETE, '1'], [DIFF_EQUAL, 'a'], [DIFF_DELETE, 'y'], [DIFF_EQUAL, 'b'], [DIFF_DELETE, '2'], [DIFF_INSERT, 'xab']], dmp.diff_main('1ayb2', 'abxab', false)); + + assertEquivalent([[DIFF_INSERT, 'xaxcx'], [DIFF_EQUAL, 'abc'], [DIFF_DELETE, 'y']], dmp.diff_main('abcy', 'xaxcxabc', false)); + + assertEquivalent([[DIFF_DELETE, 'ABCD'], [DIFF_EQUAL, 'a'], [DIFF_DELETE, '='], [DIFF_INSERT, '-'], [DIFF_EQUAL, 'bcd'], [DIFF_DELETE, '='], [DIFF_INSERT, '-'], [DIFF_EQUAL, 'efghijklmnopqrs'], [DIFF_DELETE, 'EFGHIJKLMNOefg']], dmp.diff_main('ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg', 'a-bcd-efghijklmnopqrs', false)); + + // Large equality. + assertEquivalent([[DIFF_INSERT, ' '], [DIFF_EQUAL, 'a'], [DIFF_INSERT, 'nd'], [DIFF_EQUAL, ' [[Pennsylvania]]'], [DIFF_DELETE, ' and [[New']], dmp.diff_main('a [[Pennsylvania]] and [[New', ' and [[Pennsylvania]]', false)); + + // Timeout. + dmp.Diff_Timeout = 0.1; // 100ms + var a = '`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n'; + var b = 'I am the very model of a modern major general,\nI\'ve information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n'; + // Increase the text lengths by 1024 times to ensure a timeout. + for (var x = 0; x < 10; x++) { + a = a + a; + b = b + b; + } + var startTime = (new Date()).getTime(); + dmp.diff_main(a, b); + var endTime = (new Date()).getTime(); + // Test that we took at least the timeout period. + assertTrue(dmp.Diff_Timeout * 1000 <= endTime - startTime); + // Test that we didn't take forever (be forgiving). + // Theoretically this test could fail very occasionally if the + // OS task swaps or locks up for a second at the wrong moment. + // **** + // TODO(fraser): For unknown reasons this is taking 500 ms on Google's + // internal test system. Whereas browsers take 140 ms. + //assertTrue(dmp.Diff_Timeout * 1000 * 2 > endTime - startTime); + // **** + dmp.Diff_Timeout = 0; + + // Test the linemode speedup. + // Must be long to pass the 100 char cutoff. + // Simple line-mode. + a = '1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n'; + b = 'abcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\n'; + assertEquivalent(dmp.diff_main(a, b, false), dmp.diff_main(a, b, true)); + + // Single line-mode. + a = '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'; + b = 'abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij'; + assertEquivalent(dmp.diff_main(a, b, false), dmp.diff_main(a, b, true)); + + // Overlap line-mode. + a = '1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n'; + b = 'abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n'; + var texts_linemode = diff_rebuildtexts(dmp.diff_main(a, b, true)); + var texts_textmode = diff_rebuildtexts(dmp.diff_main(a, b, false)); + assertEquivalent(texts_textmode, texts_linemode); + + // Test null inputs. + try { + dmp.diff_main(null, null); + assertEquals(Error, null); + } catch (e) { + // Exception expected. + } +} + + +// MATCH TEST FUNCTIONS + + +function testMatchAlphabet() { + // Initialise the bitmasks for Bitap. + // Unique. + assertEquivalent({'a':4, 'b':2, 'c':1}, dmp.match_alphabet_('abc')); + + // Duplicates. + assertEquivalent({'a':37, 'b':18, 'c':8}, dmp.match_alphabet_('abcaba')); +} + +function testMatchBitap() { + // Bitap algorithm. + dmp.Match_Distance = 100; + dmp.Match_Threshold = 0.5; + // Exact matches. + assertEquals(5, dmp.match_bitap_('abcdefghijk', 'fgh', 5)); + + assertEquals(5, dmp.match_bitap_('abcdefghijk', 'fgh', 0)); + + // Fuzzy matches. + assertEquals(4, dmp.match_bitap_('abcdefghijk', 'efxhi', 0)); + + assertEquals(2, dmp.match_bitap_('abcdefghijk', 'cdefxyhijk', 5)); + + assertEquals(-1, dmp.match_bitap_('abcdefghijk', 'bxy', 1)); + + // Overflow. + assertEquals(2, dmp.match_bitap_('123456789xx0', '3456789x0', 2)); + + // Threshold test. + dmp.Match_Threshold = 0.4; + assertEquals(4, dmp.match_bitap_('abcdefghijk', 'efxyhi', 1)); + + dmp.Match_Threshold = 0.3; + assertEquals(-1, dmp.match_bitap_('abcdefghijk', 'efxyhi', 1)); + + dmp.Match_Threshold = 0.0; + assertEquals(1, dmp.match_bitap_('abcdefghijk', 'bcdef', 1)); + dmp.Match_Threshold = 0.5; + + // Multiple select. + assertEquals(0, dmp.match_bitap_('abcdexyzabcde', 'abccde', 3)); + + assertEquals(8, dmp.match_bitap_('abcdexyzabcde', 'abccde', 5)); + + // Distance test. + dmp.Match_Distance = 10; // Strict location. + assertEquals(-1, dmp.match_bitap_('abcdefghijklmnopqrstuvwxyz', 'abcdefg', 24)); + + assertEquals(0, dmp.match_bitap_('abcdefghijklmnopqrstuvwxyz', 'abcdxxefg', 1)); + + dmp.Match_Distance = 1000; // Loose location. + assertEquals(0, dmp.match_bitap_('abcdefghijklmnopqrstuvwxyz', 'abcdefg', 24)); +} + +function testMatchMain() { + // Full match. + // Shortcut matches. + assertEquals(0, dmp.match_main('abcdef', 'abcdef', 1000)); + + assertEquals(-1, dmp.match_main('', 'abcdef', 1)); + + assertEquals(3, dmp.match_main('abcdef', '', 3)); + + assertEquals(3, dmp.match_main('abcdef', 'de', 3)); + + // Beyond end match. + assertEquals(3, dmp.match_main("abcdef", "defy", 4)); + + // Oversized pattern. + assertEquals(0, dmp.match_main("abcdef", "abcdefy", 0)); + + // Complex match. + assertEquals(4, dmp.match_main('I am the very model of a modern major general.', ' that berry ', 5)); + + // Test null inputs. + try { + dmp.match_main(null, null, 0); + assertEquals(Error, null); + } catch (e) { + // Exception expected. + } +} + + +// PATCH TEST FUNCTIONS + + +function testPatchObj() { + // Patch Object. + var p = new diff_match_patch.patch_obj(); + p.start1 = 20; + p.start2 = 21; + p.length1 = 18; + p.length2 = 17; + p.diffs = [[DIFF_EQUAL, 'jump'], [DIFF_DELETE, 's'], [DIFF_INSERT, 'ed'], [DIFF_EQUAL, ' over '], [DIFF_DELETE, 'the'], [DIFF_INSERT, 'a'], [DIFF_EQUAL, '\nlaz']]; + var strp = p.toString(); + assertEquals('@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n', strp); +} + +function testPatchFromText() { + assertEquivalent([], dmp.patch_fromText(strp)); + + var strp = '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n'; + assertEquals(strp, dmp.patch_fromText(strp)[0].toString()); + + assertEquals('@@ -1 +1 @@\n-a\n+b\n', dmp.patch_fromText('@@ -1 +1 @@\n-a\n+b\n')[0].toString()); + + assertEquals('@@ -1,3 +0,0 @@\n-abc\n', dmp.patch_fromText('@@ -1,3 +0,0 @@\n-abc\n')[0].toString()); + + assertEquals('@@ -0,0 +1,3 @@\n+abc\n', dmp.patch_fromText('@@ -0,0 +1,3 @@\n+abc\n')[0].toString()); + + // Generates error. + try { + dmp.patch_fromText('Bad\nPatch\n'); + assertEquals(Error, null); + } catch (e) { + // Exception expected. + } +} + +function testPatchToText() { + var strp = '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n'; + var p = dmp.patch_fromText(strp); + assertEquals(strp, dmp.patch_toText(p)); + + strp = '@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n'; + p = dmp.patch_fromText(strp); + assertEquals(strp, dmp.patch_toText(p)); +} + +function testPatchAddContext() { + dmp.Patch_Margin = 4; + var p = dmp.patch_fromText('@@ -21,4 +21,10 @@\n-jump\n+somersault\n')[0]; + dmp.patch_addContext_(p, 'The quick brown fox jumps over the lazy dog.'); + assertEquals('@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n', p.toString()); + + // Same, but not enough trailing context. + p = dmp.patch_fromText('@@ -21,4 +21,10 @@\n-jump\n+somersault\n')[0]; + dmp.patch_addContext_(p, 'The quick brown fox jumps.'); + assertEquals('@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n', p.toString()); + + // Same, but not enough leading context. + p = dmp.patch_fromText('@@ -3 +3,2 @@\n-e\n+at\n')[0]; + dmp.patch_addContext_(p, 'The quick brown fox jumps.'); + assertEquals('@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n', p.toString()); + + // Same, but with ambiguity. + p = dmp.patch_fromText('@@ -3 +3,2 @@\n-e\n+at\n')[0]; + dmp.patch_addContext_(p, 'The quick brown fox jumps. The quick brown fox crashes.'); + assertEquals('@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n', p.toString()); +} + +function testPatchMake() { + // Null case. + var patches = dmp.patch_make('', ''); + assertEquals('', dmp.patch_toText(patches)); + + var text1 = 'The quick brown fox jumps over the lazy dog.'; + var text2 = 'That quick brown fox jumped over a lazy dog.'; + // Text2+Text1 inputs. + var expectedPatch = '@@ -1,8 +1,7 @@\n Th\n-at\n+e\n qui\n@@ -21,17 +21,18 @@\n jump\n-ed\n+s\n over \n-a\n+the\n laz\n'; + // The second patch must be "-21,17 +21,18", not "-22,17 +21,18" due to rolling context. + patches = dmp.patch_make(text2, text1); + assertEquals(expectedPatch, dmp.patch_toText(patches)); + + // Text1+Text2 inputs. + expectedPatch = '@@ -1,11 +1,12 @@\n Th\n-e\n+at\n quick b\n@@ -22,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n'; + patches = dmp.patch_make(text1, text2); + assertEquals(expectedPatch, dmp.patch_toText(patches)); + + // Diff input. + var diffs = dmp.diff_main(text1, text2, false); + patches = dmp.patch_make(diffs); + assertEquals(expectedPatch, dmp.patch_toText(patches)); + + // Text1+Diff inputs. + patches = dmp.patch_make(text1, diffs); + assertEquals(expectedPatch, dmp.patch_toText(patches)); + + // Text1+Text2+Diff inputs (deprecated). + patches = dmp.patch_make(text1, text2, diffs); + assertEquals(expectedPatch, dmp.patch_toText(patches)); + + // Character encoding. + patches = dmp.patch_make('`1234567890-=[]\\;\',./', '~!@#$%^&*()_+{}|:"<>?'); + assertEquals('@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;\',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n', dmp.patch_toText(patches)); + + // Character decoding. + diffs = [[DIFF_DELETE, '`1234567890-=[]\\;\',./'], [DIFF_INSERT, '~!@#$%^&*()_+{}|:"<>?']]; + assertEquivalent(diffs, dmp.patch_fromText('@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;\',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n')[0].diffs); + + // Long string with repeats. + text1 = ''; + for (var x = 0; x < 100; x++) { + text1 += 'abcdef'; + } + text2 = text1 + '123'; + expectedPatch = '@@ -573,28 +573,31 @@\n cdefabcdefabcdefabcdefabcdef\n+123\n'; + patches = dmp.patch_make(text1, text2); + assertEquals(expectedPatch, dmp.patch_toText(patches)); + + // Test null inputs. + try { + dmp.patch_make(null); + assertEquals(Error, null); + } catch (e) { + // Exception expected. + } +} + +function testPatchSplitMax() { + // Assumes that dmp.Match_MaxBits is 32. + var patches = dmp.patch_make('abcdefghijklmnopqrstuvwxyz01234567890', 'XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0'); + dmp.patch_splitMax(patches); + assertEquals('@@ -1,32 +1,46 @@\n+X\n ab\n+X\n cd\n+X\n ef\n+X\n gh\n+X\n ij\n+X\n kl\n+X\n mn\n+X\n op\n+X\n qr\n+X\n st\n+X\n uv\n+X\n wx\n+X\n yz\n+X\n 012345\n@@ -25,13 +39,18 @@\n zX01\n+X\n 23\n+X\n 45\n+X\n 67\n+X\n 89\n+X\n 0\n', dmp.patch_toText(patches)); + + patches = dmp.patch_make('abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz', 'abcdefuvwxyz'); + var oldToText = dmp.patch_toText(patches); + dmp.patch_splitMax(patches); + assertEquals(oldToText, dmp.patch_toText(patches)); + + patches = dmp.patch_make('1234567890123456789012345678901234567890123456789012345678901234567890', 'abc'); + dmp.patch_splitMax(patches); + assertEquals('@@ -1,32 +1,4 @@\n-1234567890123456789012345678\n 9012\n@@ -29,32 +1,4 @@\n-9012345678901234567890123456\n 7890\n@@ -57,14 +1,3 @@\n-78901234567890\n+abc\n', dmp.patch_toText(patches)); + + patches = dmp.patch_make('abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1', 'abcdefghij , h : 1 , t : 1 abcdefghij , h : 1 , t : 1 abcdefghij , h : 0 , t : 1'); + dmp.patch_splitMax(patches); + assertEquals('@@ -2,32 +2,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n@@ -29,32 +29,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n', dmp.patch_toText(patches)); +} + +function testPatchAddPadding() { + // Both edges full. + var patches = dmp.patch_make('', 'test'); + assertEquals('@@ -0,0 +1,4 @@\n+test\n', dmp.patch_toText(patches)); + dmp.patch_addPadding(patches); + assertEquals('@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n', dmp.patch_toText(patches)); + + // Both edges partial. + patches = dmp.patch_make('XY', 'XtestY'); + assertEquals('@@ -1,2 +1,6 @@\n X\n+test\n Y\n', dmp.patch_toText(patches)); + dmp.patch_addPadding(patches); + assertEquals('@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n', dmp.patch_toText(patches)); + + // Both edges none. + patches = dmp.patch_make('XXXXYYYY', 'XXXXtestYYYY'); + assertEquals('@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n', dmp.patch_toText(patches)); + dmp.patch_addPadding(patches); + assertEquals('@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n', dmp.patch_toText(patches)); +} + +function testPatchApply() { + dmp.Match_Distance = 1000; + dmp.Match_Threshold = 0.5; + dmp.Patch_DeleteThreshold = 0.5; + // Null case. + var patches = dmp.patch_make('', ''); + var results = dmp.patch_apply(patches, 'Hello world.'); + assertEquivalent(['Hello world.', []], results); + + // Exact match. + patches = dmp.patch_make('The quick brown fox jumps over the lazy dog.', 'That quick brown fox jumped over a lazy dog.'); + results = dmp.patch_apply(patches, 'The quick brown fox jumps over the lazy dog.'); + assertEquivalent(['That quick brown fox jumped over a lazy dog.', [true, true]], results); + + // Partial match. + results = dmp.patch_apply(patches, 'The quick red rabbit jumps over the tired tiger.'); + assertEquivalent(['That quick red rabbit jumped over a tired tiger.', [true, true]], results); + + // Failed match. + results = dmp.patch_apply(patches, 'I am the very model of a modern major general.'); + assertEquivalent(['I am the very model of a modern major general.', [false, false]], results); + + // Big delete, small change. + patches = dmp.patch_make('x1234567890123456789012345678901234567890123456789012345678901234567890y', 'xabcy'); + results = dmp.patch_apply(patches, 'x123456789012345678901234567890-----++++++++++-----123456789012345678901234567890y'); + assertEquivalent(['xabcy', [true, true]], results); + + // Big delete, big change 1. + patches = dmp.patch_make('x1234567890123456789012345678901234567890123456789012345678901234567890y', 'xabcy'); + results = dmp.patch_apply(patches, 'x12345678901234567890---------------++++++++++---------------12345678901234567890y'); + assertEquivalent(['xabc12345678901234567890---------------++++++++++---------------12345678901234567890y', [false, true]], results); + + // Big delete, big change 2. + dmp.Patch_DeleteThreshold = 0.6; + patches = dmp.patch_make('x1234567890123456789012345678901234567890123456789012345678901234567890y', 'xabcy'); + results = dmp.patch_apply(patches, 'x12345678901234567890---------------++++++++++---------------12345678901234567890y'); + assertEquivalent(['xabcy', [true, true]], results); + dmp.Patch_DeleteThreshold = 0.5; + + // Compensate for failed patch. + dmp.Match_Threshold = 0.0; + dmp.Match_Distance = 0; + patches = dmp.patch_make('abcdefghijklmnopqrstuvwxyz--------------------1234567890', 'abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------1234567YYYYYYYYYY890'); + results = dmp.patch_apply(patches, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890'); + assertEquivalent(['ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890', [false, true]], results); + dmp.Match_Threshold = 0.5; + dmp.Match_Distance = 1000; + + // No side effects. + patches = dmp.patch_make('', 'test'); + var patchstr = dmp.patch_toText(patches); + dmp.patch_apply(patches, ''); + assertEquals(patchstr, dmp.patch_toText(patches)); + + // No side effects with major delete. + patches = dmp.patch_make('The quick brown fox jumps over the lazy dog.', 'Woof'); + patchstr = dmp.patch_toText(patches); + dmp.patch_apply(patches, 'The quick brown fox jumps over the lazy dog.'); + assertEquals(patchstr, dmp.patch_toText(patches)); + + // Edge exact match. + patches = dmp.patch_make('', 'test'); + results = dmp.patch_apply(patches, ''); + assertEquivalent(['test', [true]], results); + + // Near edge exact match. + patches = dmp.patch_make('XY', 'XtestY'); + results = dmp.patch_apply(patches, 'XY'); + assertEquivalent(['XtestY', [true]], results); + + // Edge partial match. + patches = dmp.patch_make('y', 'y123'); + results = dmp.patch_apply(patches, 'x'); + assertEquivalent(['x123', [true]], results); +} diff --git a/javascript/diff_match_patch_uncompressed.js b/javascript/diff_match_patch_uncompressed.js new file mode 100644 index 0000000..d56f2e7 --- /dev/null +++ b/javascript/diff_match_patch_uncompressed.js @@ -0,0 +1,2192 @@ +/** + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Computes the difference between two texts to create a patch. + * Applies the patch onto another text, allowing for errors. + * @author fraser@google.com (Neil Fraser) + */ + +/** + * Class containing the diff, match and patch methods. + * @constructor + */ +function diff_match_patch() { + + // Defaults. + // Redefine these in your program to override the defaults. + + // Number of seconds to map a diff before giving up (0 for infinity). + this.Diff_Timeout = 1.0; + // Cost of an empty edit operation in terms of edit characters. + this.Diff_EditCost = 4; + // At what point is no match declared (0.0 = perfection, 1.0 = very loose). + this.Match_Threshold = 0.5; + // How far to search for a match (0 = exact location, 1000+ = broad match). + // A match this many characters away from the expected location will add + // 1.0 to the score (0.0 is a perfect match). + this.Match_Distance = 1000; + // When deleting a large block of text (over ~64 characters), how close do + // the contents have to be to match the expected contents. (0.0 = perfection, + // 1.0 = very loose). Note that Match_Threshold controls how closely the + // end points of a delete need to match. + this.Patch_DeleteThreshold = 0.5; + // Chunk size for context length. + this.Patch_Margin = 4; + + // The number of bits in an int. + this.Match_MaxBits = 32; +} + + +// DIFF FUNCTIONS + + +/** + * The data structure representing a diff is an array of tuples: + * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] + * which means: delete 'Hello', add 'Goodbye' and keep ' world.' + */ +var DIFF_DELETE = -1; +var DIFF_INSERT = 1; +var DIFF_EQUAL = 0; + +/** @typedef {{0: number, 1: string}} */ +diff_match_patch.Diff; + + +/** + * Find the differences between two texts. Simplifies the problem by stripping + * any common prefix or suffix off the texts before diffing. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean=} opt_checklines Optional speedup flag. If present and false, + * then don't run a line-level diff first to identify the changed areas. + * Defaults to true, which does a faster, slightly less optimal diff. + * @param {number} opt_deadline Optional time when the diff should be complete + * by. Used internally for recursive calls. Users should set DiffTimeout + * instead. + * @return {!Array.} Array of diff tuples. + */ +diff_match_patch.prototype.diff_main = function(text1, text2, opt_checklines, + opt_deadline) { + // Set a deadline by which time the diff must be complete. + if (typeof opt_deadline == 'undefined') { + if (this.Diff_Timeout <= 0) { + opt_deadline = Number.MAX_VALUE; + } else { + opt_deadline = (new Date).getTime() + this.Diff_Timeout * 1000; + } + } + var deadline = opt_deadline; + + // Check for null inputs. + if (text1 == null || text2 == null) { + throw new Error('Null input. (diff_main)'); + } + + // Check for equality (speedup). + if (text1 == text2) { + if (text1) { + return [[DIFF_EQUAL, text1]]; + } + return []; + } + + if (typeof opt_checklines == 'undefined') { + opt_checklines = true; + } + var checklines = opt_checklines; + + // Trim off common prefix (speedup). + var commonlength = this.diff_commonPrefix(text1, text2); + var commonprefix = text1.substring(0, commonlength); + text1 = text1.substring(commonlength); + text2 = text2.substring(commonlength); + + // Trim off common suffix (speedup). + commonlength = this.diff_commonSuffix(text1, text2); + var commonsuffix = text1.substring(text1.length - commonlength); + text1 = text1.substring(0, text1.length - commonlength); + text2 = text2.substring(0, text2.length - commonlength); + + // Compute the diff on the middle block. + var diffs = this.diff_compute_(text1, text2, checklines, deadline); + + // Restore the prefix and suffix. + if (commonprefix) { + diffs.unshift([DIFF_EQUAL, commonprefix]); + } + if (commonsuffix) { + diffs.push([DIFF_EQUAL, commonsuffix]); + } + this.diff_cleanupMerge(diffs); + return diffs; +}; + + +/** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean} checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster, slightly less optimal diff. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_compute_ = function(text1, text2, checklines, + deadline) { + var diffs; + + if (!text1) { + // Just add some text (speedup). + return [[DIFF_INSERT, text2]]; + } + + if (!text2) { + // Just delete some text (speedup). + return [[DIFF_DELETE, text1]]; + } + + var longtext = text1.length > text2.length ? text1 : text2; + var shorttext = text1.length > text2.length ? text2 : text1; + var i = longtext.indexOf(shorttext); + if (i != -1) { + // Shorter text is inside the longer text (speedup). + diffs = [[DIFF_INSERT, longtext.substring(0, i)], + [DIFF_EQUAL, shorttext], + [DIFF_INSERT, longtext.substring(i + shorttext.length)]]; + // Swap insertions for deletions if diff is reversed. + if (text1.length > text2.length) { + diffs[0][0] = diffs[2][0] = DIFF_DELETE; + } + return diffs; + } + + if (shorttext.length == 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; + } + + // Check to see if the problem can be split in two. + var hm = this.diff_halfMatch_(text1, text2); + if (hm) { + // A half-match was found, sort out the return data. + var text1_a = hm[0]; + var text1_b = hm[1]; + var text2_a = hm[2]; + var text2_b = hm[3]; + var mid_common = hm[4]; + // Send both pairs off for separate processing. + var diffs_a = this.diff_main(text1_a, text2_a, checklines, deadline); + var diffs_b = this.diff_main(text1_b, text2_b, checklines, deadline); + // Merge the results. + return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b); + } + + if (checklines && text1.length > 100 && text2.length > 100) { + return this.diff_lineMode_(text1, text2, deadline); + } + + return this.diff_bisect_(text1, text2, deadline); +}; + + +/** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_lineMode_ = function(text1, text2, deadline) { + // Scan the text on a line-by-line basis first. + var a = this.diff_linesToChars_(text1, text2); + text1 = a.chars1; + text2 = a.chars2; + var linearray = a.lineArray; + + var diffs = this.diff_main(text1, text2, false, deadline); + + // Convert the diff back to original text. + this.diff_charsToLines_(diffs, linearray); + // Eliminate freak matches (e.g. blank lines) + this.diff_cleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.push([DIFF_EQUAL, '']); + var pointer = 0; + var count_delete = 0; + var count_insert = 0; + var text_delete = ''; + var text_insert = ''; + while (pointer < diffs.length) { + switch (diffs[pointer][0]) { + case DIFF_INSERT: + count_insert++; + text_insert += diffs[pointer][1]; + break; + case DIFF_DELETE: + count_delete++; + text_delete += diffs[pointer][1]; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + diffs.splice(pointer - count_delete - count_insert, + count_delete + count_insert); + pointer = pointer - count_delete - count_insert; + var a = this.diff_main(text_delete, text_insert, false, deadline); + for (var j = a.length - 1; j >= 0; j--) { + diffs.splice(pointer, 0, a[j]); + } + pointer = pointer + a.length; + } + count_insert = 0; + count_delete = 0; + text_delete = ''; + text_insert = ''; + break; + } + pointer++; + } + diffs.pop(); // Remove the dummy entry at the end. + + return diffs; +}; + + +/** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_bisect_ = function(text1, text2, deadline) { + // Cache the text lengths to prevent multiple calls. + var text1_length = text1.length; + var text2_length = text2.length; + var max_d = Math.ceil((text1_length + text2_length) / 2); + var v_offset = max_d; + var v_length = 2 * max_d; + var v1 = new Array(v_length); + var v2 = new Array(v_length); + // Setting all elements to -1 is faster in Chrome & Firefox than mixing + // integers and undefined. + for (var x = 0; x < v_length; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[v_offset + 1] = 0; + v2[v_offset + 1] = 0; + var delta = text1_length - text2_length; + // If the total number of characters is odd, then the front path will collide + // with the reverse path. + var front = (delta % 2 != 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + var k1start = 0; + var k1end = 0; + var k2start = 0; + var k2end = 0; + for (var d = 0; d < max_d; d++) { + // Bail out if deadline is reached. + if ((new Date()).getTime() > deadline) { + break; + } + + // Walk the front path one step. + for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + var k1_offset = v_offset + k1; + var x1; + if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { + x1 = v1[k1_offset + 1]; + } else { + x1 = v1[k1_offset - 1] + 1; + } + var y1 = x1 - k1; + while (x1 < text1_length && y1 < text2_length && + text1.charAt(x1) == text2.charAt(y1)) { + x1++; + y1++; + } + v1[k1_offset] = x1; + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + var k2_offset = v_offset + delta - k1; + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { + // Mirror x2 onto top-left coordinate system. + var x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return this.diff_bisectSplit_(text1, text2, x1, y1, deadline); + } + } + } + } + + // Walk the reverse path one step. + for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + var k2_offset = v_offset + k2; + var x2; + if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { + x2 = v2[k2_offset + 1]; + } else { + x2 = v2[k2_offset - 1] + 1; + } + var y2 = x2 - k2; + while (x2 < text1_length && y2 < text2_length && + text1.charAt(text1_length - x2 - 1) == + text2.charAt(text2_length - y2 - 1)) { + x2++; + y2++; + } + v2[k2_offset] = x2; + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + var k1_offset = v_offset + delta - k2; + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { + var x1 = v1[k1_offset]; + var y1 = v_offset + x1 - k1_offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2; + if (x1 >= x2) { + // Overlap detected. + return this.diff_bisectSplit_(text1, text2, x1, y1, deadline); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; +}; + + +/** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} x Index of split point in text1. + * @param {number} y Index of split point in text2. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_bisectSplit_ = function(text1, text2, x, y, + deadline) { + var text1a = text1.substring(0, x); + var text2a = text2.substring(0, y); + var text1b = text1.substring(x); + var text2b = text2.substring(y); + + // Compute both diffs serially. + var diffs = this.diff_main(text1a, text2a, false, deadline); + var diffsb = this.diff_main(text1b, text2b, false, deadline); + + return diffs.concat(diffsb); +}; + + +/** + * Split two texts into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {{chars1: string, chars2: string, lineArray: !Array.}} + * An object containing the encoded text1, the encoded text2 and + * the array of unique strings. + * The zeroth element of the array of unique strings is intentionally blank. + * @private + */ +diff_match_patch.prototype.diff_linesToChars_ = function(text1, text2) { + var lineArray = []; // e.g. lineArray[4] == 'Hello\n' + var lineHash = {}; // e.g. lineHash['Hello\n'] == 4 + + // '\x00' is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray[0] = ''; + + /** + * Split a text into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * Modifies linearray and linehash through being a closure. + * @param {string} text String to encode. + * @return {string} Encoded string. + * @private + */ + function diff_linesToCharsMunge_(text) { + var chars = ''; + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + var lineStart = 0; + var lineEnd = -1; + // Keeping our own length variable is faster than looking it up. + var lineArrayLength = lineArray.length; + while (lineEnd < text.length - 1) { + lineEnd = text.indexOf('\n', lineStart); + if (lineEnd == -1) { + lineEnd = text.length - 1; + } + var line = text.substring(lineStart, lineEnd + 1); + lineStart = lineEnd + 1; + + if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : + (lineHash[line] !== undefined)) { + chars += String.fromCharCode(lineHash[line]); + } else { + chars += String.fromCharCode(lineArrayLength); + lineHash[line] = lineArrayLength; + lineArray[lineArrayLength++] = line; + } + } + return chars; + } + + var chars1 = diff_linesToCharsMunge_(text1); + var chars2 = diff_linesToCharsMunge_(text2); + return {chars1: chars1, chars2: chars2, lineArray: lineArray}; +}; + + +/** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * @param {!Array.} diffs Array of diff tuples. + * @param {!Array.} lineArray Array of unique strings. + * @private + */ +diff_match_patch.prototype.diff_charsToLines_ = function(diffs, lineArray) { + for (var x = 0; x < diffs.length; x++) { + var chars = diffs[x][1]; + var text = []; + for (var y = 0; y < chars.length; y++) { + text[y] = lineArray[chars.charCodeAt(y)]; + } + diffs[x][1] = text.join(''); + } +}; + + +/** + * Determine the common prefix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the start of each + * string. + */ +diff_match_patch.prototype.diff_commonPrefix = function(text1, text2) { + // Quick check for common null cases. + if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) { + return 0; + } + // Binary search. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + var pointermin = 0; + var pointermax = Math.min(text1.length, text2.length); + var pointermid = pointermax; + var pointerstart = 0; + while (pointermin < pointermid) { + if (text1.substring(pointerstart, pointermid) == + text2.substring(pointerstart, pointermid)) { + pointermin = pointermid; + pointerstart = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); + } + return pointermid; +}; + + +/** + * Determine the common suffix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of each string. + */ +diff_match_patch.prototype.diff_commonSuffix = function(text1, text2) { + // Quick check for common null cases. + if (!text1 || !text2 || + text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) { + return 0; + } + // Binary search. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + var pointermin = 0; + var pointermax = Math.min(text1.length, text2.length); + var pointermid = pointermax; + var pointerend = 0; + while (pointermin < pointermid) { + if (text1.substring(text1.length - pointermid, text1.length - pointerend) == + text2.substring(text2.length - pointermid, text2.length - pointerend)) { + pointermin = pointermid; + pointerend = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); + } + return pointermid; +}; + + +/** + * Determine if the suffix of one string is the prefix of another. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of the first + * string and the start of the second string. + * @private + */ +diff_match_patch.prototype.diff_commonOverlap_ = function(text1, text2) { + // Cache the text lengths to prevent multiple calls. + var text1_length = text1.length; + var text2_length = text2.length; + // Eliminate the null case. + if (text1_length == 0 || text2_length == 0) { + return 0; + } + // Truncate the longer string. + if (text1_length > text2_length) { + text1 = text1.substring(text1_length - text2_length); + } else if (text1_length < text2_length) { + text2 = text2.substring(0, text1_length); + } + var text_length = Math.min(text1_length, text2_length); + // Quick check for the worst case. + if (text1 == text2) { + return text_length; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: http://neil.fraser.name/news/2010/11/04/ + var best = 0; + var length = 1; + while (true) { + var pattern = text1.substring(text_length - length); + var found = text2.indexOf(pattern); + if (found == -1) { + return best; + } + length += found; + if (found == 0 || text1.substring(text_length - length) == + text2.substring(0, length)) { + best = length; + length++; + } + } +}; + + +/** + * Do the two texts share a substring which is at least half the length of the + * longer text? + * This speedup can produce non-minimal diffs. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {Array.} Five element Array, containing the prefix of + * text1, the suffix of text1, the prefix of text2, the suffix of + * text2 and the common middle. Or null if there was no match. + * @private + */ +diff_match_patch.prototype.diff_halfMatch_ = function(text1, text2) { + if (this.Diff_Timeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return null; + } + var longtext = text1.length > text2.length ? text1 : text2; + var shorttext = text1.length > text2.length ? text2 : text1; + if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { + return null; // Pointless. + } + var dmp = this; // 'this' becomes 'window' in a closure. + + /** + * Does a substring of shorttext exist within longtext such that the substring + * is at least half the length of longtext? + * Closure, but does not reference any external variables. + * @param {string} longtext Longer string. + * @param {string} shorttext Shorter string. + * @param {number} i Start index of quarter length substring within longtext. + * @return {Array.} Five element Array, containing the prefix of + * longtext, the suffix of longtext, the prefix of shorttext, the suffix + * of shorttext and the common middle. Or null if there was no match. + * @private + */ + function diff_halfMatchI_(longtext, shorttext, i) { + // Start with a 1/4 length substring at position i as a seed. + var seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); + var j = -1; + var best_common = ''; + var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b; + while ((j = shorttext.indexOf(seed, j + 1)) != -1) { + var prefixLength = dmp.diff_commonPrefix(longtext.substring(i), + shorttext.substring(j)); + var suffixLength = dmp.diff_commonSuffix(longtext.substring(0, i), + shorttext.substring(0, j)); + if (best_common.length < suffixLength + prefixLength) { + best_common = shorttext.substring(j - suffixLength, j) + + shorttext.substring(j, j + prefixLength); + best_longtext_a = longtext.substring(0, i - suffixLength); + best_longtext_b = longtext.substring(i + prefixLength); + best_shorttext_a = shorttext.substring(0, j - suffixLength); + best_shorttext_b = shorttext.substring(j + prefixLength); + } + } + if (best_common.length * 2 >= longtext.length) { + return [best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common]; + } else { + return null; + } + } + + // First check if the second quarter is the seed for a half-match. + var hm1 = diff_halfMatchI_(longtext, shorttext, + Math.ceil(longtext.length / 4)); + // Check again based on the third quarter. + var hm2 = diff_halfMatchI_(longtext, shorttext, + Math.ceil(longtext.length / 2)); + var hm; + if (!hm1 && !hm2) { + return null; + } else if (!hm2) { + hm = hm1; + } else if (!hm1) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].length > hm2[4].length ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + var text1_a, text1_b, text2_a, text2_b; + if (text1.length > text2.length) { + text1_a = hm[0]; + text1_b = hm[1]; + text2_a = hm[2]; + text2_b = hm[3]; + } else { + text2_a = hm[0]; + text2_b = hm[1]; + text1_a = hm[2]; + text1_b = hm[3]; + } + var mid_common = hm[4]; + return [text1_a, text1_b, text2_a, text2_b, mid_common]; +}; + + +/** + * Reduce the number of edits by eliminating semantically trivial equalities. + * @param {!Array.} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupSemantic = function(diffs) { + var changes = false; + var equalities = []; // Stack of indices where equalities are found. + var equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + var lastequality = null; + // Always equal to diffs[equalities[equalitiesLength - 1]][1] + var pointer = 0; // Index of current position. + // Number of characters that changed prior to the equality. + var length_insertions1 = 0; + var length_deletions1 = 0; + // Number of characters that changed after the equality. + var length_insertions2 = 0; + var length_deletions2 = 0; + while (pointer < diffs.length) { + if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found. + equalities[equalitiesLength++] = pointer; + length_insertions1 = length_insertions2; + length_deletions1 = length_deletions2; + length_insertions2 = 0; + length_deletions2 = 0; + lastequality = diffs[pointer][1]; + } else { // An insertion or deletion. + if (diffs[pointer][0] == DIFF_INSERT) { + length_insertions2 += diffs[pointer][1].length; + } else { + length_deletions2 += diffs[pointer][1].length; + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastequality && (lastequality.length <= + Math.max(length_insertions1, length_deletions1)) && + (lastequality.length <= Math.max(length_insertions2, + length_deletions2))) { + // Duplicate record. + diffs.splice(equalities[equalitiesLength - 1], 0, + [DIFF_DELETE, lastequality]); + // Change second copy to insert. + diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; + // Throw away the equality we just deleted. + equalitiesLength--; + // Throw away the previous equality (it needs to be reevaluated). + equalitiesLength--; + pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; + length_insertions1 = 0; // Reset the counters. + length_deletions1 = 0; + length_insertions2 = 0; + length_deletions2 = 0; + lastequality = null; + changes = true; + } + } + pointer++; + } + + // Normalize the diff. + if (changes) { + this.diff_cleanupMerge(diffs); + } + this.diff_cleanupSemanticLossless(diffs); + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 1; + while (pointer < diffs.length) { + if (diffs[pointer - 1][0] == DIFF_DELETE && + diffs[pointer][0] == DIFF_INSERT) { + var deletion = diffs[pointer - 1][1]; + var insertion = diffs[pointer][1]; + var overlap_length1 = this.diff_commonOverlap_(deletion, insertion); + var overlap_length2 = this.diff_commonOverlap_(insertion, deletion); + if (overlap_length1 >= overlap_length2) { + if (overlap_length1 >= deletion.length / 2 || + overlap_length1 >= insertion.length / 2) { + // Overlap found. Insert an equality and trim the surrounding edits. + diffs.splice(pointer, 0, + [DIFF_EQUAL, insertion.substring(0, overlap_length1)]); + diffs[pointer - 1][1] = + deletion.substring(0, deletion.length - overlap_length1); + diffs[pointer + 1][1] = insertion.substring(overlap_length1); + pointer++; + } + } else { + if (overlap_length2 >= deletion.length / 2 || + overlap_length2 >= insertion.length / 2) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + diffs.splice(pointer, 0, + [DIFF_EQUAL, deletion.substring(0, overlap_length2)]); + diffs[pointer - 1][0] = DIFF_INSERT; + diffs[pointer - 1][1] = + insertion.substring(0, insertion.length - overlap_length2); + diffs[pointer + 1][0] = DIFF_DELETE; + diffs[pointer + 1][1] = + deletion.substring(overlap_length2); + pointer++; + } + } + pointer++; + } + pointer++; + } +}; + + +/** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The cat came. -> The cat came. + * @param {!Array.} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupSemanticLossless = function(diffs) { + /** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 6 (best) to 0 (worst). + * Closure, but does not reference any external variables. + * @param {string} one First string. + * @param {string} two Second string. + * @return {number} The score. + * @private + */ + function diff_cleanupSemanticScore_(one, two) { + if (!one || !two) { + // Edges are the best. + return 6; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + var char1 = one.charAt(one.length - 1); + var char2 = two.charAt(0); + var nonAlphaNumeric1 = char1.match(diff_match_patch.nonAlphaNumericRegex_); + var nonAlphaNumeric2 = char2.match(diff_match_patch.nonAlphaNumericRegex_); + var whitespace1 = nonAlphaNumeric1 && + char1.match(diff_match_patch.whitespaceRegex_); + var whitespace2 = nonAlphaNumeric2 && + char2.match(diff_match_patch.whitespaceRegex_); + var lineBreak1 = whitespace1 && + char1.match(diff_match_patch.linebreakRegex_); + var lineBreak2 = whitespace2 && + char2.match(diff_match_patch.linebreakRegex_); + var blankLine1 = lineBreak1 && + one.match(diff_match_patch.blanklineEndRegex_); + var blankLine2 = lineBreak2 && + two.match(diff_match_patch.blanklineStartRegex_); + + if (blankLine1 || blankLine2) { + // Five points for blank lines. + return 5; + } else if (lineBreak1 || lineBreak2) { + // Four points for line breaks. + return 4; + } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { + // Three points for end of sentences. + return 3; + } else if (whitespace1 || whitespace2) { + // Two points for whitespace. + return 2; + } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { + // One point for non-alphanumeric. + return 1; + } + return 0; + } + + var pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.length - 1) { + if (diffs[pointer - 1][0] == DIFF_EQUAL && + diffs[pointer + 1][0] == DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + var equality1 = diffs[pointer - 1][1]; + var edit = diffs[pointer][1]; + var equality2 = diffs[pointer + 1][1]; + + // First, shift the edit as far left as possible. + var commonOffset = this.diff_commonSuffix(equality1, edit); + if (commonOffset) { + var commonString = edit.substring(edit.length - commonOffset); + equality1 = equality1.substring(0, equality1.length - commonOffset); + edit = commonString + edit.substring(0, edit.length - commonOffset); + equality2 = commonString + equality2; + } + + // Second, step character by character right, looking for the best fit. + var bestEquality1 = equality1; + var bestEdit = edit; + var bestEquality2 = equality2; + var bestScore = diff_cleanupSemanticScore_(equality1, edit) + + diff_cleanupSemanticScore_(edit, equality2); + while (edit.charAt(0) === equality2.charAt(0)) { + equality1 += edit.charAt(0); + edit = edit.substring(1) + equality2.charAt(0); + equality2 = equality2.substring(1); + var score = diff_cleanupSemanticScore_(equality1, edit) + + diff_cleanupSemanticScore_(edit, equality2); + // The >= encourages trailing rather than leading whitespace on edits. + if (score >= bestScore) { + bestScore = score; + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + } + } + + if (diffs[pointer - 1][1] != bestEquality1) { + // We have an improvement, save it back to the diff. + if (bestEquality1) { + diffs[pointer - 1][1] = bestEquality1; + } else { + diffs.splice(pointer - 1, 1); + pointer--; + } + diffs[pointer][1] = bestEdit; + if (bestEquality2) { + diffs[pointer + 1][1] = bestEquality2; + } else { + diffs.splice(pointer + 1, 1); + pointer--; + } + } + } + pointer++; + } +}; + +// Define some regex patterns for matching boundaries. +diff_match_patch.nonAlphaNumericRegex_ = /[^a-zA-Z0-9]/; +diff_match_patch.whitespaceRegex_ = /\s/; +diff_match_patch.linebreakRegex_ = /[\r\n]/; +diff_match_patch.blanklineEndRegex_ = /\n\r?\n$/; +diff_match_patch.blanklineStartRegex_ = /^\r?\n\r?\n/; + +/** + * Reduce the number of edits by eliminating operationally trivial equalities. + * @param {!Array.} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupEfficiency = function(diffs) { + var changes = false; + var equalities = []; // Stack of indices where equalities are found. + var equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + var lastequality = null; + // Always equal to diffs[equalities[equalitiesLength - 1]][1] + var pointer = 0; // Index of current position. + // Is there an insertion operation before the last equality. + var pre_ins = false; + // Is there a deletion operation before the last equality. + var pre_del = false; + // Is there an insertion operation after the last equality. + var post_ins = false; + // Is there a deletion operation after the last equality. + var post_del = false; + while (pointer < diffs.length) { + if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found. + if (diffs[pointer][1].length < this.Diff_EditCost && + (post_ins || post_del)) { + // Candidate found. + equalities[equalitiesLength++] = pointer; + pre_ins = post_ins; + pre_del = post_del; + lastequality = diffs[pointer][1]; + } else { + // Not a candidate, and can never become one. + equalitiesLength = 0; + lastequality = null; + } + post_ins = post_del = false; + } else { // An insertion or deletion. + if (diffs[pointer][0] == DIFF_DELETE) { + post_del = true; + } else { + post_ins = true; + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if (lastequality && ((pre_ins && pre_del && post_ins && post_del) || + ((lastequality.length < this.Diff_EditCost / 2) && + (pre_ins + pre_del + post_ins + post_del) == 3))) { + // Duplicate record. + diffs.splice(equalities[equalitiesLength - 1], 0, + [DIFF_DELETE, lastequality]); + // Change second copy to insert. + diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; + equalitiesLength--; // Throw away the equality we just deleted; + lastequality = null; + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_ins = post_del = true; + equalitiesLength = 0; + } else { + equalitiesLength--; // Throw away the previous equality. + pointer = equalitiesLength > 0 ? + equalities[equalitiesLength - 1] : -1; + post_ins = post_del = false; + } + changes = true; + } + } + pointer++; + } + + if (changes) { + this.diff_cleanupMerge(diffs); + } +}; + + +/** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param {!Array.} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupMerge = function(diffs) { + diffs.push([DIFF_EQUAL, '']); // Add a dummy entry at the end. + var pointer = 0; + var count_delete = 0; + var count_insert = 0; + var text_delete = ''; + var text_insert = ''; + var commonlength; + while (pointer < diffs.length) { + switch (diffs[pointer][0]) { + case DIFF_INSERT: + count_insert++; + text_insert += diffs[pointer][1]; + pointer++; + break; + case DIFF_DELETE: + count_delete++; + text_delete += diffs[pointer][1]; + pointer++; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete + count_insert > 1) { + if (count_delete !== 0 && count_insert !== 0) { + // Factor out any common prefixies. + commonlength = this.diff_commonPrefix(text_insert, text_delete); + if (commonlength !== 0) { + if ((pointer - count_delete - count_insert) > 0 && + diffs[pointer - count_delete - count_insert - 1][0] == + DIFF_EQUAL) { + diffs[pointer - count_delete - count_insert - 1][1] += + text_insert.substring(0, commonlength); + } else { + diffs.splice(0, 0, [DIFF_EQUAL, + text_insert.substring(0, commonlength)]); + pointer++; + } + text_insert = text_insert.substring(commonlength); + text_delete = text_delete.substring(commonlength); + } + // Factor out any common suffixies. + commonlength = this.diff_commonSuffix(text_insert, text_delete); + if (commonlength !== 0) { + diffs[pointer][1] = text_insert.substring(text_insert.length - + commonlength) + diffs[pointer][1]; + text_insert = text_insert.substring(0, text_insert.length - + commonlength); + text_delete = text_delete.substring(0, text_delete.length - + commonlength); + } + } + // Delete the offending records and add the merged ones. + if (count_delete === 0) { + diffs.splice(pointer - count_insert, + count_delete + count_insert, [DIFF_INSERT, text_insert]); + } else if (count_insert === 0) { + diffs.splice(pointer - count_delete, + count_delete + count_insert, [DIFF_DELETE, text_delete]); + } else { + diffs.splice(pointer - count_delete - count_insert, + count_delete + count_insert, [DIFF_DELETE, text_delete], + [DIFF_INSERT, text_insert]); + } + pointer = pointer - count_delete - count_insert + + (count_delete ? 1 : 0) + (count_insert ? 1 : 0) + 1; + } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) { + // Merge this equality with the previous one. + diffs[pointer - 1][1] += diffs[pointer][1]; + diffs.splice(pointer, 1); + } else { + pointer++; + } + count_insert = 0; + count_delete = 0; + text_delete = ''; + text_insert = ''; + break; + } + } + if (diffs[diffs.length - 1][1] === '') { + diffs.pop(); // Remove the dummy entry at the end. + } + + // Second pass: look for single edits surrounded on both sides by equalities + // which can be shifted sideways to eliminate an equality. + // e.g: ABAC -> ABAC + var changes = false; + pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.length - 1) { + if (diffs[pointer - 1][0] == DIFF_EQUAL && + diffs[pointer + 1][0] == DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + if (diffs[pointer][1].substring(diffs[pointer][1].length - + diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) { + // Shift the edit over the previous equality. + diffs[pointer][1] = diffs[pointer - 1][1] + + diffs[pointer][1].substring(0, diffs[pointer][1].length - + diffs[pointer - 1][1].length); + diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; + diffs.splice(pointer - 1, 1); + changes = true; + } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) == + diffs[pointer + 1][1]) { + // Shift the edit over the next equality. + diffs[pointer - 1][1] += diffs[pointer + 1][1]; + diffs[pointer][1] = + diffs[pointer][1].substring(diffs[pointer + 1][1].length) + + diffs[pointer + 1][1]; + diffs.splice(pointer + 1, 1); + changes = true; + } + } + pointer++; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + this.diff_cleanupMerge(diffs); + } +}; + + +/** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. 'The cat' vs 'The big cat', 1->1, 5->8 + * @param {!Array.} diffs Array of diff tuples. + * @param {number} loc Location within text1. + * @return {number} Location within text2. + */ +diff_match_patch.prototype.diff_xIndex = function(diffs, loc) { + var chars1 = 0; + var chars2 = 0; + var last_chars1 = 0; + var last_chars2 = 0; + var x; + for (x = 0; x < diffs.length; x++) { + if (diffs[x][0] !== DIFF_INSERT) { // Equality or deletion. + chars1 += diffs[x][1].length; + } + if (diffs[x][0] !== DIFF_DELETE) { // Equality or insertion. + chars2 += diffs[x][1].length; + } + if (chars1 > loc) { // Overshot the location. + break; + } + last_chars1 = chars1; + last_chars2 = chars2; + } + // Was the location was deleted? + if (diffs.length != x && diffs[x][0] === DIFF_DELETE) { + return last_chars2; + } + // Add the remaining character length. + return last_chars2 + (loc - last_chars1); +}; + + +/** + * Convert a diff array into a pretty HTML report. + * @param {!Array.} diffs Array of diff tuples. + * @return {string} HTML representation. + */ +diff_match_patch.prototype.diff_prettyHtml = function(diffs) { + var html = []; + var pattern_amp = /&/g; + var pattern_lt = //g; + var pattern_para = /\n/g; + for (var x = 0; x < diffs.length; x++) { + var op = diffs[x][0]; // Operation (insert, delete, equal) + var data = diffs[x][1]; // Text of change. + var text = data.replace(pattern_amp, '&').replace(pattern_lt, '<') + .replace(pattern_gt, '>').replace(pattern_para, '¶
    '); + switch (op) { + case DIFF_INSERT: + html[x] = '' + text + ''; + break; + case DIFF_DELETE: + html[x] = '' + text + ''; + break; + case DIFF_EQUAL: + html[x] = '' + text + ''; + break; + } + } + return html.join(''); +}; + + +/** + * Compute and return the source text (all equalities and deletions). + * @param {!Array.} diffs Array of diff tuples. + * @return {string} Source text. + */ +diff_match_patch.prototype.diff_text1 = function(diffs) { + var text = []; + for (var x = 0; x < diffs.length; x++) { + if (diffs[x][0] !== DIFF_INSERT) { + text[x] = diffs[x][1]; + } + } + return text.join(''); +}; + + +/** + * Compute and return the destination text (all equalities and insertions). + * @param {!Array.} diffs Array of diff tuples. + * @return {string} Destination text. + */ +diff_match_patch.prototype.diff_text2 = function(diffs) { + var text = []; + for (var x = 0; x < diffs.length; x++) { + if (diffs[x][0] !== DIFF_DELETE) { + text[x] = diffs[x][1]; + } + } + return text.join(''); +}; + + +/** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * @param {!Array.} diffs Array of diff tuples. + * @return {number} Number of changes. + */ +diff_match_patch.prototype.diff_levenshtein = function(diffs) { + var levenshtein = 0; + var insertions = 0; + var deletions = 0; + for (var x = 0; x < diffs.length; x++) { + var op = diffs[x][0]; + var data = diffs[x][1]; + switch (op) { + case DIFF_INSERT: + insertions += data.length; + break; + case DIFF_DELETE: + deletions += data.length; + break; + case DIFF_EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += Math.max(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += Math.max(insertions, deletions); + return levenshtein; +}; + + +/** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx notation. + * @param {!Array.} diffs Array of diff tuples. + * @return {string} Delta text. + */ +diff_match_patch.prototype.diff_toDelta = function(diffs) { + var text = []; + for (var x = 0; x < diffs.length; x++) { + switch (diffs[x][0]) { + case DIFF_INSERT: + text[x] = '+' + encodeURI(diffs[x][1]); + break; + case DIFF_DELETE: + text[x] = '-' + diffs[x][1].length; + break; + case DIFF_EQUAL: + text[x] = '=' + diffs[x][1].length; + break; + } + } + return text.join('\t').replace(/%20/g, ' '); +}; + + +/** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, compute the full diff. + * @param {string} text1 Source string for the diff. + * @param {string} delta Delta text. + * @return {!Array.} Array of diff tuples. + * @throws {!Error} If invalid input. + */ +diff_match_patch.prototype.diff_fromDelta = function(text1, delta) { + var diffs = []; + var diffsLength = 0; // Keeping our own length var is faster in JS. + var pointer = 0; // Cursor in text1 + var tokens = delta.split(/\t/g); + for (var x = 0; x < tokens.length; x++) { + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + var param = tokens[x].substring(1); + switch (tokens[x].charAt(0)) { + case '+': + try { + diffs[diffsLength++] = [DIFF_INSERT, decodeURI(param)]; + } catch (ex) { + // Malformed URI sequence. + throw new Error('Illegal escape in diff_fromDelta: ' + param); + } + break; + case '-': + // Fall through. + case '=': + var n = parseInt(param, 10); + if (isNaN(n) || n < 0) { + throw new Error('Invalid number in diff_fromDelta: ' + param); + } + var text = text1.substring(pointer, pointer += n); + if (tokens[x].charAt(0) == '=') { + diffs[diffsLength++] = [DIFF_EQUAL, text]; + } else { + diffs[diffsLength++] = [DIFF_DELETE, text]; + } + break; + default: + // Blank tokens are ok (from a trailing \t). + // Anything else is an error. + if (tokens[x]) { + throw new Error('Invalid diff operation in diff_fromDelta: ' + + tokens[x]); + } + } + } + if (pointer != text1.length) { + throw new Error('Delta length (' + pointer + + ') does not equal source text length (' + text1.length + ').'); + } + return diffs; +}; + + +// MATCH FUNCTIONS + + +/** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * @param {string} text The text to search. + * @param {string} pattern The pattern to search for. + * @param {number} loc The location to search around. + * @return {number} Best match index or -1. + */ +diff_match_patch.prototype.match_main = function(text, pattern, loc) { + // Check for null inputs. + if (text == null || pattern == null || loc == null) { + throw new Error('Null input. (match_main)'); + } + + loc = Math.max(0, Math.min(loc, text.length)); + if (text == pattern) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (!text.length) { + // Nothing to match. + return -1; + } else if (text.substring(loc, loc + pattern.length) == pattern) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return loc; + } else { + // Do a fuzzy compare. + return this.match_bitap_(text, pattern, loc); + } +}; + + +/** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. + * @param {string} text The text to search. + * @param {string} pattern The pattern to search for. + * @param {number} loc The location to search around. + * @return {number} Best match index or -1. + * @private + */ +diff_match_patch.prototype.match_bitap_ = function(text, pattern, loc) { + if (pattern.length > this.Match_MaxBits) { + throw new Error('Pattern too long for this browser.'); + } + + // Initialise the alphabet. + var s = this.match_alphabet_(pattern); + + var dmp = this; // 'this' becomes 'window' in a closure. + + /** + * Compute and return the score for a match with e errors and x location. + * Accesses loc and pattern through being a closure. + * @param {number} e Number of errors in match. + * @param {number} x Location of match. + * @return {number} Overall score for match (0.0 = good, 1.0 = bad). + * @private + */ + function match_bitapScore_(e, x) { + var accuracy = e / pattern.length; + var proximity = Math.abs(loc - x); + if (!dmp.Match_Distance) { + // Dodge divide by zero error. + return proximity ? 1.0 : accuracy; + } + return accuracy + (proximity / dmp.Match_Distance); + } + + // Highest score beyond which we give up. + var score_threshold = this.Match_Threshold; + // Is there a nearby exact match? (speedup) + var best_loc = text.indexOf(pattern, loc); + if (best_loc != -1) { + score_threshold = Math.min(match_bitapScore_(0, best_loc), score_threshold); + // What about in the other direction? (speedup) + best_loc = text.lastIndexOf(pattern, loc + pattern.length); + if (best_loc != -1) { + score_threshold = + Math.min(match_bitapScore_(0, best_loc), score_threshold); + } + } + + // Initialise the bit arrays. + var matchmask = 1 << (pattern.length - 1); + best_loc = -1; + + var bin_min, bin_mid; + var bin_max = pattern.length + text.length; + var last_rd; + for (var d = 0; d < pattern.length; d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at this + // error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (match_bitapScore_(d, loc + bin_mid) <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = Math.floor((bin_max - bin_min) / 2 + bin_min); + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + var start = Math.max(1, loc - bin_mid + 1); + var finish = Math.min(loc + bin_mid, text.length) + pattern.length; + + var rd = Array(finish + 2); + rd[finish + 1] = (1 << d) - 1; + for (var j = finish; j >= start; j--) { + // The alphabet (s) is a sparse hash, so the following line generates + // warnings. + var charMatch = s[text.charAt(j - 1)]; + if (d === 0) { // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { // Subsequent passes: fuzzy match. + rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) | + (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | + last_rd[j + 1]; + } + if (rd[j] & matchmask) { + var score = match_bitapScore_(d, j - 1); + // This match will almost certainly be better than any existing match. + // But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = Math.max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + // No hope for a (better) match at greater error levels. + if (match_bitapScore_(d + 1, loc) > score_threshold) { + break; + } + last_rd = rd; + } + return best_loc; +}; + + +/** + * Initialise the alphabet for the Bitap algorithm. + * @param {string} pattern The text to encode. + * @return {!Object} Hash of character locations. + * @private + */ +diff_match_patch.prototype.match_alphabet_ = function(pattern) { + var s = {}; + for (var i = 0; i < pattern.length; i++) { + s[pattern.charAt(i)] = 0; + } + for (var i = 0; i < pattern.length; i++) { + s[pattern.charAt(i)] |= 1 << (pattern.length - i - 1); + } + return s; +}; + + +// PATCH FUNCTIONS + + +/** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * @param {!diff_match_patch.patch_obj} patch The patch to grow. + * @param {string} text Source text. + * @private + */ +diff_match_patch.prototype.patch_addContext_ = function(patch, text) { + if (text.length == 0) { + return; + } + var pattern = text.substring(patch.start2, patch.start2 + patch.length1); + var padding = 0; + + // Look for the first and last matches of pattern in text. If two different + // matches are found, increase the pattern length. + while (text.indexOf(pattern) != text.lastIndexOf(pattern) && + pattern.length < this.Match_MaxBits - this.Patch_Margin - + this.Patch_Margin) { + padding += this.Patch_Margin; + pattern = text.substring(patch.start2 - padding, + patch.start2 + patch.length1 + padding); + } + // Add one chunk for good luck. + padding += this.Patch_Margin; + + // Add the prefix. + var prefix = text.substring(patch.start2 - padding, patch.start2); + if (prefix) { + patch.diffs.unshift([DIFF_EQUAL, prefix]); + } + // Add the suffix. + var suffix = text.substring(patch.start2 + patch.length1, + patch.start2 + patch.length1 + padding); + if (suffix) { + patch.diffs.push([DIFF_EQUAL, suffix]); + } + + // Roll back the start points. + patch.start1 -= prefix.length; + patch.start2 -= prefix.length; + // Extend the lengths. + patch.length1 += prefix.length + suffix.length; + patch.length2 += prefix.length + suffix.length; +}; + + +/** + * Compute a list of patches to turn text1 into text2. + * Use diffs if provided, otherwise compute it ourselves. + * There are four ways to call this function, depending on what data is + * available to the caller: + * Method 1: + * a = text1, b = text2 + * Method 2: + * a = diffs + * Method 3 (optimal): + * a = text1, b = diffs + * Method 4 (deprecated, use method 3): + * a = text1, b = text2, c = diffs + * + * @param {string|!Array.} a text1 (methods 1,3,4) or + * Array of diff tuples for text1 to text2 (method 2). + * @param {string|!Array.} opt_b text2 (methods 1,4) or + * Array of diff tuples for text1 to text2 (method 3) or undefined (method 2). + * @param {string|!Array.} opt_c Array of diff tuples + * for text1 to text2 (method 4) or undefined (methods 1,2,3). + * @return {!Array.} Array of Patch objects. + */ +diff_match_patch.prototype.patch_make = function(a, opt_b, opt_c) { + var text1, diffs; + if (typeof a == 'string' && typeof opt_b == 'string' && + typeof opt_c == 'undefined') { + // Method 1: text1, text2 + // Compute diffs from text1 and text2. + text1 = /** @type {string} */(a); + diffs = this.diff_main(text1, /** @type {string} */(opt_b), true); + if (diffs.length > 2) { + this.diff_cleanupSemantic(diffs); + this.diff_cleanupEfficiency(diffs); + } + } else if (a && typeof a == 'object' && typeof opt_b == 'undefined' && + typeof opt_c == 'undefined') { + // Method 2: diffs + // Compute text1 from diffs. + diffs = /** @type {!Array.} */(a); + text1 = this.diff_text1(diffs); + } else if (typeof a == 'string' && opt_b && typeof opt_b == 'object' && + typeof opt_c == 'undefined') { + // Method 3: text1, diffs + text1 = /** @type {string} */(a); + diffs = /** @type {!Array.} */(opt_b); + } else if (typeof a == 'string' && typeof opt_b == 'string' && + opt_c && typeof opt_c == 'object') { + // Method 4: text1, text2, diffs + // text2 is not used. + text1 = /** @type {string} */(a); + diffs = /** @type {!Array.} */(opt_c); + } else { + throw new Error('Unknown call format to patch_make.'); + } + + if (diffs.length === 0) { + return []; // Get rid of the null case. + } + var patches = []; + var patch = new diff_match_patch.patch_obj(); + var patchDiffLength = 0; // Keeping our own length var is faster in JS. + var char_count1 = 0; // Number of characters into the text1 string. + var char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + var prepatch_text = text1; + var postpatch_text = text1; + for (var x = 0; x < diffs.length; x++) { + var diff_type = diffs[x][0]; + var diff_text = diffs[x][1]; + + if (!patchDiffLength && diff_type !== DIFF_EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (diff_type) { + case DIFF_INSERT: + patch.diffs[patchDiffLength++] = diffs[x]; + patch.length2 += diff_text.length; + postpatch_text = postpatch_text.substring(0, char_count2) + diff_text + + postpatch_text.substring(char_count2); + break; + case DIFF_DELETE: + patch.length1 += diff_text.length; + patch.diffs[patchDiffLength++] = diffs[x]; + postpatch_text = postpatch_text.substring(0, char_count2) + + postpatch_text.substring(char_count2 + + diff_text.length); + break; + case DIFF_EQUAL: + if (diff_text.length <= 2 * this.Patch_Margin && + patchDiffLength && diffs.length != x + 1) { + // Small equality inside a patch. + patch.diffs[patchDiffLength++] = diffs[x]; + patch.length1 += diff_text.length; + patch.length2 += diff_text.length; + } else if (diff_text.length >= 2 * this.Patch_Margin) { + // Time for a new patch. + if (patchDiffLength) { + this.patch_addContext_(patch, prepatch_text); + patches.push(patch); + patch = new diff_match_patch.patch_obj(); + patchDiffLength = 0; + // Unlike Unidiff, our patch lists have a rolling context. + // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (diff_type !== DIFF_INSERT) { + char_count1 += diff_text.length; + } + if (diff_type !== DIFF_DELETE) { + char_count2 += diff_text.length; + } + } + // Pick up the leftover patch if not empty. + if (patchDiffLength) { + this.patch_addContext_(patch, prepatch_text); + patches.push(patch); + } + + return patches; +}; + + +/** + * Given an array of patches, return another array that is identical. + * @param {!Array.} patches Array of Patch objects. + * @return {!Array.} Array of Patch objects. + */ +diff_match_patch.prototype.patch_deepCopy = function(patches) { + // Making deep copies is hard in JavaScript. + var patchesCopy = []; + for (var x = 0; x < patches.length; x++) { + var patch = patches[x]; + var patchCopy = new diff_match_patch.patch_obj(); + patchCopy.diffs = []; + for (var y = 0; y < patch.diffs.length; y++) { + patchCopy.diffs[y] = patch.diffs[y].slice(); + } + patchCopy.start1 = patch.start1; + patchCopy.start2 = patch.start2; + patchCopy.length1 = patch.length1; + patchCopy.length2 = patch.length2; + patchesCopy[x] = patchCopy; + } + return patchesCopy; +}; + + +/** + * Merge a set of patches onto the text. Return a patched text, as well + * as a list of true/false values indicating which patches were applied. + * @param {!Array.} patches Array of Patch objects. + * @param {string} text Old text. + * @return {!Array.>} Two element Array, containing the + * new text and an array of boolean values. + */ +diff_match_patch.prototype.patch_apply = function(patches, text) { + if (patches.length == 0) { + return [text, []]; + } + + // Deep copy the patches so that no changes are made to originals. + patches = this.patch_deepCopy(patches); + + var nullPadding = this.patch_addPadding(patches); + text = nullPadding + text + nullPadding; + + this.patch_splitMax(patches); + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + var delta = 0; + var results = []; + for (var x = 0; x < patches.length; x++) { + var expected_loc = patches[x].start2 + delta; + var text1 = this.diff_text1(patches[x].diffs); + var start_loc; + var end_loc = -1; + if (text1.length > this.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = this.match_main(text, text1.substring(0, this.Match_MaxBits), + expected_loc); + if (start_loc != -1) { + end_loc = this.match_main(text, + text1.substring(text1.length - this.Match_MaxBits), + expected_loc + text1.length - this.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = this.match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= patches[x].length2 - patches[x].length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + var text2; + if (end_loc == -1) { + text2 = text.substring(start_loc, start_loc + text1.length); + } else { + text2 = text.substring(start_loc, end_loc + this.Match_MaxBits); + } + if (text1 == text2) { + // Perfect match, just shove the replacement text in. + text = text.substring(0, start_loc) + + this.diff_text2(patches[x].diffs) + + text.substring(start_loc + text1.length); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + var diffs = this.diff_main(text1, text2, false); + if (text1.length > this.Match_MaxBits && + this.diff_levenshtein(diffs) / text1.length > + this.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + this.diff_cleanupSemanticLossless(diffs); + var index1 = 0; + var index2; + for (var y = 0; y < patches[x].diffs.length; y++) { + var mod = patches[x].diffs[y]; + if (mod[0] !== DIFF_EQUAL) { + index2 = this.diff_xIndex(diffs, index1); + } + if (mod[0] === DIFF_INSERT) { // Insertion + text = text.substring(0, start_loc + index2) + mod[1] + + text.substring(start_loc + index2); + } else if (mod[0] === DIFF_DELETE) { // Deletion + text = text.substring(0, start_loc + index2) + + text.substring(start_loc + this.diff_xIndex(diffs, + index1 + mod[1].length)); + } + if (mod[0] !== DIFF_DELETE) { + index1 += mod[1].length; + } + } + } + } + } + } + // Strip the padding off. + text = text.substring(nullPadding.length, text.length - nullPadding.length); + return [text, results]; +}; + + +/** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * @param {!Array.} patches Array of Patch objects. + * @return {string} The padding string added to each side. + */ +diff_match_patch.prototype.patch_addPadding = function(patches) { + var paddingLength = this.Patch_Margin; + var nullPadding = ''; + for (var x = 1; x <= paddingLength; x++) { + nullPadding += String.fromCharCode(x); + } + + // Bump all the patches forward. + for (var x = 0; x < patches.length; x++) { + patches[x].start1 += paddingLength; + patches[x].start2 += paddingLength; + } + + // Add some padding on start of first diff. + var patch = patches[0]; + var diffs = patch.diffs; + if (diffs.length == 0 || diffs[0][0] != DIFF_EQUAL) { + // Add nullPadding equality. + diffs.unshift([DIFF_EQUAL, nullPadding]); + patch.start1 -= paddingLength; // Should be 0. + patch.start2 -= paddingLength; // Should be 0. + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs[0][1].length) { + // Grow first equality. + var extraLength = paddingLength - diffs[0][1].length; + diffs[0][1] = nullPadding.substring(diffs[0][1].length) + diffs[0][1]; + patch.start1 -= extraLength; + patch.start2 -= extraLength; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + // Add some padding on end of last diff. + patch = patches[patches.length - 1]; + diffs = patch.diffs; + if (diffs.length == 0 || diffs[diffs.length - 1][0] != DIFF_EQUAL) { + // Add nullPadding equality. + diffs.push([DIFF_EQUAL, nullPadding]); + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs[diffs.length - 1][1].length) { + // Grow last equality. + var extraLength = paddingLength - diffs[diffs.length - 1][1].length; + diffs[diffs.length - 1][1] += nullPadding.substring(0, extraLength); + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + return nullPadding; +}; + + +/** + * Look through the patches and break up any which are longer than the maximum + * limit of the match algorithm. + * Intended to be called only from within patch_apply. + * @param {!Array.} patches Array of Patch objects. + */ +diff_match_patch.prototype.patch_splitMax = function(patches) { + var patch_size = this.Match_MaxBits; + for (var x = 0; x < patches.length; x++) { + if (patches[x].length1 <= patch_size) { + continue; + } + var bigpatch = patches[x]; + // Remove the big old patch. + patches.splice(x--, 1); + var start1 = bigpatch.start1; + var start2 = bigpatch.start2; + var precontext = ''; + while (bigpatch.diffs.length !== 0) { + // Create one of several smaller patches. + var patch = new diff_match_patch.patch_obj(); + var empty = true; + patch.start1 = start1 - precontext.length; + patch.start2 = start2 - precontext.length; + if (precontext !== '') { + patch.length1 = patch.length2 = precontext.length; + patch.diffs.push([DIFF_EQUAL, precontext]); + } + while (bigpatch.diffs.length !== 0 && + patch.length1 < patch_size - this.Patch_Margin) { + var diff_type = bigpatch.diffs[0][0]; + var diff_text = bigpatch.diffs[0][1]; + if (diff_type === DIFF_INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.length; + start2 += diff_text.length; + patch.diffs.push(bigpatch.diffs.shift()); + empty = false; + } else if (diff_type === DIFF_DELETE && patch.diffs.length == 1 && + patch.diffs[0][0] == DIFF_EQUAL && + diff_text.length > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.length; + start1 += diff_text.length; + empty = false; + patch.diffs.push([diff_type, diff_text]); + bigpatch.diffs.shift(); + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text.substring(0, + patch_size - patch.length1 - this.Patch_Margin); + patch.length1 += diff_text.length; + start1 += diff_text.length; + if (diff_type === DIFF_EQUAL) { + patch.length2 += diff_text.length; + start2 += diff_text.length; + } else { + empty = false; + } + patch.diffs.push([diff_type, diff_text]); + if (diff_text == bigpatch.diffs[0][1]) { + bigpatch.diffs.shift(); + } else { + bigpatch.diffs[0][1] = + bigpatch.diffs[0][1].substring(diff_text.length); + } + } + } + // Compute the head context for the next patch. + precontext = this.diff_text2(patch.diffs); + precontext = + precontext.substring(precontext.length - this.Patch_Margin); + // Append the end context for this patch. + var postcontext = this.diff_text1(bigpatch.diffs) + .substring(0, this.Patch_Margin); + if (postcontext !== '') { + patch.length1 += postcontext.length; + patch.length2 += postcontext.length; + if (patch.diffs.length !== 0 && + patch.diffs[patch.diffs.length - 1][0] === DIFF_EQUAL) { + patch.diffs[patch.diffs.length - 1][1] += postcontext; + } else { + patch.diffs.push([DIFF_EQUAL, postcontext]); + } + } + if (!empty) { + patches.splice(++x, 0, patch); + } + } + } +}; + + +/** + * Take a list of patches and return a textual representation. + * @param {!Array.} patches Array of Patch objects. + * @return {string} Text representation of patches. + */ +diff_match_patch.prototype.patch_toText = function(patches) { + var text = []; + for (var x = 0; x < patches.length; x++) { + text[x] = patches[x]; + } + return text.join(''); +}; + + +/** + * Parse a textual representation of patches and return a list of Patch objects. + * @param {string} textline Text representation of patches. + * @return {!Array.} Array of Patch objects. + * @throws {!Error} If invalid input. + */ +diff_match_patch.prototype.patch_fromText = function(textline) { + var patches = []; + if (!textline) { + return patches; + } + var text = textline.split('\n'); + var textPointer = 0; + var patchHeader = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/; + while (textPointer < text.length) { + var m = text[textPointer].match(patchHeader); + if (!m) { + throw new Error('Invalid patch string: ' + text[textPointer]); + } + var patch = new diff_match_patch.patch_obj(); + patches.push(patch); + patch.start1 = parseInt(m[1], 10); + if (m[2] === '') { + patch.start1--; + patch.length1 = 1; + } else if (m[2] == '0') { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = parseInt(m[2], 10); + } + + patch.start2 = parseInt(m[3], 10); + if (m[4] === '') { + patch.start2--; + patch.length2 = 1; + } else if (m[4] == '0') { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = parseInt(m[4], 10); + } + textPointer++; + + while (textPointer < text.length) { + var sign = text[textPointer].charAt(0); + try { + var line = decodeURI(text[textPointer].substring(1)); + } catch (ex) { + // Malformed URI sequence. + throw new Error('Illegal escape in patch_fromText: ' + line); + } + if (sign == '-') { + // Deletion. + patch.diffs.push([DIFF_DELETE, line]); + } else if (sign == '+') { + // Insertion. + patch.diffs.push([DIFF_INSERT, line]); + } else if (sign == ' ') { + // Minor equality. + patch.diffs.push([DIFF_EQUAL, line]); + } else if (sign == '@') { + // Start of next patch. + break; + } else if (sign === '') { + // Blank line? Whatever. + } else { + // WTF? + throw new Error('Invalid patch mode "' + sign + '" in: ' + line); + } + textPointer++; + } + } + return patches; +}; + + +/** + * Class representing one patch operation. + * @constructor + */ +diff_match_patch.patch_obj = function() { + /** @type {!Array.} */ + this.diffs = []; + /** @type {?number} */ + this.start1 = null; + /** @type {?number} */ + this.start2 = null; + /** @type {number} */ + this.length1 = 0; + /** @type {number} */ + this.length2 = 0; +}; + + +/** + * Emmulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indicies are printed as 1-based, not 0-based. + * @return {string} The GNU diff string. + */ +diff_match_patch.patch_obj.prototype.toString = function() { + var coords1, coords2; + if (this.length1 === 0) { + coords1 = this.start1 + ',0'; + } else if (this.length1 == 1) { + coords1 = this.start1 + 1; + } else { + coords1 = (this.start1 + 1) + ',' + this.length1; + } + if (this.length2 === 0) { + coords2 = this.start2 + ',0'; + } else if (this.length2 == 1) { + coords2 = this.start2 + 1; + } else { + coords2 = (this.start2 + 1) + ',' + this.length2; + } + var text = ['@@ -' + coords1 + ' +' + coords2 + ' @@\n']; + var op; + // Escape the body of the patch with %xx notation. + for (var x = 0; x < this.diffs.length; x++) { + switch (this.diffs[x][0]) { + case DIFF_INSERT: + op = '+'; + break; + case DIFF_DELETE: + op = '-'; + break; + case DIFF_EQUAL: + op = ' '; + break; + } + text[x + 1] = op + encodeURI(this.diffs[x][1]) + '\n'; + } + return text.join('').replace(/%20/g, ' '); +}; + + +// Export these global variables so that they survive Google's JS compiler. +// In a browser, 'this' will be 'window'. +// Users of node.js should 'require' the uncompressed version since Google's +// JS compiler may break the following exports for non-browser environments. +this['diff_match_patch'] = diff_match_patch; +this['DIFF_DELETE'] = DIFF_DELETE; +this['DIFF_INSERT'] = DIFF_INSERT; +this['DIFF_EQUAL'] = DIFF_EQUAL; diff --git a/lua/diff_match_patch.lua b/lua/diff_match_patch.lua new file mode 100644 index 0000000..ec56577 --- /dev/null +++ b/lua/diff_match_patch.lua @@ -0,0 +1,2196 @@ +--[[ +* Diff Match and Patch +* Copyright 2018 The diff-match-patch Authors. +* https://github.com/google/diff-match-patch +* +* Based on the JavaScript implementation by Neil Fraser. +* Ported to Lua by Duncan Cross. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +--]] + +--[[ +-- Lua 5.1 and earlier requires the external BitOp library. +-- This library is built-in from Lua 5.2 and later as 'bit32'. +require 'bit' -- +local band, bor, lshift + = bit.band, bit.bor, bit.lshift +--]] + +local band, bor, lshift + = bit32.band, bit32.bor, bit32.lshift +local type, setmetatable, ipairs, select + = type, setmetatable, ipairs, select +local unpack, tonumber, error + = unpack, tonumber, error +local strsub, strbyte, strchar, gmatch, gsub + = string.sub, string.byte, string.char, string.gmatch, string.gsub +local strmatch, strfind, strformat + = string.match, string.find, string.format +local tinsert, tremove, tconcat + = table.insert, table.remove, table.concat +local max, min, floor, ceil, abs + = math.max, math.min, math.floor, math.ceil, math.abs +local clock = os.clock + + +-- Utility functions. + +local percentEncode_pattern = '[^A-Za-z0-9%-=;\',./~!@#$%&*%(%)_%+ %?]' +local function percentEncode_replace(v) + return strformat('%%%02X', strbyte(v)) +end + +local function tsplice(t, idx, deletions, ...) + local insertions = select('#', ...) + for i = 1, deletions do + tremove(t, idx) + end + for i = insertions, 1, -1 do + -- do not remove parentheses around select + tinsert(t, idx, (select(i, ...))) + end +end + +local function strelement(str, i) + return strsub(str, i, i) +end + +local function indexOf(a, b, start) + if (#b == 0) then + return nil + end + return strfind(a, b, start, true) +end + +local htmlEncode_pattern = '[&<>\n]' +local htmlEncode_replace = { + ['&'] = '&', ['<'] = '<', ['>'] = '>', ['\n'] = '¶
    ' +} + +-- Public API Functions +-- (Exported at the end of the script) + +local diff_main, + diff_cleanupSemantic, + diff_cleanupEfficiency, + diff_levenshtein, + diff_prettyHtml + +local match_main + +local patch_make, + patch_toText, + patch_fromText, + patch_apply + +--[[ +* The data structure representing a diff is an array of tuples: +* {{DIFF_DELETE, 'Hello'}, {DIFF_INSERT, 'Goodbye'}, {DIFF_EQUAL, ' world.'}} +* which means: delete 'Hello', add 'Goodbye' and keep ' world.' +--]] +local DIFF_DELETE = -1 +local DIFF_INSERT = 1 +local DIFF_EQUAL = 0 + +-- Number of seconds to map a diff before giving up (0 for infinity). +local Diff_Timeout = 1.0 +-- Cost of an empty edit operation in terms of edit characters. +local Diff_EditCost = 4 +-- At what point is no match declared (0.0 = perfection, 1.0 = very loose). +local Match_Threshold = 0.5 +-- How far to search for a match (0 = exact location, 1000+ = broad match). +-- A match this many characters away from the expected location will add +-- 1.0 to the score (0.0 is a perfect match). +local Match_Distance = 1000 +-- When deleting a large block of text (over ~64 characters), how close do +-- the contents have to be to match the expected contents. (0.0 = perfection, +-- 1.0 = very loose). Note that Match_Threshold controls how closely the +-- end points of a delete need to match. +local Patch_DeleteThreshold = 0.5 +-- Chunk size for context length. +local Patch_Margin = 4 +-- The number of bits in an int. +local Match_MaxBits = 32 + +function settings(new) + if new then + Diff_Timeout = new.Diff_Timeout or Diff_Timeout + Diff_EditCost = new.Diff_EditCost or Diff_EditCost + Match_Threshold = new.Match_Threshold or Match_Threshold + Match_Distance = new.Match_Distance or Match_Distance + Patch_DeleteThreshold = new.Patch_DeleteThreshold or Patch_DeleteThreshold + Patch_Margin = new.Patch_Margin or Patch_Margin + Match_MaxBits = new.Match_MaxBits or Match_MaxBits + else + return { + Diff_Timeout = Diff_Timeout; + Diff_EditCost = Diff_EditCost; + Match_Threshold = Match_Threshold; + Match_Distance = Match_Distance; + Patch_DeleteThreshold = Patch_DeleteThreshold; + Patch_Margin = Patch_Margin; + Match_MaxBits = Match_MaxBits; + } + end +end + +-- --------------------------------------------------------------------------- +-- DIFF API +-- --------------------------------------------------------------------------- + +-- The private diff functions +local _diff_compute, + _diff_bisect, + _diff_halfMatchI, + _diff_halfMatch, + _diff_cleanupSemanticScore, + _diff_cleanupSemanticLossless, + _diff_cleanupMerge, + _diff_commonPrefix, + _diff_commonSuffix, + _diff_commonOverlap, + _diff_xIndex, + _diff_text1, + _diff_text2, + _diff_toDelta, + _diff_fromDelta + +--[[ +* Find the differences between two texts. Simplifies the problem by stripping +* any common prefix or suffix off the texts before diffing. +* @param {string} text1 Old string to be diffed. +* @param {string} text2 New string to be diffed. +* @param {boolean} opt_checklines Has no effect in Lua. +* @param {number} opt_deadline Optional time when the diff should be complete +* by. Used internally for recursive calls. Users should set DiffTimeout +* instead. +* @return {Array.>} Array of diff tuples. +--]] +function diff_main(text1, text2, opt_checklines, opt_deadline) + -- Set a deadline by which time the diff must be complete. + if opt_deadline == nil then + if Diff_Timeout <= 0 then + opt_deadline = 2 ^ 31 + else + opt_deadline = clock() + Diff_Timeout + end + end + local deadline = opt_deadline + + -- Check for null inputs. + if text1 == nil or text1 == nil then + error('Null inputs. (diff_main)') + end + + -- Check for equality (speedup). + if text1 == text2 then + if #text1 > 0 then + return {{DIFF_EQUAL, text1}} + end + return {} + end + + -- LUANOTE: Due to the lack of Unicode support, Lua is incapable of + -- implementing the line-mode speedup. + local checklines = false + + -- Trim off common prefix (speedup). + local commonlength = _diff_commonPrefix(text1, text2) + local commonprefix + if commonlength > 0 then + commonprefix = strsub(text1, 1, commonlength) + text1 = strsub(text1, commonlength + 1) + text2 = strsub(text2, commonlength + 1) + end + + -- Trim off common suffix (speedup). + commonlength = _diff_commonSuffix(text1, text2) + local commonsuffix + if commonlength > 0 then + commonsuffix = strsub(text1, -commonlength) + text1 = strsub(text1, 1, -commonlength - 1) + text2 = strsub(text2, 1, -commonlength - 1) + end + + -- Compute the diff on the middle block. + local diffs = _diff_compute(text1, text2, checklines, deadline) + + -- Restore the prefix and suffix. + if commonprefix then + tinsert(diffs, 1, {DIFF_EQUAL, commonprefix}) + end + if commonsuffix then + diffs[#diffs + 1] = {DIFF_EQUAL, commonsuffix} + end + + _diff_cleanupMerge(diffs) + return diffs +end + +--[[ +* Reduce the number of edits by eliminating semantically trivial equalities. +* @param {Array.>} diffs Array of diff tuples. +--]] +function diff_cleanupSemantic(diffs) + local changes = false + local equalities = {} -- Stack of indices where equalities are found. + local equalitiesLength = 0 -- Keeping our own length var is faster. + local lastequality = nil + -- Always equal to diffs[equalities[equalitiesLength]][2] + local pointer = 1 -- Index of current position. + -- Number of characters that changed prior to the equality. + local length_insertions1 = 0 + local length_deletions1 = 0 + -- Number of characters that changed after the equality. + local length_insertions2 = 0 + local length_deletions2 = 0 + + while diffs[pointer] do + if diffs[pointer][1] == DIFF_EQUAL then -- Equality found. + equalitiesLength = equalitiesLength + 1 + equalities[equalitiesLength] = pointer + length_insertions1 = length_insertions2 + length_deletions1 = length_deletions2 + length_insertions2 = 0 + length_deletions2 = 0 + lastequality = diffs[pointer][2] + else -- An insertion or deletion. + if diffs[pointer][1] == DIFF_INSERT then + length_insertions2 = length_insertions2 + #(diffs[pointer][2]) + else + length_deletions2 = length_deletions2 + #(diffs[pointer][2]) + end + -- Eliminate an equality that is smaller or equal to the edits on both + -- sides of it. + if lastequality + and (#lastequality <= max(length_insertions1, length_deletions1)) + and (#lastequality <= max(length_insertions2, length_deletions2)) then + -- Duplicate record. + tinsert(diffs, equalities[equalitiesLength], + {DIFF_DELETE, lastequality}) + -- Change second copy to insert. + diffs[equalities[equalitiesLength] + 1][1] = DIFF_INSERT + -- Throw away the equality we just deleted. + equalitiesLength = equalitiesLength - 1 + -- Throw away the previous equality (it needs to be reevaluated). + equalitiesLength = equalitiesLength - 1 + pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0 + length_insertions1, length_deletions1 = 0, 0 -- Reset the counters. + length_insertions2, length_deletions2 = 0, 0 + lastequality = nil + changes = true + end + end + pointer = pointer + 1 + end + + -- Normalize the diff. + if changes then + _diff_cleanupMerge(diffs) + end + _diff_cleanupSemanticLossless(diffs) + + -- Find any overlaps between deletions and insertions. + -- e.g: abcxxxxxxdef + -- -> abcxxxdef + -- e.g: xxxabcdefxxx + -- -> defxxxabc + -- Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 2 + while diffs[pointer] do + if (diffs[pointer - 1][1] == DIFF_DELETE and + diffs[pointer][1] == DIFF_INSERT) then + local deletion = diffs[pointer - 1][2] + local insertion = diffs[pointer][2] + local overlap_length1 = _diff_commonOverlap(deletion, insertion) + local overlap_length2 = _diff_commonOverlap(insertion, deletion) + if (overlap_length1 >= overlap_length2) then + if (overlap_length1 >= #deletion / 2 or + overlap_length1 >= #insertion / 2) then + -- Overlap found. Insert an equality and trim the surrounding edits. + tinsert(diffs, pointer, + {DIFF_EQUAL, strsub(insertion, 1, overlap_length1)}) + diffs[pointer - 1][2] = + strsub(deletion, 1, #deletion - overlap_length1) + diffs[pointer + 1][2] = strsub(insertion, overlap_length1 + 1) + pointer = pointer + 1 + end + else + if (overlap_length2 >= #deletion / 2 or + overlap_length2 >= #insertion / 2) then + -- Reverse overlap found. + -- Insert an equality and swap and trim the surrounding edits. + tinsert(diffs, pointer, + {DIFF_EQUAL, strsub(deletion, 1, overlap_length2)}) + diffs[pointer - 1] = {DIFF_INSERT, + strsub(insertion, 1, #insertion - overlap_length2)} + diffs[pointer + 1] = {DIFF_DELETE, + strsub(deletion, overlap_length2 + 1)} + pointer = pointer + 1 + end + end + pointer = pointer + 1 + end + pointer = pointer + 1 + end +end + +--[[ +* Reduce the number of edits by eliminating operationally trivial equalities. +* @param {Array.>} diffs Array of diff tuples. +--]] +function diff_cleanupEfficiency(diffs) + local changes = false + -- Stack of indices where equalities are found. + local equalities = {} + -- Keeping our own length var is faster. + local equalitiesLength = 0 + -- Always equal to diffs[equalities[equalitiesLength]][2] + local lastequality = nil + -- Index of current position. + local pointer = 1 + + -- The following four are really booleans but are stored as numbers because + -- they are used at one point like this: + -- + -- (pre_ins + pre_del + post_ins + post_del) == 3 + -- + -- ...i.e. checking that 3 of them are true and 1 of them is false. + + -- Is there an insertion operation before the last equality. + local pre_ins = 0 + -- Is there a deletion operation before the last equality. + local pre_del = 0 + -- Is there an insertion operation after the last equality. + local post_ins = 0 + -- Is there a deletion operation after the last equality. + local post_del = 0 + + while diffs[pointer] do + if diffs[pointer][1] == DIFF_EQUAL then -- Equality found. + local diffText = diffs[pointer][2] + if (#diffText < Diff_EditCost) and (post_ins == 1 or post_del == 1) then + -- Candidate found. + equalitiesLength = equalitiesLength + 1 + equalities[equalitiesLength] = pointer + pre_ins, pre_del = post_ins, post_del + lastequality = diffText + else + -- Not a candidate, and can never become one. + equalitiesLength = 0 + lastequality = nil + end + post_ins, post_del = 0, 0 + else -- An insertion or deletion. + if diffs[pointer][1] == DIFF_DELETE then + post_del = 1 + else + post_ins = 1 + end + --[[ + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + --]] + if lastequality and ( + (pre_ins+pre_del+post_ins+post_del == 4) + or + ( + (#lastequality < Diff_EditCost / 2) + and + (pre_ins+pre_del+post_ins+post_del == 3) + )) then + -- Duplicate record. + tinsert(diffs, equalities[equalitiesLength], + {DIFF_DELETE, lastequality}) + -- Change second copy to insert. + diffs[equalities[equalitiesLength] + 1][1] = DIFF_INSERT + -- Throw away the equality we just deleted. + equalitiesLength = equalitiesLength - 1 + lastequality = nil + if (pre_ins == 1) and (pre_del == 1) then + -- No changes made which could affect previous entry, keep going. + post_ins, post_del = 1, 1 + equalitiesLength = 0 + else + -- Throw away the previous equality. + equalitiesLength = equalitiesLength - 1 + pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0 + post_ins, post_del = 0, 0 + end + changes = true + end + end + pointer = pointer + 1 + end + + if changes then + _diff_cleanupMerge(diffs) + end +end + +--[[ +* Compute the Levenshtein distance; the number of inserted, deleted or +* substituted characters. +* @param {Array.>} diffs Array of diff tuples. +* @return {number} Number of changes. +--]] +function diff_levenshtein(diffs) + local levenshtein = 0 + local insertions, deletions = 0, 0 + for x, diff in ipairs(diffs) do + local op, data = diff[1], diff[2] + if (op == DIFF_INSERT) then + insertions = insertions + #data + elseif (op == DIFF_DELETE) then + deletions = deletions + #data + elseif (op == DIFF_EQUAL) then + -- A deletion and an insertion is one substitution. + levenshtein = levenshtein + max(insertions, deletions) + insertions = 0 + deletions = 0 + end + end + levenshtein = levenshtein + max(insertions, deletions) + return levenshtein +end + +--[[ +* Convert a diff array into a pretty HTML report. +* @param {Array.>} diffs Array of diff tuples. +* @return {string} HTML representation. +--]] +function diff_prettyHtml(diffs) + local html = {} + for x, diff in ipairs(diffs) do + local op = diff[1] -- Operation (insert, delete, equal) + local data = diff[2] -- Text of change. + local text = gsub(data, htmlEncode_pattern, htmlEncode_replace) + if op == DIFF_INSERT then + html[x] = '' .. text .. '' + elseif op == DIFF_DELETE then + html[x] = '' .. text .. '' + elseif op == DIFF_EQUAL then + html[x] = '' .. text .. '' + end + end + return tconcat(html) +end + +-- --------------------------------------------------------------------------- +-- UNOFFICIAL/PRIVATE DIFF FUNCTIONS +-- --------------------------------------------------------------------------- + +--[[ +* Find the differences between two texts. Assumes that the texts do not +* have any common prefix or suffix. +* @param {string} text1 Old string to be diffed. +* @param {string} text2 New string to be diffed. +* @param {boolean} checklines Has no effect in Lua. +* @param {number} deadline Time when the diff should be complete by. +* @return {Array.>} Array of diff tuples. +* @private +--]] +function _diff_compute(text1, text2, checklines, deadline) + if #text1 == 0 then + -- Just add some text (speedup). + return {{DIFF_INSERT, text2}} + end + + if #text2 == 0 then + -- Just delete some text (speedup). + return {{DIFF_DELETE, text1}} + end + + local diffs + + local longtext = (#text1 > #text2) and text1 or text2 + local shorttext = (#text1 > #text2) and text2 or text1 + local i = indexOf(longtext, shorttext) + + if i ~= nil then + -- Shorter text is inside the longer text (speedup). + diffs = { + {DIFF_INSERT, strsub(longtext, 1, i - 1)}, + {DIFF_EQUAL, shorttext}, + {DIFF_INSERT, strsub(longtext, i + #shorttext)} + } + -- Swap insertions for deletions if diff is reversed. + if #text1 > #text2 then + diffs[1][1], diffs[3][1] = DIFF_DELETE, DIFF_DELETE + end + return diffs + end + + if #shorttext == 1 then + -- Single character string. + -- After the previous speedup, the character can't be an equality. + return {{DIFF_DELETE, text1}, {DIFF_INSERT, text2}} + end + + -- Check to see if the problem can be split in two. + do + local + text1_a, text1_b, + text2_a, text2_b, + mid_common = _diff_halfMatch(text1, text2) + + if text1_a then + -- A half-match was found, sort out the return data. + -- Send both pairs off for separate processing. + local diffs_a = diff_main(text1_a, text2_a, checklines, deadline) + local diffs_b = diff_main(text1_b, text2_b, checklines, deadline) + -- Merge the results. + local diffs_a_len = #diffs_a + diffs = diffs_a + diffs[diffs_a_len + 1] = {DIFF_EQUAL, mid_common} + for i, b_diff in ipairs(diffs_b) do + diffs[diffs_a_len + 1 + i] = b_diff + end + return diffs + end + end + + return _diff_bisect(text1, text2, deadline) +end + +--[[ +* Find the 'middle snake' of a diff, split the problem in two +* and return the recursively constructed diff. +* See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. +* @param {string} text1 Old string to be diffed. +* @param {string} text2 New string to be diffed. +* @param {number} deadline Time at which to bail if not yet complete. +* @return {Array.>} Array of diff tuples. +* @private +--]] +function _diff_bisect(text1, text2, deadline) + -- Cache the text lengths to prevent multiple calls. + local text1_length = #text1 + local text2_length = #text2 + local _sub, _element + local max_d = ceil((text1_length + text2_length) / 2) + local v_offset = max_d + local v_length = 2 * max_d + local v1 = {} + local v2 = {} + -- Setting all elements to -1 is faster in Lua than mixing integers and nil. + for x = 0, v_length - 1 do + v1[x] = -1 + v2[x] = -1 + end + v1[v_offset + 1] = 0 + v2[v_offset + 1] = 0 + local delta = text1_length - text2_length + -- If the total number of characters is odd, then + -- the front path will collide with the reverse path. + local front = (delta % 2 ~= 0) + -- Offsets for start and end of k loop. + -- Prevents mapping of space beyond the grid. + local k1start = 0 + local k1end = 0 + local k2start = 0 + local k2end = 0 + for d = 0, max_d - 1 do + -- Bail out if deadline is reached. + if clock() > deadline then + break + end + + -- Walk the front path one step. + for k1 = -d + k1start, d - k1end, 2 do + local k1_offset = v_offset + k1 + local x1 + if (k1 == -d) or ((k1 ~= d) and + (v1[k1_offset - 1] < v1[k1_offset + 1])) then + x1 = v1[k1_offset + 1] + else + x1 = v1[k1_offset - 1] + 1 + end + local y1 = x1 - k1 + while (x1 <= text1_length) and (y1 <= text2_length) + and (strelement(text1, x1) == strelement(text2, y1)) do + x1 = x1 + 1 + y1 = y1 + 1 + end + v1[k1_offset] = x1 + if x1 > text1_length + 1 then + -- Ran off the right of the graph. + k1end = k1end + 2 + elseif y1 > text2_length + 1 then + -- Ran off the bottom of the graph. + k1start = k1start + 2 + elseif front then + local k2_offset = v_offset + delta - k1 + if k2_offset >= 0 and k2_offset < v_length and v2[k2_offset] ~= -1 then + -- Mirror x2 onto top-left coordinate system. + local x2 = text1_length - v2[k2_offset] + 1 + if x1 > x2 then + -- Overlap detected. + return _diff_bisectSplit(text1, text2, x1, y1, deadline) + end + end + end + end + + -- Walk the reverse path one step. + for k2 = -d + k2start, d - k2end, 2 do + local k2_offset = v_offset + k2 + local x2 + if (k2 == -d) or ((k2 ~= d) and + (v2[k2_offset - 1] < v2[k2_offset + 1])) then + x2 = v2[k2_offset + 1] + else + x2 = v2[k2_offset - 1] + 1 + end + local y2 = x2 - k2 + while (x2 <= text1_length) and (y2 <= text2_length) + and (strelement(text1, -x2) == strelement(text2, -y2)) do + x2 = x2 + 1 + y2 = y2 + 1 + end + v2[k2_offset] = x2 + if x2 > text1_length + 1 then + -- Ran off the left of the graph. + k2end = k2end + 2 + elseif y2 > text2_length + 1 then + -- Ran off the top of the graph. + k2start = k2start + 2 + elseif not front then + local k1_offset = v_offset + delta - k2 + if k1_offset >= 0 and k1_offset < v_length and v1[k1_offset] ~= -1 then + local x1 = v1[k1_offset] + local y1 = v_offset + x1 - k1_offset + -- Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2 + 1 + if x1 > x2 then + -- Overlap detected. + return _diff_bisectSplit(text1, text2, x1, y1, deadline) + end + end + end + end + end + -- Diff took too long and hit the deadline or + -- number of diffs equals number of characters, no commonality at all. + return {{DIFF_DELETE, text1}, {DIFF_INSERT, text2}} +end + +--[[ + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} x Index of split point in text1. + * @param {number} y Index of split point in text2. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {Array.>} Array of diff tuples. + * @private +--]] +function _diff_bisectSplit(text1, text2, x, y, deadline) + local text1a = strsub(text1, 1, x - 1) + local text2a = strsub(text2, 1, y - 1) + local text1b = strsub(text1, x) + local text2b = strsub(text2, y) + + -- Compute both diffs serially. + local diffs = diff_main(text1a, text2a, false, deadline) + local diffsb = diff_main(text1b, text2b, false, deadline) + + local diffs_len = #diffs + for i, v in ipairs(diffsb) do + diffs[diffs_len + i] = v + end + return diffs +end + +--[[ +* Determine the common prefix of two strings. +* @param {string} text1 First string. +* @param {string} text2 Second string. +* @return {number} The number of characters common to the start of each +* string. +--]] +function _diff_commonPrefix(text1, text2) + -- Quick check for common null cases. + if (#text1 == 0) or (#text2 == 0) or (strbyte(text1, 1) ~= strbyte(text2, 1)) + then + return 0 + end + -- Binary search. + -- Performance analysis: http://neil.fraser.name/news/2007/10/09/ + local pointermin = 1 + local pointermax = min(#text1, #text2) + local pointermid = pointermax + local pointerstart = 1 + while (pointermin < pointermid) do + if (strsub(text1, pointerstart, pointermid) + == strsub(text2, pointerstart, pointermid)) then + pointermin = pointermid + pointerstart = pointermin + else + pointermax = pointermid + end + pointermid = floor(pointermin + (pointermax - pointermin) / 2) + end + return pointermid +end + +--[[ +* Determine the common suffix of two strings. +* @param {string} text1 First string. +* @param {string} text2 Second string. +* @return {number} The number of characters common to the end of each string. +--]] +function _diff_commonSuffix(text1, text2) + -- Quick check for common null cases. + if (#text1 == 0) or (#text2 == 0) + or (strbyte(text1, -1) ~= strbyte(text2, -1)) then + return 0 + end + -- Binary search. + -- Performance analysis: http://neil.fraser.name/news/2007/10/09/ + local pointermin = 1 + local pointermax = min(#text1, #text2) + local pointermid = pointermax + local pointerend = 1 + while (pointermin < pointermid) do + if (strsub(text1, -pointermid, -pointerend) + == strsub(text2, -pointermid, -pointerend)) then + pointermin = pointermid + pointerend = pointermin + else + pointermax = pointermid + end + pointermid = floor(pointermin + (pointermax - pointermin) / 2) + end + return pointermid +end + +--[[ +* Determine if the suffix of one string is the prefix of another. +* @param {string} text1 First string. +* @param {string} text2 Second string. +* @return {number} The number of characters common to the end of the first +* string and the start of the second string. +* @private +--]] +function _diff_commonOverlap(text1, text2) + -- Cache the text lengths to prevent multiple calls. + local text1_length = #text1 + local text2_length = #text2 + -- Eliminate the null case. + if text1_length == 0 or text2_length == 0 then + return 0 + end + -- Truncate the longer string. + if text1_length > text2_length then + text1 = strsub(text1, text1_length - text2_length + 1) + elseif text1_length < text2_length then + text2 = strsub(text2, 1, text1_length) + end + local text_length = min(text1_length, text2_length) + -- Quick check for the worst case. + if text1 == text2 then + return text_length + end + + -- Start by looking for a single character match + -- and increase length until no match is found. + -- Performance analysis: http://neil.fraser.name/news/2010/11/04/ + local best = 0 + local length = 1 + while true do + local pattern = strsub(text1, text_length - length + 1) + local found = strfind(text2, pattern, 1, true) + if found == nil then + return best + end + length = length + found - 1 + if found == 1 or strsub(text1, text_length - length + 1) == + strsub(text2, 1, length) then + best = length + length = length + 1 + end + end +end + +--[[ +* Does a substring of shorttext exist within longtext such that the substring +* is at least half the length of longtext? +* This speedup can produce non-minimal diffs. +* Closure, but does not reference any external variables. +* @param {string} longtext Longer string. +* @param {string} shorttext Shorter string. +* @param {number} i Start index of quarter length substring within longtext. +* @return {?Array.} Five element Array, containing the prefix of +* longtext, the suffix of longtext, the prefix of shorttext, the suffix +* of shorttext and the common middle. Or nil if there was no match. +* @private +--]] +function _diff_halfMatchI(longtext, shorttext, i) + -- Start with a 1/4 length substring at position i as a seed. + local seed = strsub(longtext, i, i + floor(#longtext / 4)) + local j = 0 -- LUANOTE: do not change to 1, was originally -1 + local best_common = '' + local best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b + while true do + j = indexOf(shorttext, seed, j + 1) + if (j == nil) then + break + end + local prefixLength = _diff_commonPrefix(strsub(longtext, i), + strsub(shorttext, j)) + local suffixLength = _diff_commonSuffix(strsub(longtext, 1, i - 1), + strsub(shorttext, 1, j - 1)) + if #best_common < suffixLength + prefixLength then + best_common = strsub(shorttext, j - suffixLength, j - 1) + .. strsub(shorttext, j, j + prefixLength - 1) + best_longtext_a = strsub(longtext, 1, i - suffixLength - 1) + best_longtext_b = strsub(longtext, i + prefixLength) + best_shorttext_a = strsub(shorttext, 1, j - suffixLength - 1) + best_shorttext_b = strsub(shorttext, j + prefixLength) + end + end + if #best_common * 2 >= #longtext then + return {best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common} + else + return nil + end +end + +--[[ +* Do the two texts share a substring which is at least half the length of the +* longer text? +* @param {string} text1 First string. +* @param {string} text2 Second string. +* @return {?Array.} Five element Array, containing the prefix of +* text1, the suffix of text1, the prefix of text2, the suffix of +* text2 and the common middle. Or nil if there was no match. +* @private +--]] +function _diff_halfMatch(text1, text2) + if Diff_Timeout <= 0 then + -- Don't risk returning a non-optimal diff if we have unlimited time. + return nil + end + local longtext = (#text1 > #text2) and text1 or text2 + local shorttext = (#text1 > #text2) and text2 or text1 + if (#longtext < 4) or (#shorttext * 2 < #longtext) then + return nil -- Pointless. + end + + -- First check if the second quarter is the seed for a half-match. + local hm1 = _diff_halfMatchI(longtext, shorttext, ceil(#longtext / 4)) + -- Check again based on the third quarter. + local hm2 = _diff_halfMatchI(longtext, shorttext, ceil(#longtext / 2)) + local hm + if not hm1 and not hm2 then + return nil + elseif not hm2 then + hm = hm1 + elseif not hm1 then + hm = hm2 + else + -- Both matched. Select the longest. + hm = (#hm1[5] > #hm2[5]) and hm1 or hm2 + end + + -- A half-match was found, sort out the return data. + local text1_a, text1_b, text2_a, text2_b + if (#text1 > #text2) then + text1_a, text1_b = hm[1], hm[2] + text2_a, text2_b = hm[3], hm[4] + else + text2_a, text2_b = hm[1], hm[2] + text1_a, text1_b = hm[3], hm[4] + end + local mid_common = hm[5] + return text1_a, text1_b, text2_a, text2_b, mid_common +end + +--[[ +* Given two strings, compute a score representing whether the internal +* boundary falls on logical boundaries. +* Scores range from 6 (best) to 0 (worst). +* @param {string} one First string. +* @param {string} two Second string. +* @return {number} The score. +* @private +--]] +function _diff_cleanupSemanticScore(one, two) + if (#one == 0) or (#two == 0) then + -- Edges are the best. + return 6 + end + + -- Each port of this function behaves slightly differently due to + -- subtle differences in each language's definition of things like + -- 'whitespace'. Since this function's purpose is largely cosmetic, + -- the choice has been made to use each language's native features + -- rather than force total conformity. + local char1 = strsub(one, -1) + local char2 = strsub(two, 1, 1) + local nonAlphaNumeric1 = strmatch(char1, '%W') + local nonAlphaNumeric2 = strmatch(char2, '%W') + local whitespace1 = nonAlphaNumeric1 and strmatch(char1, '%s') + local whitespace2 = nonAlphaNumeric2 and strmatch(char2, '%s') + local lineBreak1 = whitespace1 and strmatch(char1, '%c') + local lineBreak2 = whitespace2 and strmatch(char2, '%c') + local blankLine1 = lineBreak1 and strmatch(one, '\n\r?\n$') + local blankLine2 = lineBreak2 and strmatch(two, '^\r?\n\r?\n') + + if blankLine1 or blankLine2 then + -- Five points for blank lines. + return 5 + elseif lineBreak1 or lineBreak2 then + -- Four points for line breaks. + return 4 + elseif nonAlphaNumeric1 and not whitespace1 and whitespace2 then + -- Three points for end of sentences. + return 3 + elseif whitespace1 or whitespace2 then + -- Two points for whitespace. + return 2 + elseif nonAlphaNumeric1 or nonAlphaNumeric2 then + -- One point for non-alphanumeric. + return 1 + end + return 0 +end + +--[[ +* Look for single edits surrounded on both sides by equalities +* which can be shifted sideways to align the edit to a word boundary. +* e.g: The cat came. -> The cat came. +* @param {Array.>} diffs Array of diff tuples. +--]] +function _diff_cleanupSemanticLossless(diffs) + local pointer = 2 + -- Intentionally ignore the first and last element (don't need checking). + while diffs[pointer + 1] do + local prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1] + if (prevDiff[1] == DIFF_EQUAL) and (nextDiff[1] == DIFF_EQUAL) then + -- This is a single edit surrounded by equalities. + local diff = diffs[pointer] + + local equality1 = prevDiff[2] + local edit = diff[2] + local equality2 = nextDiff[2] + + -- First, shift the edit as far left as possible. + local commonOffset = _diff_commonSuffix(equality1, edit) + if commonOffset > 0 then + local commonString = strsub(edit, -commonOffset) + equality1 = strsub(equality1, 1, -commonOffset - 1) + edit = commonString .. strsub(edit, 1, -commonOffset - 1) + equality2 = commonString .. equality2 + end + + -- Second, step character by character right, looking for the best fit. + local bestEquality1 = equality1 + local bestEdit = edit + local bestEquality2 = equality2 + local bestScore = _diff_cleanupSemanticScore(equality1, edit) + + _diff_cleanupSemanticScore(edit, equality2) + + while strbyte(edit, 1) == strbyte(equality2, 1) do + equality1 = equality1 .. strsub(edit, 1, 1) + edit = strsub(edit, 2) .. strsub(equality2, 1, 1) + equality2 = strsub(equality2, 2) + local score = _diff_cleanupSemanticScore(equality1, edit) + + _diff_cleanupSemanticScore(edit, equality2) + -- The >= encourages trailing rather than leading whitespace on edits. + if score >= bestScore then + bestScore = score + bestEquality1 = equality1 + bestEdit = edit + bestEquality2 = equality2 + end + end + if prevDiff[2] ~= bestEquality1 then + -- We have an improvement, save it back to the diff. + if #bestEquality1 > 0 then + diffs[pointer - 1][2] = bestEquality1 + else + tremove(diffs, pointer - 1) + pointer = pointer - 1 + end + diffs[pointer][2] = bestEdit + if #bestEquality2 > 0 then + diffs[pointer + 1][2] = bestEquality2 + else + tremove(diffs, pointer + 1, 1) + pointer = pointer - 1 + end + end + end + pointer = pointer + 1 + end +end + +--[[ +* Reorder and merge like edit sections. Merge equalities. +* Any edit section can move as long as it doesn't cross an equality. +* @param {Array.>} diffs Array of diff tuples. +--]] +function _diff_cleanupMerge(diffs) + diffs[#diffs + 1] = {DIFF_EQUAL, ''} -- Add a dummy entry at the end. + local pointer = 1 + local count_delete, count_insert = 0, 0 + local text_delete, text_insert = '', '' + local commonlength + while diffs[pointer] do + local diff_type = diffs[pointer][1] + if diff_type == DIFF_INSERT then + count_insert = count_insert + 1 + text_insert = text_insert .. diffs[pointer][2] + pointer = pointer + 1 + elseif diff_type == DIFF_DELETE then + count_delete = count_delete + 1 + text_delete = text_delete .. diffs[pointer][2] + pointer = pointer + 1 + elseif diff_type == DIFF_EQUAL then + -- Upon reaching an equality, check for prior redundancies. + if count_delete + count_insert > 1 then + if (count_delete > 0) and (count_insert > 0) then + -- Factor out any common prefixies. + commonlength = _diff_commonPrefix(text_insert, text_delete) + if commonlength > 0 then + local back_pointer = pointer - count_delete - count_insert + if (back_pointer > 1) and (diffs[back_pointer - 1][1] == DIFF_EQUAL) + then + diffs[back_pointer - 1][2] = diffs[back_pointer - 1][2] + .. strsub(text_insert, 1, commonlength) + else + tinsert(diffs, 1, + {DIFF_EQUAL, strsub(text_insert, 1, commonlength)}) + pointer = pointer + 1 + end + text_insert = strsub(text_insert, commonlength + 1) + text_delete = strsub(text_delete, commonlength + 1) + end + -- Factor out any common suffixies. + commonlength = _diff_commonSuffix(text_insert, text_delete) + if commonlength ~= 0 then + diffs[pointer][2] = + strsub(text_insert, -commonlength) .. diffs[pointer][2] + text_insert = strsub(text_insert, 1, -commonlength - 1) + text_delete = strsub(text_delete, 1, -commonlength - 1) + end + end + -- Delete the offending records and add the merged ones. + if count_delete == 0 then + tsplice(diffs, pointer - count_insert, + count_insert, {DIFF_INSERT, text_insert}) + elseif count_insert == 0 then + tsplice(diffs, pointer - count_delete, + count_delete, {DIFF_DELETE, text_delete}) + else + tsplice(diffs, pointer - count_delete - count_insert, + count_delete + count_insert, + {DIFF_DELETE, text_delete}, {DIFF_INSERT, text_insert}) + end + pointer = pointer - count_delete - count_insert + + (count_delete>0 and 1 or 0) + (count_insert>0 and 1 or 0) + 1 + elseif (pointer > 1) and (diffs[pointer - 1][1] == DIFF_EQUAL) then + -- Merge this equality with the previous one. + diffs[pointer - 1][2] = diffs[pointer - 1][2] .. diffs[pointer][2] + tremove(diffs, pointer) + else + pointer = pointer + 1 + end + count_insert, count_delete = 0, 0 + text_delete, text_insert = '', '' + end + end + if diffs[#diffs][2] == '' then + diffs[#diffs] = nil -- Remove the dummy entry at the end. + end + + -- Second pass: look for single edits surrounded on both sides by equalities + -- which can be shifted sideways to eliminate an equality. + -- e.g: ABAC -> ABAC + local changes = false + pointer = 2 + -- Intentionally ignore the first and last element (don't need checking). + while pointer < #diffs do + local prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1] + if (prevDiff[1] == DIFF_EQUAL) and (nextDiff[1] == DIFF_EQUAL) then + -- This is a single edit surrounded by equalities. + local diff = diffs[pointer] + local currentText = diff[2] + local prevText = prevDiff[2] + local nextText = nextDiff[2] + if strsub(currentText, -#prevText) == prevText then + -- Shift the edit over the previous equality. + diff[2] = prevText .. strsub(currentText, 1, -#prevText - 1) + nextDiff[2] = prevText .. nextDiff[2] + tremove(diffs, pointer - 1) + changes = true + elseif strsub(currentText, 1, #nextText) == nextText then + -- Shift the edit over the next equality. + prevDiff[2] = prevText .. nextText + diff[2] = strsub(currentText, #nextText + 1) .. nextText + tremove(diffs, pointer + 1) + changes = true + end + end + pointer = pointer + 1 + end + -- If shifts were made, the diff needs reordering and another shift sweep. + if changes then + -- LUANOTE: no return value, but necessary to use 'return' to get + -- tail calls. + return _diff_cleanupMerge(diffs) + end +end + +--[[ +* loc is a location in text1, compute and return the equivalent location in +* text2. +* e.g. 'The cat' vs 'The big cat', 1->1, 5->8 +* @param {Array.>} diffs Array of diff tuples. +* @param {number} loc Location within text1. +* @return {number} Location within text2. +--]] +function _diff_xIndex(diffs, loc) + local chars1 = 1 + local chars2 = 1 + local last_chars1 = 1 + local last_chars2 = 1 + local x + for _x, diff in ipairs(diffs) do + x = _x + if diff[1] ~= DIFF_INSERT then -- Equality or deletion. + chars1 = chars1 + #diff[2] + end + if diff[1] ~= DIFF_DELETE then -- Equality or insertion. + chars2 = chars2 + #diff[2] + end + if chars1 > loc then -- Overshot the location. + break + end + last_chars1 = chars1 + last_chars2 = chars2 + end + -- Was the location deleted? + if diffs[x + 1] and (diffs[x][1] == DIFF_DELETE) then + return last_chars2 + end + -- Add the remaining character length. + return last_chars2 + (loc - last_chars1) +end + +--[[ +* Compute and return the source text (all equalities and deletions). +* @param {Array.>} diffs Array of diff tuples. +* @return {string} Source text. +--]] +function _diff_text1(diffs) + local text = {} + for x, diff in ipairs(diffs) do + if diff[1] ~= DIFF_INSERT then + text[#text + 1] = diff[2] + end + end + return tconcat(text) +end + +--[[ +* Compute and return the destination text (all equalities and insertions). +* @param {Array.>} diffs Array of diff tuples. +* @return {string} Destination text. +--]] +function _diff_text2(diffs) + local text = {} + for x, diff in ipairs(diffs) do + if diff[1] ~= DIFF_DELETE then + text[#text + 1] = diff[2] + end + end + return tconcat(text) +end + +--[[ +* Crush the diff into an encoded string which describes the operations +* required to transform text1 into text2. +* E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. +* Operations are tab-separated. Inserted text is escaped using %xx notation. +* @param {Array.>} diffs Array of diff tuples. +* @return {string} Delta text. +--]] +function _diff_toDelta(diffs) + local text = {} + for x, diff in ipairs(diffs) do + local op, data = diff[1], diff[2] + if op == DIFF_INSERT then + text[x] = '+' .. gsub(data, percentEncode_pattern, percentEncode_replace) + elseif op == DIFF_DELETE then + text[x] = '-' .. #data + elseif op == DIFF_EQUAL then + text[x] = '=' .. #data + end + end + return tconcat(text, '\t') +end + +--[[ +* Given the original text1, and an encoded string which describes the +* operations required to transform text1 into text2, compute the full diff. +* @param {string} text1 Source string for the diff. +* @param {string} delta Delta text. +* @return {Array.>} Array of diff tuples. +* @throws {Errorend If invalid input. +--]] +function _diff_fromDelta(text1, delta) + local diffs = {} + local diffsLength = 0 -- Keeping our own length var is faster + local pointer = 1 -- Cursor in text1 + for token in gmatch(delta, '[^\t]+') do + -- Each token begins with a one character parameter which specifies the + -- operation of this token (delete, insert, equality). + local tokenchar, param = strsub(token, 1, 1), strsub(token, 2) + if (tokenchar == '+') then + local invalidDecode = false + local decoded = gsub(param, '%%(.?.?)', + function(c) + local n = tonumber(c, 16) + if (#c ~= 2) or (n == nil) then + invalidDecode = true + return '' + end + return strchar(n) + end) + if invalidDecode then + -- Malformed URI sequence. + error('Illegal escape in _diff_fromDelta: ' .. param) + end + diffsLength = diffsLength + 1 + diffs[diffsLength] = {DIFF_INSERT, decoded} + elseif (tokenchar == '-') or (tokenchar == '=') then + local n = tonumber(param) + if (n == nil) or (n < 0) then + error('Invalid number in _diff_fromDelta: ' .. param) + end + local text = strsub(text1, pointer, pointer + n - 1) + pointer = pointer + n + if (tokenchar == '=') then + diffsLength = diffsLength + 1 + diffs[diffsLength] = {DIFF_EQUAL, text} + else + diffsLength = diffsLength + 1 + diffs[diffsLength] = {DIFF_DELETE, text} + end + else + error('Invalid diff operation in _diff_fromDelta: ' .. token) + end + end + if (pointer ~= #text1 + 1) then + error('Delta length (' .. (pointer - 1) + .. ') does not equal source text length (' .. #text1 .. ').') + end + return diffs +end + +-- --------------------------------------------------------------------------- +-- MATCH API +-- --------------------------------------------------------------------------- + +local _match_bitap, _match_alphabet + +--[[ +* Locate the best instance of 'pattern' in 'text' near 'loc'. +* @param {string} text The text to search. +* @param {string} pattern The pattern to search for. +* @param {number} loc The location to search around. +* @return {number} Best match index or -1. +--]] +function match_main(text, pattern, loc) + -- Check for null inputs. + if text == nil or pattern == nil or loc == nil then + error('Null inputs. (match_main)') + end + + if text == pattern then + -- Shortcut (potentially not guaranteed by the algorithm) + return 1 + elseif #text == 0 then + -- Nothing to match. + return -1 + end + loc = max(1, min(loc, #text)) + if strsub(text, loc, loc + #pattern - 1) == pattern then + -- Perfect match at the perfect spot! (Includes case of null pattern) + return loc + else + -- Do a fuzzy compare. + return _match_bitap(text, pattern, loc) + end +end + +-- --------------------------------------------------------------------------- +-- UNOFFICIAL/PRIVATE MATCH FUNCTIONS +-- --------------------------------------------------------------------------- + +--[[ +* Initialise the alphabet for the Bitap algorithm. +* @param {string} pattern The text to encode. +* @return {Object} Hash of character locations. +* @private +--]] +function _match_alphabet(pattern) + local s = {} + local i = 0 + for c in gmatch(pattern, '.') do + s[c] = bor(s[c] or 0, lshift(1, #pattern - i - 1)) + i = i + 1 + end + return s +end + +--[[ +* Locate the best instance of 'pattern' in 'text' near 'loc' using the +* Bitap algorithm. +* @param {string} text The text to search. +* @param {string} pattern The pattern to search for. +* @param {number} loc The location to search around. +* @return {number} Best match index or -1. +* @private +--]] +function _match_bitap(text, pattern, loc) + if #pattern > Match_MaxBits then + error('Pattern too long.') + end + + -- Initialise the alphabet. + local s = _match_alphabet(pattern) + + --[[ + * Compute and return the score for a match with e errors and x location. + * Accesses loc and pattern through being a closure. + * @param {number} e Number of errors in match. + * @param {number} x Location of match. + * @return {number} Overall score for match (0.0 = good, 1.0 = bad). + * @private + --]] + local function _match_bitapScore(e, x) + local accuracy = e / #pattern + local proximity = abs(loc - x) + if (Match_Distance == 0) then + -- Dodge divide by zero error. + return (proximity == 0) and 1 or accuracy + end + return accuracy + (proximity / Match_Distance) + end + + -- Highest score beyond which we give up. + local score_threshold = Match_Threshold + -- Is there a nearby exact match? (speedup) + local best_loc = indexOf(text, pattern, loc) + if best_loc then + score_threshold = min(_match_bitapScore(0, best_loc), score_threshold) + -- LUANOTE: Ideally we'd also check from the other direction, but Lua + -- doesn't have an efficent lastIndexOf function. + end + + -- Initialise the bit arrays. + local matchmask = lshift(1, #pattern - 1) + best_loc = -1 + + local bin_min, bin_mid + local bin_max = #pattern + #text + local last_rd + for d = 0, #pattern - 1, 1 do + -- Scan for the best match; each iteration allows for one more error. + -- Run a binary search to determine how far from 'loc' we can stray at this + -- error level. + bin_min = 0 + bin_mid = bin_max + while (bin_min < bin_mid) do + if (_match_bitapScore(d, loc + bin_mid) <= score_threshold) then + bin_min = bin_mid + else + bin_max = bin_mid + end + bin_mid = floor(bin_min + (bin_max - bin_min) / 2) + end + -- Use the result from this iteration as the maximum for the next. + bin_max = bin_mid + local start = max(1, loc - bin_mid + 1) + local finish = min(loc + bin_mid, #text) + #pattern + + local rd = {} + for j = start, finish do + rd[j] = 0 + end + rd[finish + 1] = lshift(1, d) - 1 + for j = finish, start, -1 do + local charMatch = s[strsub(text, j - 1, j - 1)] or 0 + if (d == 0) then -- First pass: exact match. + rd[j] = band(bor((rd[j + 1] * 2), 1), charMatch) + else + -- Subsequent passes: fuzzy match. + -- Functions instead of operators make this hella messy. + rd[j] = bor( + band( + bor( + lshift(rd[j + 1], 1), + 1 + ), + charMatch + ), + bor( + bor( + lshift(bor(last_rd[j + 1], last_rd[j]), 1), + 1 + ), + last_rd[j + 1] + ) + ) + end + if (band(rd[j], matchmask) ~= 0) then + local score = _match_bitapScore(d, j - 1) + -- This match will almost certainly be better than any existing match. + -- But check anyway. + if (score <= score_threshold) then + -- Told you so. + score_threshold = score + best_loc = j - 1 + if (best_loc > loc) then + -- When passing loc, don't exceed our current distance from loc. + start = max(1, loc * 2 - best_loc) + else + -- Already passed loc, downhill from here on in. + break + end + end + end + end + -- No hope for a (better) match at greater error levels. + if (_match_bitapScore(d + 1, loc) > score_threshold) then + break + end + last_rd = rd + end + return best_loc +end + +-- ----------------------------------------------------------------------------- +-- PATCH API +-- ----------------------------------------------------------------------------- + +local _patch_addContext, + _patch_deepCopy, + _patch_addPadding, + _patch_splitMax, + _patch_appendText, + _new_patch_obj + +--[[ +* Compute a list of patches to turn text1 into text2. +* Use diffs if provided, otherwise compute it ourselves. +* There are four ways to call this function, depending on what data is +* available to the caller: +* Method 1: +* a = text1, b = text2 +* Method 2: +* a = diffs +* Method 3 (optimal): +* a = text1, b = diffs +* Method 4 (deprecated, use method 3): +* a = text1, b = text2, c = diffs +* +* @param {string|Array.>} a text1 (methods 1,3,4) or +* Array of diff tuples for text1 to text2 (method 2). +* @param {string|Array.>} opt_b text2 (methods 1,4) or +* Array of diff tuples for text1 to text2 (method 3) or undefined (method 2). +* @param {string|Array.>} opt_c Array of diff tuples for +* text1 to text2 (method 4) or undefined (methods 1,2,3). +* @return {Array.<_new_patch_obj>} Array of patch objects. +--]] +function patch_make(a, opt_b, opt_c) + local text1, diffs + local type_a, type_b, type_c = type(a), type(opt_b), type(opt_c) + if (type_a == 'string') and (type_b == 'string') and (type_c == 'nil') then + -- Method 1: text1, text2 + -- Compute diffs from text1 and text2. + text1 = a + diffs = diff_main(text1, opt_b, true) + if (#diffs > 2) then + diff_cleanupSemantic(diffs) + diff_cleanupEfficiency(diffs) + end + elseif (type_a == 'table') and (type_b == 'nil') and (type_c == 'nil') then + -- Method 2: diffs + -- Compute text1 from diffs. + diffs = a + text1 = _diff_text1(diffs) + elseif (type_a == 'string') and (type_b == 'table') and (type_c == 'nil') then + -- Method 3: text1, diffs + text1 = a + diffs = opt_b + elseif (type_a == 'string') and (type_b == 'string') and (type_c == 'table') + then + -- Method 4: text1, text2, diffs + -- text2 is not used. + text1 = a + diffs = opt_c + else + error('Unknown call format to patch_make.') + end + + if (diffs[1] == nil) then + return {} -- Get rid of the null case. + end + + local patches = {} + local patch = _new_patch_obj() + local patchDiffLength = 0 -- Keeping our own length var is faster. + local char_count1 = 0 -- Number of characters into the text1 string. + local char_count2 = 0 -- Number of characters into the text2 string. + -- Start with text1 (prepatch_text) and apply the diffs until we arrive at + -- text2 (postpatch_text). We recreate the patches one by one to determine + -- context info. + local prepatch_text, postpatch_text = text1, text1 + for x, diff in ipairs(diffs) do + local diff_type, diff_text = diff[1], diff[2] + + if (patchDiffLength == 0) and (diff_type ~= DIFF_EQUAL) then + -- A new patch starts here. + patch.start1 = char_count1 + 1 + patch.start2 = char_count2 + 1 + end + + if (diff_type == DIFF_INSERT) then + patchDiffLength = patchDiffLength + 1 + patch.diffs[patchDiffLength] = diff + patch.length2 = patch.length2 + #diff_text + postpatch_text = strsub(postpatch_text, 1, char_count2) + .. diff_text .. strsub(postpatch_text, char_count2 + 1) + elseif (diff_type == DIFF_DELETE) then + patch.length1 = patch.length1 + #diff_text + patchDiffLength = patchDiffLength + 1 + patch.diffs[patchDiffLength] = diff + postpatch_text = strsub(postpatch_text, 1, char_count2) + .. strsub(postpatch_text, char_count2 + #diff_text + 1) + elseif (diff_type == DIFF_EQUAL) then + if (#diff_text <= Patch_Margin * 2) + and (patchDiffLength ~= 0) and (#diffs ~= x) then + -- Small equality inside a patch. + patchDiffLength = patchDiffLength + 1 + patch.diffs[patchDiffLength] = diff + patch.length1 = patch.length1 + #diff_text + patch.length2 = patch.length2 + #diff_text + elseif (#diff_text >= Patch_Margin * 2) then + -- Time for a new patch. + if (patchDiffLength ~= 0) then + _patch_addContext(patch, prepatch_text) + patches[#patches + 1] = patch + patch = _new_patch_obj() + patchDiffLength = 0 + -- Unlike Unidiff, our patch lists have a rolling context. + -- http://code.google.com/p/google-diff-match-patch/wiki/Unidiff + -- Update prepatch text & pos to reflect the application of the + -- just completed patch. + prepatch_text = postpatch_text + char_count1 = char_count2 + end + end + end + + -- Update the current character count. + if (diff_type ~= DIFF_INSERT) then + char_count1 = char_count1 + #diff_text + end + if (diff_type ~= DIFF_DELETE) then + char_count2 = char_count2 + #diff_text + end + end + + -- Pick up the leftover patch if not empty. + if (patchDiffLength > 0) then + _patch_addContext(patch, prepatch_text) + patches[#patches + 1] = patch + end + + return patches +end + +--[[ +* Merge a set of patches onto the text. Return a patched text, as well +* as a list of true/false values indicating which patches were applied. +* @param {Array.<_new_patch_obj>} patches Array of patch objects. +* @param {string} text Old text. +* @return {Array.>} Two return values, the +* new text and an array of boolean values. +--]] +function patch_apply(patches, text) + if patches[1] == nil then + return text, {} + end + + -- Deep copy the patches so that no changes are made to originals. + patches = _patch_deepCopy(patches) + + local nullPadding = _patch_addPadding(patches) + text = nullPadding .. text .. nullPadding + + _patch_splitMax(patches) + -- delta keeps track of the offset between the expected and actual location + -- of the previous patch. If there are patches expected at positions 10 and + -- 20, but the first patch was found at 12, delta is 2 and the second patch + -- has an effective expected position of 22. + local delta = 0 + local results = {} + for x, patch in ipairs(patches) do + local expected_loc = patch.start2 + delta + local text1 = _diff_text1(patch.diffs) + local start_loc + local end_loc = -1 + if #text1 > Match_MaxBits then + -- _patch_splitMax will only provide an oversized pattern in + -- the case of a monster delete. + start_loc = match_main(text, + strsub(text1, 1, Match_MaxBits), expected_loc) + if start_loc ~= -1 then + end_loc = match_main(text, strsub(text1, -Match_MaxBits), + expected_loc + #text1 - Match_MaxBits) + if end_loc == -1 or start_loc >= end_loc then + -- Can't find valid trailing context. Drop this patch. + start_loc = -1 + end + end + else + start_loc = match_main(text, text1, expected_loc) + end + if start_loc == -1 then + -- No match found. :( + results[x] = false + -- Subtract the delta for this failed patch from subsequent patches. + delta = delta - patch.length2 - patch.length1 + else + -- Found a match. :) + results[x] = true + delta = start_loc - expected_loc + local text2 + if end_loc == -1 then + text2 = strsub(text, start_loc, start_loc + #text1 - 1) + else + text2 = strsub(text, start_loc, end_loc + Match_MaxBits - 1) + end + if text1 == text2 then + -- Perfect match, just shove the replacement text in. + text = strsub(text, 1, start_loc - 1) .. _diff_text2(patch.diffs) + .. strsub(text, start_loc + #text1) + else + -- Imperfect match. Run a diff to get a framework of equivalent + -- indices. + local diffs = diff_main(text1, text2, false) + if (#text1 > Match_MaxBits) + and (diff_levenshtein(diffs) / #text1 > Patch_DeleteThreshold) then + -- The end points match, but the content is unacceptably bad. + results[x] = false + else + _diff_cleanupSemanticLossless(diffs) + local index1 = 1 + local index2 + for y, mod in ipairs(patch.diffs) do + if mod[1] ~= DIFF_EQUAL then + index2 = _diff_xIndex(diffs, index1) + end + if mod[1] == DIFF_INSERT then + text = strsub(text, 1, start_loc + index2 - 2) + .. mod[2] .. strsub(text, start_loc + index2 - 1) + elseif mod[1] == DIFF_DELETE then + text = strsub(text, 1, start_loc + index2 - 2) .. strsub(text, + start_loc + _diff_xIndex(diffs, index1 + #mod[2] - 1)) + end + if mod[1] ~= DIFF_DELETE then + index1 = index1 + #mod[2] + end + end + end + end + end + end + -- Strip the padding off. + text = strsub(text, #nullPadding + 1, -#nullPadding - 1) + return text, results +end + +--[[ +* Take a list of patches and return a textual representation. +* @param {Array.<_new_patch_obj>} patches Array of patch objects. +* @return {string} Text representation of patches. +--]] +function patch_toText(patches) + local text = {} + for x, patch in ipairs(patches) do + _patch_appendText(patch, text) + end + return tconcat(text) +end + +--[[ +* Parse a textual representation of patches and return a list of patch objects. +* @param {string} textline Text representation of patches. +* @return {Array.<_new_patch_obj>} Array of patch objects. +* @throws {Error} If invalid input. +--]] +function patch_fromText(textline) + local patches = {} + if (#textline == 0) then + return patches + end + local text = {} + for line in gmatch(textline, '([^\n]*)') do + text[#text + 1] = line + end + local textPointer = 1 + while (textPointer <= #text) do + local start1, length1, start2, length2 + = strmatch(text[textPointer], '^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@$') + if (start1 == nil) then + error('Invalid patch string: "' .. text[textPointer] .. '"') + end + local patch = _new_patch_obj() + patches[#patches + 1] = patch + + start1 = tonumber(start1) + length1 = tonumber(length1) or 1 + if (length1 == 0) then + start1 = start1 + 1 + end + patch.start1 = start1 + patch.length1 = length1 + + start2 = tonumber(start2) + length2 = tonumber(length2) or 1 + if (length2 == 0) then + start2 = start2 + 1 + end + patch.start2 = start2 + patch.length2 = length2 + + textPointer = textPointer + 1 + + while true do + local line = text[textPointer] + if (line == nil) then + break + end + local sign; sign, line = strsub(line, 1, 1), strsub(line, 2) + + local invalidDecode = false + local decoded = gsub(line, '%%(.?.?)', + function(c) + local n = tonumber(c, 16) + if (#c ~= 2) or (n == nil) then + invalidDecode = true + return '' + end + return strchar(n) + end) + if invalidDecode then + -- Malformed URI sequence. + error('Illegal escape in patch_fromText: ' .. line) + end + + line = decoded + + if (sign == '-') then + -- Deletion. + patch.diffs[#patch.diffs + 1] = {DIFF_DELETE, line} + elseif (sign == '+') then + -- Insertion. + patch.diffs[#patch.diffs + 1] = {DIFF_INSERT, line} + elseif (sign == ' ') then + -- Minor equality. + patch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, line} + elseif (sign == '@') then + -- Start of next patch. + break + elseif (sign == '') then + -- Blank line? Whatever. + else + -- WTF? + error('Invalid patch mode "' .. sign .. '" in: ' .. line) + end + textPointer = textPointer + 1 + end + end + return patches +end + +-- --------------------------------------------------------------------------- +-- UNOFFICIAL/PRIVATE PATCH FUNCTIONS +-- --------------------------------------------------------------------------- + +local patch_meta = { + __tostring = function(patch) + local buf = {} + _patch_appendText(patch, buf) + return tconcat(buf) + end +} + +--[[ +* Class representing one patch operation. +* @constructor +--]] +function _new_patch_obj() + return setmetatable({ + --[[ @type {Array.>} ]] + diffs = {}; + --[[ @type {?number} ]] + start1 = 1; -- nil; + --[[ @type {?number} ]] + start2 = 1; -- nil; + --[[ @type {number} ]] + length1 = 0; + --[[ @type {number} ]] + length2 = 0; + }, patch_meta) +end + +--[[ +* Increase the context until it is unique, +* but don't let the pattern expand beyond Match_MaxBits. +* @param {_new_patch_obj} patch The patch to grow. +* @param {string} text Source text. +* @private +--]] +function _patch_addContext(patch, text) + if (#text == 0) then + return + end + local pattern = strsub(text, patch.start2, patch.start2 + patch.length1 - 1) + local padding = 0 + + -- LUANOTE: Lua's lack of a lastIndexOf function results in slightly + -- different logic here than in other language ports. + -- Look for the first two matches of pattern in text. If two are found, + -- increase the pattern length. + local firstMatch = indexOf(text, pattern) + local secondMatch = nil + if (firstMatch ~= nil) then + secondMatch = indexOf(text, pattern, firstMatch + 1) + end + while (#pattern == 0 or secondMatch ~= nil) + and (#pattern < Match_MaxBits - Patch_Margin - Patch_Margin) do + padding = padding + Patch_Margin + pattern = strsub(text, max(1, patch.start2 - padding), + patch.start2 + patch.length1 - 1 + padding) + firstMatch = indexOf(text, pattern) + if (firstMatch ~= nil) then + secondMatch = indexOf(text, pattern, firstMatch + 1) + else + secondMatch = nil + end + end + -- Add one chunk for good luck. + padding = padding + Patch_Margin + + -- Add the prefix. + local prefix = strsub(text, max(1, patch.start2 - padding), patch.start2 - 1) + if (#prefix > 0) then + tinsert(patch.diffs, 1, {DIFF_EQUAL, prefix}) + end + -- Add the suffix. + local suffix = strsub(text, patch.start2 + patch.length1, + patch.start2 + patch.length1 - 1 + padding) + if (#suffix > 0) then + patch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, suffix} + end + + -- Roll back the start points. + patch.start1 = patch.start1 - #prefix + patch.start2 = patch.start2 - #prefix + -- Extend the lengths. + patch.length1 = patch.length1 + #prefix + #suffix + patch.length2 = patch.length2 + #prefix + #suffix +end + +--[[ +* Given an array of patches, return another array that is identical. +* @param {Array.<_new_patch_obj>} patches Array of patch objects. +* @return {Array.<_new_patch_obj>} Array of patch objects. +--]] +function _patch_deepCopy(patches) + local patchesCopy = {} + for x, patch in ipairs(patches) do + local patchCopy = _new_patch_obj() + local diffsCopy = {} + for i, diff in ipairs(patch.diffs) do + diffsCopy[i] = {diff[1], diff[2]} + end + patchCopy.diffs = diffsCopy + patchCopy.start1 = patch.start1 + patchCopy.start2 = patch.start2 + patchCopy.length1 = patch.length1 + patchCopy.length2 = patch.length2 + patchesCopy[x] = patchCopy + end + return patchesCopy +end + +--[[ +* Add some padding on text start and end so that edges can match something. +* Intended to be called only from within patch_apply. +* @param {Array.<_new_patch_obj>} patches Array of patch objects. +* @return {string} The padding string added to each side. +--]] +function _patch_addPadding(patches) + local paddingLength = Patch_Margin + local nullPadding = '' + for x = 1, paddingLength do + nullPadding = nullPadding .. strchar(x) + end + + -- Bump all the patches forward. + for x, patch in ipairs(patches) do + patch.start1 = patch.start1 + paddingLength + patch.start2 = patch.start2 + paddingLength + end + + -- Add some padding on start of first diff. + local patch = patches[1] + local diffs = patch.diffs + local firstDiff = diffs[1] + if (firstDiff == nil) or (firstDiff[1] ~= DIFF_EQUAL) then + -- Add nullPadding equality. + tinsert(diffs, 1, {DIFF_EQUAL, nullPadding}) + patch.start1 = patch.start1 - paddingLength -- Should be 0. + patch.start2 = patch.start2 - paddingLength -- Should be 0. + patch.length1 = patch.length1 + paddingLength + patch.length2 = patch.length2 + paddingLength + elseif (paddingLength > #firstDiff[2]) then + -- Grow first equality. + local extraLength = paddingLength - #firstDiff[2] + firstDiff[2] = strsub(nullPadding, #firstDiff[2] + 1) .. firstDiff[2] + patch.start1 = patch.start1 - extraLength + patch.start2 = patch.start2 - extraLength + patch.length1 = patch.length1 + extraLength + patch.length2 = patch.length2 + extraLength + end + + -- Add some padding on end of last diff. + patch = patches[#patches] + diffs = patch.diffs + local lastDiff = diffs[#diffs] + if (lastDiff == nil) or (lastDiff[1] ~= DIFF_EQUAL) then + -- Add nullPadding equality. + diffs[#diffs + 1] = {DIFF_EQUAL, nullPadding} + patch.length1 = patch.length1 + paddingLength + patch.length2 = patch.length2 + paddingLength + elseif (paddingLength > #lastDiff[2]) then + -- Grow last equality. + local extraLength = paddingLength - #lastDiff[2] + lastDiff[2] = lastDiff[2] .. strsub(nullPadding, 1, extraLength) + patch.length1 = patch.length1 + extraLength + patch.length2 = patch.length2 + extraLength + end + + return nullPadding +end + +--[[ +* Look through the patches and break up any which are longer than the maximum +* limit of the match algorithm. +* Intended to be called only from within patch_apply. +* @param {Array.<_new_patch_obj>} patches Array of patch objects. +--]] +function _patch_splitMax(patches) + local patch_size = Match_MaxBits + local x = 1 + while true do + local patch = patches[x] + if patch == nil then + return + end + if patch.length1 > patch_size then + local bigpatch = patch + -- Remove the big old patch. + tremove(patches, x) + x = x - 1 + local start1 = bigpatch.start1 + local start2 = bigpatch.start2 + local precontext = '' + while bigpatch.diffs[1] do + -- Create one of several smaller patches. + local patch = _new_patch_obj() + local empty = true + patch.start1 = start1 - #precontext + patch.start2 = start2 - #precontext + if precontext ~= '' then + patch.length1, patch.length2 = #precontext, #precontext + patch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, precontext} + end + while bigpatch.diffs[1] and (patch.length1 < patch_size-Patch_Margin) do + local diff_type = bigpatch.diffs[1][1] + local diff_text = bigpatch.diffs[1][2] + if (diff_type == DIFF_INSERT) then + -- Insertions are harmless. + patch.length2 = patch.length2 + #diff_text + start2 = start2 + #diff_text + patch.diffs[#(patch.diffs) + 1] = bigpatch.diffs[1] + tremove(bigpatch.diffs, 1) + empty = false + elseif (diff_type == DIFF_DELETE) and (#patch.diffs == 1) + and (patch.diffs[1][1] == DIFF_EQUAL) + and (#diff_text > 2 * patch_size) then + -- This is a large deletion. Let it pass in one chunk. + patch.length1 = patch.length1 + #diff_text + start1 = start1 + #diff_text + empty = false + patch.diffs[#patch.diffs + 1] = {diff_type, diff_text} + tremove(bigpatch.diffs, 1) + else + -- Deletion or equality. + -- Only take as much as we can stomach. + diff_text = strsub(diff_text, 1, + patch_size - patch.length1 - Patch_Margin) + patch.length1 = patch.length1 + #diff_text + start1 = start1 + #diff_text + if (diff_type == DIFF_EQUAL) then + patch.length2 = patch.length2 + #diff_text + start2 = start2 + #diff_text + else + empty = false + end + patch.diffs[#patch.diffs + 1] = {diff_type, diff_text} + if (diff_text == bigpatch.diffs[1][2]) then + tremove(bigpatch.diffs, 1) + else + bigpatch.diffs[1][2] + = strsub(bigpatch.diffs[1][2], #diff_text + 1) + end + end + end + -- Compute the head context for the next patch. + precontext = _diff_text2(patch.diffs) + precontext = strsub(precontext, -Patch_Margin) + -- Append the end context for this patch. + local postcontext = strsub(_diff_text1(bigpatch.diffs), 1, Patch_Margin) + if postcontext ~= '' then + patch.length1 = patch.length1 + #postcontext + patch.length2 = patch.length2 + #postcontext + if patch.diffs[1] + and (patch.diffs[#patch.diffs][1] == DIFF_EQUAL) then + patch.diffs[#patch.diffs][2] = patch.diffs[#patch.diffs][2] + .. postcontext + else + patch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, postcontext} + end + end + if not empty then + x = x + 1 + tinsert(patches, x, patch) + end + end + end + x = x + 1 + end +end + +--[[ +* Emulate GNU diff's format. +* Header: @@ -382,8 +481,9 @@ +* @return {string} The GNU diff string. +--]] +function _patch_appendText(patch, text) + local coords1, coords2 + local length1, length2 = patch.length1, patch.length2 + local start1, start2 = patch.start1, patch.start2 + local diffs = patch.diffs + + if length1 == 1 then + coords1 = start1 + else + coords1 = ((length1 == 0) and (start1 - 1) or start1) .. ',' .. length1 + end + + if length2 == 1 then + coords2 = start2 + else + coords2 = ((length2 == 0) and (start2 - 1) or start2) .. ',' .. length2 + end + text[#text + 1] = '@@ -' .. coords1 .. ' +' .. coords2 .. ' @@\n' + + local op + -- Escape the body of the patch with %xx notation. + for x, diff in ipairs(patch.diffs) do + local diff_type = diff[1] + if diff_type == DIFF_INSERT then + op = '+' + elseif diff_type == DIFF_DELETE then + op = '-' + elseif diff_type == DIFF_EQUAL then + op = ' ' + end + text[#text + 1] = op + .. gsub(diffs[x][2], percentEncode_pattern, percentEncode_replace) + .. '\n' + end + + return text +end + +-- Expose the API +local _M = {} + +_M.DIFF_DELETE = DIFF_DELETE +_M.DIFF_INSERT = DIFF_INSERT +_M.DIFF_EQUAL = DIFF_EQUAL + +_M.diff_main = diff_main +_M.diff_cleanupSemantic = diff_cleanupSemantic +_M.diff_cleanupEfficiency = diff_cleanupEfficiency +_M.diff_levenshtein = diff_levenshtein +_M.diff_prettyHtml = diff_prettyHtml + +_M.match_main = match_main + +_M.patch_make = patch_make +_M.patch_toText = patch_toText +_M.patch_fromText = patch_fromText +_M.patch_apply = patch_apply + +-- Expose some non-API functions as well, for testing purposes etc. +_M.diff_commonPrefix = _diff_commonPrefix +_M.diff_commonSuffix = _diff_commonSuffix +_M.diff_commonOverlap = _diff_commonOverlap +_M.diff_halfMatch = _diff_halfMatch +_M.diff_bisect = _diff_bisect +_M.diff_cleanupMerge = _diff_cleanupMerge +_M.diff_cleanupSemanticLossless = _diff_cleanupSemanticLossless +_M.diff_text1 = _diff_text1 +_M.diff_text2 = _diff_text2 +_M.diff_toDelta = _diff_toDelta +_M.diff_fromDelta = _diff_fromDelta +_M.diff_xIndex = _diff_xIndex +_M.match_alphabet = _match_alphabet +_M.match_bitap = _match_bitap +_M.new_patch_obj = _new_patch_obj +_M.patch_addContext = _patch_addContext +_M.patch_splitMax = _patch_splitMax +_M.patch_addPadding = _patch_addPadding +_M.settings = settings + +return _M diff --git a/lua/diff_match_patch_test.lua b/lua/diff_match_patch_test.lua new file mode 100644 index 0000000..943fced --- /dev/null +++ b/lua/diff_match_patch_test.lua @@ -0,0 +1,1201 @@ +--[[ +* Diff Match and Patch -- Test Harness +* Copyright 2018 The diff-match-patch Authors. +* https://github.com/google/diff-match-patch +* +* Based on the JavaScript implementation by Neil Fraser +* Ported to Lua by Duncan Cross +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +--]] + +local dmp = require 'diff_match_patch' + +local DIFF_INSERT = dmp.DIFF_INSERT +local DIFF_DELETE = dmp.DIFF_DELETE +local DIFF_EQUAL = dmp.DIFF_EQUAL + +-- Utility functions. + +local function pretty(v) + if (type(v) == 'string') then + return string.format('%q', v):gsub('\\\n', '\\n') + elseif (type(v) == 'table') then + local str = {} + local next_i = 1 + for i, v in pairs(v) do + if (i == next_i) then + next_i = next_i + 1 + str[#str + 1] = pretty(v) + else + str[#str + 1] = '[' .. pretty(i) .. ']=' .. pretty(v) + end + end + return '{' .. table.concat(str, ',') .. '}' + else + return tostring(v) + end +end + +function assertEquals(...) + local msg, expected, actual + if (select('#', ...) == 2) then + expected, actual = ... + msg = 'Expected: \'' .. pretty(expected) + .. '\' Actual: \'' .. pretty(actual) .. '\'' + else + msg, expected, actual = ... + end + assert(expected == actual, msg) +end + +function assertTrue(...) + local msg, actual + if (select('#', ...) == 1) then + actual = ... + assertEquals(true, actual) + else + msg, actual = ... + assertEquals(msg, true, actual) + end +end + +function assertFalse(...) + local msg, actual + if (select('#', ...) == 1) then + actual = ... + assertEquals(flase, actual) + else + msg, actual = ... + assertEquals(msg, false, actual) + end +end + +-- If expected and actual are the equivalent, pass the test. +function assertEquivalent(...) + local msg, expected, actual + expected, actual = ... + msg = 'Expected: \'' .. pretty(expected) + .. '\' Actual: \'' .. pretty(actual) .. '\'' + if (_equivalent(expected, actual)) then + assertEquals(msg, pretty(expected), pretty(actual)) + else + assertEquals(msg, expected, actual) + end +end + +-- Are a and b the equivalent? -- Recursive. +function _equivalent(a, b) + if (a == b) then + return true + end + if (type(a) == 'table') and (type(b) == 'table') then + for k, v in pairs(a) do + if not _equivalent(v, b[k]) then + return false + end + end + for k, v in pairs(b) do + if not _equivalent(v, a[k]) then + return false + end + end + return true + end + return false +end + +function diff_rebuildtexts(diffs) + -- Construct the two texts which made up the diff originally. + local text1, text2 = {}, {} + for x, diff in ipairs(diffs) do + local op, data = diff[1], diff[2] + if (op ~= DIFF_INSERT) then + text1[#text1 + 1] = data + end + if (op ~= DIFF_DELETE) then + text2[#text2 + 1] = data + end + end + return table.concat(text1), table.concat(text2) +end + + +-- DIFF TEST FUNCTIONS + + +function testDiffCommonPrefix() + -- Detect any common prefix. + + -- Null case. + assertEquals(0, dmp.diff_commonPrefix('abc', 'xyz')) + -- Non-null case. + assertEquals(4, dmp.diff_commonPrefix('1234abcdef', '1234xyz')) + -- Whole case. + assertEquals(4, dmp.diff_commonPrefix('1234', '1234xyz')) +end + +function testDiffCommonSuffix() + -- Detect any common suffix. + + -- Null case. + assertEquals(0, dmp.diff_commonSuffix('abc', 'xyz')) + -- Non-null case. + assertEquals(4, dmp.diff_commonSuffix('abcdef1234', 'xyz1234')) + -- Whole case. + assertEquals(4, dmp.diff_commonSuffix('1234', 'xyz1234')) +end + +function testDiffCommonOverlap() + -- Detect any suffix/prefix overlap. + + -- Null case. + assertEquals(0, dmp.diff_commonOverlap('', 'abcd')); + -- Whole case. + assertEquals(3, dmp.diff_commonOverlap('abc', 'abcd')); + -- No overlap. + assertEquals(0, dmp.diff_commonOverlap('123456', 'abcd')); + -- Overlap. + assertEquals(3, dmp.diff_commonOverlap('123456xxx', 'xxxabcd')); + --[[ + -- Unicode. + -- Some overly clever languages (C#) may treat ligatures as equal to their + -- component letters. E.g. U+FB01 == 'fi' + -- LUANOTE: No ability to handle Unicode. + assertEquals(0, dmp.diff_commonOverlap('fi', '\ufb01i')); + --]] +end + +function testDiffHalfMatch() + -- Detect a halfmatch. + dmp.settings{Diff_Timeout = 1} + + -- No match. + assertEquivalent({nil}, {dmp.diff_halfMatch('1234567890', 'abcdef')}) + assertEquivalent({nil}, {dmp.diff_halfMatch('12345', '23')}) + -- Single Match. + assertEquivalent({'12', '90', 'a', 'z', '345678'}, + {dmp.diff_halfMatch('1234567890', 'a345678z')}) + assertEquivalent({'a', 'z', '12', '90', '345678'}, + {dmp.diff_halfMatch('a345678z', '1234567890')}) + assertEquivalent({'abc', 'z', '1234', '0', '56789'}, + {dmp.diff_halfMatch('abc56789z', '1234567890')}) + assertEquivalent({'a', 'xyz', '1', '7890', '23456'}, + {dmp.diff_halfMatch('a23456xyz', '1234567890')}) + -- Multiple Matches. + assertEquivalent({'12123', '123121', 'a', 'z', '1234123451234'}, + {dmp.diff_halfMatch('121231234123451234123121', 'a1234123451234z')}) + assertEquivalent({'', '-=-=-=-=-=', 'x', '', 'x-=-=-=-=-=-=-='}, + {dmp.diff_halfMatch('x-=-=-=-=-=-=-=-=-=-=-=-=', 'xx-=-=-=-=-=-=-=')}) + assertEquivalent({'-=-=-=-=-=', '', '', 'y', '-=-=-=-=-=-=-=y'}, + {dmp.diff_halfMatch('-=-=-=-=-=-=-=-=-=-=-=-=y', '-=-=-=-=-=-=-=yy')}) + + -- Non-optimal halfmatch. + -- Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy + assertEquivalent({'qHillo', 'w', 'x', 'Hulloy', 'HelloHe'}, + {dmp.diff_halfMatch('qHilloHelloHew', 'xHelloHeHulloy')}) + -- Optimal no halfmatch. + dmp.settings{Diff_Timeout = 0} + assertEquivalent({nill}, {dmp.diff_halfMatch('qHilloHelloHew', 'xHelloHeHulloy')}) +end + +function testDiffCleanupMerge() + -- Cleanup a messy diff. + + -- Null case. + local diffs = {} + dmp.diff_cleanupMerge(diffs) + assertEquivalent({}, diffs) + -- No change case. + diffs = {{DIFF_EQUAL, 'a'}, {DIFF_DELETE, 'b'}, {DIFF_INSERT, 'c'}} + dmp.diff_cleanupMerge(diffs) + assertEquivalent({{DIFF_EQUAL, 'a'}, {DIFF_DELETE, 'b'}, {DIFF_INSERT, 'c'}}, + diffs) + -- Merge equalities. + diffs = {{DIFF_EQUAL, 'a'}, {DIFF_EQUAL, 'b'}, {DIFF_EQUAL, 'c'}} + dmp.diff_cleanupMerge(diffs) + assertEquivalent({{DIFF_EQUAL, 'abc'}}, diffs) + -- Merge deletions. + diffs = {{DIFF_DELETE, 'a'}, {DIFF_DELETE, 'b'}, {DIFF_DELETE, 'c'}} + dmp.diff_cleanupMerge(diffs) + assertEquivalent({{DIFF_DELETE, 'abc'}}, diffs) + -- Merge insertions. + diffs = {{DIFF_INSERT, 'a'}, {DIFF_INSERT, 'b'}, {DIFF_INSERT, 'c'}} + dmp.diff_cleanupMerge(diffs) + assertEquivalent({{DIFF_INSERT, 'abc'}}, diffs) + -- Merge interweave. + diffs = {{DIFF_DELETE, 'a'}, {DIFF_INSERT, 'b'}, {DIFF_DELETE, 'c'}, + {DIFF_INSERT, 'd'}, {DIFF_EQUAL, 'e'}, {DIFF_EQUAL, 'f'}} + dmp.diff_cleanupMerge(diffs) + assertEquivalent({{DIFF_DELETE, 'ac'}, {DIFF_INSERT, 'bd'}, {DIFF_EQUAL, 'ef'}}, + diffs) + -- Prefix and suffix detection. + diffs = {{DIFF_DELETE, 'a'}, {DIFF_INSERT, 'abc'}, {DIFF_DELETE, 'dc'}} + dmp.diff_cleanupMerge(diffs) + assertEquivalent({{DIFF_EQUAL, 'a'}, {DIFF_DELETE, 'd'}, + {DIFF_INSERT, 'b'}, {DIFF_EQUAL, 'c'}}, diffs) + -- Prefix and suffix detection with equalities. + diffs = {{DIFF_EQUAL, 'x'}, {DIFF_DELETE, 'a'}, {DIFF_INSERT, 'abc'}, + {DIFF_DELETE, 'dc'}, {DIFF_EQUAL, 'y'}} + dmp.diff_cleanupMerge(diffs) + assertEquivalent({{DIFF_EQUAL, 'xa'}, {DIFF_DELETE, 'd'}, + {DIFF_INSERT, 'b'}, {DIFF_EQUAL, 'cy'}}, diffs) + -- Slide edit left. + diffs = {{DIFF_EQUAL, 'a'}, {DIFF_INSERT, 'ba'}, {DIFF_EQUAL, 'c'}} + dmp.diff_cleanupMerge(diffs) + assertEquivalent({{DIFF_INSERT, 'ab'}, {DIFF_EQUAL, 'ac'}}, diffs) + -- Slide edit right. + diffs = {{DIFF_EQUAL, 'c'}, {DIFF_INSERT, 'ab'}, {DIFF_EQUAL, 'a'}} + dmp.diff_cleanupMerge(diffs) + assertEquivalent({{DIFF_EQUAL, 'ca'}, {DIFF_INSERT, 'ba'}}, diffs) + -- Slide edit left recursive. + diffs = {{DIFF_EQUAL, 'a'}, {DIFF_DELETE, 'b'}, {DIFF_EQUAL, 'c'}, + {DIFF_DELETE, 'ac'}, {DIFF_EQUAL, 'x'}} + dmp.diff_cleanupMerge(diffs) + assertEquivalent({{DIFF_DELETE, 'abc'}, {DIFF_EQUAL, 'acx'}}, diffs) + -- Slide edit right recursive. + diffs = {{DIFF_EQUAL, 'x'}, {DIFF_DELETE, 'ca'}, {DIFF_EQUAL, 'c'}, + {DIFF_DELETE, 'b'}, {DIFF_EQUAL, 'a'}} + dmp.diff_cleanupMerge(diffs) + assertEquivalent({{DIFF_EQUAL, 'xca'}, {DIFF_DELETE, 'cba'}}, diffs) +end + +function testDiffCleanupSemanticLossless() + -- Slide diffs to match logical boundaries. + + -- Null case. + local diffs = {} + dmp.diff_cleanupSemanticLossless(diffs) + assertEquivalent({}, diffs) + -- Blank lines. + diffs = {{DIFF_EQUAL, 'AAA\r\n\r\nBBB'}, {DIFF_INSERT, '\r\nDDD\r\n\r\nBBB'}, + {DIFF_EQUAL, '\r\nEEE'}} + dmp.diff_cleanupSemanticLossless(diffs) + assertEquivalent({{DIFF_EQUAL, 'AAA\r\n\r\n'}, + {DIFF_INSERT, 'BBB\r\nDDD\r\n\r\n'}, {DIFF_EQUAL, 'BBB\r\nEEE'}}, diffs) + -- Line boundaries. + diffs = {{DIFF_EQUAL, 'AAA\r\nBBB'}, {DIFF_INSERT, ' DDD\r\nBBB'}, + {DIFF_EQUAL, ' EEE'}} + dmp.diff_cleanupSemanticLossless(diffs) + assertEquivalent({{DIFF_EQUAL, 'AAA\r\n'}, {DIFF_INSERT, 'BBB DDD\r\n'}, + {DIFF_EQUAL, 'BBB EEE'}}, diffs) + -- Word boundaries. + diffs = {{DIFF_EQUAL, 'The c'}, {DIFF_INSERT, 'ow and the c'}, + {DIFF_EQUAL, 'at.'}} + dmp.diff_cleanupSemanticLossless(diffs) + assertEquivalent({{DIFF_EQUAL, 'The '}, {DIFF_INSERT, 'cow and the '}, + {DIFF_EQUAL, 'cat.'}}, diffs) + -- Alphanumeric boundaries. + diffs = {{DIFF_EQUAL, 'The-c'}, {DIFF_INSERT, 'ow-and-the-c'}, + {DIFF_EQUAL, 'at.'}} + dmp.diff_cleanupSemanticLossless(diffs) + assertEquivalent({{DIFF_EQUAL, 'The-'}, {DIFF_INSERT, 'cow-and-the-'}, + {DIFF_EQUAL, 'cat.'}}, diffs) + -- Hitting the start. + diffs = {{DIFF_EQUAL, 'a'}, {DIFF_DELETE, 'a'}, {DIFF_EQUAL, 'ax'}} + dmp.diff_cleanupSemanticLossless(diffs) + assertEquivalent({{DIFF_DELETE, 'a'}, {DIFF_EQUAL, 'aax'}}, diffs) + -- Hitting the end. + diffs = {{DIFF_EQUAL, 'xa'}, {DIFF_DELETE, 'a'}, {DIFF_EQUAL, 'a'}} + dmp.diff_cleanupSemanticLossless(diffs) + assertEquivalent({{DIFF_EQUAL, 'xaa'}, {DIFF_DELETE, 'a'}}, diffs) + -- Sentence boundaries. + diffs = {{DIFF_EQUAL, 'The xxx. The '}, {DIFF_INSERT, 'zzz. The '}, + {DIFF_EQUAL, 'yyy.'}} + dmp.diff_cleanupSemanticLossless(diffs) + assertEquivalent({{DIFF_EQUAL, 'The xxx.'}, {DIFF_INSERT, ' The zzz.'}, + {DIFF_EQUAL, ' The yyy.'}}, diffs) +end + +function testDiffCleanupSemantic() + -- Cleanup semantically trivial equalities. + + -- Null case. + local diffs = {} + dmp.diff_cleanupSemantic(diffs) + assertEquivalent({}, diffs) + -- No elimination #1. + diffs = {{DIFF_DELETE, 'ab'}, {DIFF_INSERT, 'cd'}, {DIFF_EQUAL, '12'}, + {DIFF_DELETE, 'e'}} + dmp.diff_cleanupSemantic(diffs) + assertEquivalent({{DIFF_DELETE, 'ab'}, {DIFF_INSERT, 'cd'}, {DIFF_EQUAL, '12'}, + {DIFF_DELETE, 'e'}}, diffs) + -- No elimination #2. + diffs = {{DIFF_DELETE, 'abc'}, {DIFF_INSERT, 'ABC'}, {DIFF_EQUAL, '1234'}, + {DIFF_DELETE, 'wxyz'}} + dmp.diff_cleanupSemantic(diffs) + assertEquivalent({{DIFF_DELETE, 'abc'}, {DIFF_INSERT, 'ABC'}, {DIFF_EQUAL, '1234'}, + {DIFF_DELETE, 'wxyz'}}, diffs) + -- Simple elimination. + diffs = {{DIFF_DELETE, 'a'}, {DIFF_EQUAL, 'b'}, {DIFF_DELETE, 'c'}} + dmp.diff_cleanupSemantic(diffs) + assertEquivalent({{DIFF_DELETE, 'abc'}, {DIFF_INSERT, 'b'}}, diffs) + -- Backpass elimination. + diffs = {{DIFF_DELETE, 'ab'}, {DIFF_EQUAL, 'cd'}, {DIFF_DELETE, 'e'}, + {DIFF_EQUAL, 'f'}, {DIFF_INSERT, 'g'}} + dmp.diff_cleanupSemantic(diffs) + assertEquivalent({{DIFF_DELETE, 'abcdef'}, {DIFF_INSERT, 'cdfg'}}, diffs) + -- Multiple eliminations. + diffs = {{DIFF_INSERT, '1'}, {DIFF_EQUAL, 'A'}, {DIFF_DELETE, 'B'}, + {DIFF_INSERT, '2'}, {DIFF_EQUAL, '_'}, {DIFF_INSERT, '1'}, + {DIFF_EQUAL, 'A'}, {DIFF_DELETE, 'B'}, {DIFF_INSERT, '2'}} + dmp.diff_cleanupSemantic(diffs) + assertEquivalent({{DIFF_DELETE, 'AB_AB'}, {DIFF_INSERT, '1A2_1A2'}}, diffs) + -- Word boundaries. + diffs = {{DIFF_EQUAL, 'The c'}, {DIFF_DELETE, 'ow and the c'}, + {DIFF_EQUAL, 'at.'}} + dmp.diff_cleanupSemantic(diffs) + assertEquivalent({{DIFF_EQUAL, 'The '}, {DIFF_DELETE, 'cow and the '}, + {DIFF_EQUAL, 'cat.'}}, diffs) + -- No overlap elimination. + diffs = {{DIFF_DELETE, 'abcxx'}, {DIFF_INSERT, 'xxdef'}} + dmp.diff_cleanupSemantic(diffs) + assertEquivalent({{DIFF_DELETE, 'abcxx'}, {DIFF_INSERT, 'xxdef'}}, diffs) + -- Overlap elimination. + diffs = {{DIFF_DELETE, 'abcxxx'}, {DIFF_INSERT, 'xxxdef'}} + dmp.diff_cleanupSemantic(diffs) + assertEquivalent({{DIFF_DELETE, 'abc'}, {DIFF_EQUAL, 'xxx'}, {DIFF_INSERT, 'def'}}, diffs) + -- Reverse overlap elimination. + diffs = {{DIFF_DELETE, 'xxxabc'}, {DIFF_INSERT, 'defxxx'}} + dmp.diff_cleanupSemantic(diffs) + assertEquivalent({{DIFF_INSERT, 'def'}, {DIFF_EQUAL, 'xxx'}, {DIFF_DELETE, 'abc'}}, diffs) + -- Two overlap eliminations. + diffs = {{DIFF_DELETE, 'abcd1212'}, {DIFF_INSERT, '1212efghi'}, {DIFF_EQUAL, '----'}, {DIFF_DELETE, 'A3'}, {DIFF_INSERT, '3BC'}} + dmp.diff_cleanupSemantic(diffs) + assertEquivalent({{DIFF_DELETE, 'abcd'}, {DIFF_EQUAL, '1212'}, {DIFF_INSERT, 'efghi'}, {DIFF_EQUAL, '----'}, {DIFF_DELETE, 'A'}, {DIFF_EQUAL, '3'}, {DIFF_INSERT, 'BC'}}, diffs) +end + +function testDiffCleanupEfficiency() + -- Cleanup operationally trivial equalities. + local diffs + dmp.settings{Diff_EditCost = 4} + + -- Null case. + diffs = {} + dmp.diff_cleanupEfficiency(diffs) + assertEquivalent({}, diffs) + -- No elimination. + diffs = {{DIFF_DELETE, 'ab'}, {DIFF_INSERT, '12'}, {DIFF_EQUAL, 'wxyz'}, + {DIFF_DELETE, 'cd'}, {DIFF_INSERT, '34'}} + dmp.diff_cleanupEfficiency(diffs) + assertEquivalent({{DIFF_DELETE, 'ab'}, {DIFF_INSERT, '12'}, + {DIFF_EQUAL, 'wxyz'}, {DIFF_DELETE, 'cd'}, {DIFF_INSERT, '34'}}, diffs) + -- Four-edit elimination. + diffs = {{DIFF_DELETE, 'ab'}, {DIFF_INSERT, '12'}, {DIFF_EQUAL, 'xyz'}, + {DIFF_DELETE, 'cd'}, {DIFF_INSERT, '34'}} + dmp.diff_cleanupEfficiency(diffs) + assertEquivalent({ + {DIFF_DELETE, 'abxyzcd'}, + {DIFF_INSERT, '12xyz34'} + }, diffs) + + -- Three-edit elimination. + diffs = { + {DIFF_INSERT, '12'}, + {DIFF_EQUAL, 'x'}, + {DIFF_DELETE, 'cd'}, + {DIFF_INSERT, '34'} + } + dmp.diff_cleanupEfficiency(diffs) + assertEquivalent({ + {DIFF_DELETE, 'xcd'}, + {DIFF_INSERT, '12x34'} + }, diffs) + + -- Backpass elimination. + diffs = { + {DIFF_DELETE, 'ab'}, + {DIFF_INSERT, '12'}, + {DIFF_EQUAL, 'xy'}, + {DIFF_INSERT, '34'}, + {DIFF_EQUAL, 'z'}, + {DIFF_DELETE, 'cd'}, + {DIFF_INSERT, '56'} + } + dmp.diff_cleanupEfficiency(diffs) + assertEquivalent({ + {DIFF_DELETE, 'abxyzcd'}, + {DIFF_INSERT, '12xy34z56'} + }, diffs) + + -- High cost elimination. + dmp.settings{Diff_EditCost = 5} + diffs = { + {DIFF_DELETE, 'ab'}, + {DIFF_INSERT, '12'}, + {DIFF_EQUAL, 'wxyz'}, + {DIFF_DELETE, 'cd'}, + {DIFF_INSERT, '34'} + } + dmp.diff_cleanupEfficiency(diffs) + assertEquivalent({ + {DIFF_DELETE, 'abwxyzcd'}, + {DIFF_INSERT, '12wxyz34'} + }, diffs) + + dmp.settings{Diff_EditCost = 4} +end + +function testDiffPrettyHtml() + -- Pretty print. + local diffs = { + {DIFF_EQUAL, 'a\n'}, + {DIFF_DELETE, 'b'}, + {DIFF_INSERT, 'c&d'} + } + assertEquals( + '
    ' + .. '<B>b</B>' + .. 'c&d', + dmp.diff_prettyHtml(diffs) + ) +end + +function testDiffText() + -- Compute the source and destination texts. + local diffs = { + {DIFF_EQUAL, 'jump'}, + {DIFF_DELETE, 's'}, + {DIFF_INSERT, 'ed'}, + {DIFF_EQUAL, ' over '}, + {DIFF_DELETE, 'the'}, + {DIFF_INSERT, 'a'}, + {DIFF_EQUAL, ' lazy'} + } + assertEquals('jumps over the lazy', dmp.diff_text1(diffs)) + assertEquals('jumped over a lazy', dmp.diff_text2(diffs)) +end + +function testDiffDelta() + -- Convert a diff into delta string. + local diffs = { + {DIFF_EQUAL, 'jump'}, + {DIFF_DELETE, 's'}, + {DIFF_INSERT, 'ed'}, + {DIFF_EQUAL, ' over '}, + {DIFF_DELETE, 'the'}, + {DIFF_INSERT, 'a'}, + {DIFF_EQUAL, ' lazy'}, + {DIFF_INSERT, 'old dog'} + } + local text1 = dmp.diff_text1(diffs) + assertEquals('jumps over the lazy', text1) + + local delta = dmp.diff_toDelta(diffs) + assertEquals('=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog', delta) + + -- Convert delta string into a diff. + assertEquivalent(diffs, dmp.diff_fromDelta(text1, delta)) + + -- Generates error (19 ~= 20). + success, result = pcall(dmp.diff_fromDelta, text1 .. 'x', delta) + assertEquals(false, success) + + -- Generates error (19 ~= 18). + success, result = pcall(dmp.diff_fromDelta, string.sub(text1, 2), delta) + assertEquals(false, success) + + -- Generates error (%c3%xy invalid Unicode). + success, result = pcall(dmp.patch_fromDelta, '', '+%c3%xy') + assertEquals(false, success) + + --[[ + -- Test deltas with special characters. + -- LUANOTE: No ability to handle Unicode. + diffs = {{DIFF_EQUAL, '\u0680 \000 \t %'}, {DIFF_DELETE, '\u0681 \x01 \n ^'}, {DIFF_INSERT, '\u0682 \x02 \\ |'}} + text1 = dmp.diff_text1(diffs) + assertEquals('\u0680 \x00 \t %\u0681 \x01 \n ^', text1) + + delta = dmp.diff_toDelta(diffs) + assertEquals('=7\t-7\t+%DA%82 %02 %5C %7C', delta) + --]] + + -- Convert delta string into a diff. + assertEquivalent(diffs, dmp.diff_fromDelta(text1, delta)) + + -- Verify pool of unchanged characters. + diffs = { + {DIFF_INSERT, 'A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? = @ & = + $ , # '} + } + local text2 = dmp.diff_text2(diffs) + assertEquals( + 'A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? = @ & = + $ , # ', + text2 + ) + + delta = dmp.diff_toDelta(diffs) + assertEquals( + '+A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? = @ & = + $ , # ', + delta + ) + + -- Convert delta string into a diff. + assertEquivalent(diffs, dmp.diff_fromDelta('', delta)) +end + +function testDiffXIndex() + -- Translate a location in text1 to text2. + + -- Translation on equality. + assertEquals(6, dmp.diff_xIndex({ + {DIFF_DELETE, 'a'}, + {DIFF_INSERT, '1234'}, + {DIFF_EQUAL, 'xyz'} + }, 3)) + + -- Translation on deletion. + assertEquals(2, dmp.diff_xIndex({ + {DIFF_EQUAL, 'a'}, + {DIFF_DELETE, '1234'}, + {DIFF_EQUAL, 'xyz'} + }, 4)) +end + +function testDiffLevenshtein() + -- Levenshtein with trailing equality. + assertEquals(4, dmp.diff_levenshtein({ + {DIFF_DELETE, 'abc'}, + {DIFF_INSERT, '1234'}, + {DIFF_EQUAL, 'xyz'} + })) + -- Levenshtein with leading equality. + assertEquals(4, dmp.diff_levenshtein({ + {DIFF_EQUAL, 'xyz'}, + {DIFF_DELETE, 'abc'}, + {DIFF_INSERT, '1234'} + })) + -- Levenshtein with middle equality. + assertEquals(7, dmp.diff_levenshtein({ + {DIFF_DELETE, 'abc'}, + {DIFF_EQUAL, 'xyz'}, + {DIFF_INSERT, '1234'} + })) +end + +function testDiffBisect() + -- Normal. + local a = 'cat' + local b = 'map' + -- Since the resulting diff hasn't been normalized, it would be ok if + -- the insertion and deletion pairs are swapped. + -- If the order changes, tweak this test as required. + assertEquivalent({ + {DIFF_DELETE, 'c'}, + {DIFF_INSERT, 'm'}, + {DIFF_EQUAL, 'a'}, + {DIFF_DELETE, 't'}, + {DIFF_INSERT, 'p'} + }, dmp.diff_bisect(a, b, 2 ^ 31)) + + -- Timeout. + assertEquivalent({ + {DIFF_DELETE, 'cat'}, + {DIFF_INSERT, 'map'} + }, dmp.diff_bisect(a, b, 0)) +end + +function testDiffMain() + -- Perform a trivial diff. + local a,b + + -- Null case. + assertEquivalent({}, dmp.diff_main('', '', false)) + + -- Equality. + assertEquivalent({ + {DIFF_EQUAL, 'abc'} + }, dmp.diff_main('abc', 'abc', false)) + + -- Simple insertion. + assertEquivalent({ + {DIFF_EQUAL, 'ab'}, + {DIFF_INSERT, '123'}, + {DIFF_EQUAL, 'c'} + }, dmp.diff_main('abc', 'ab123c', false)) + + -- Simple deletion. + assertEquivalent({ + {DIFF_EQUAL, 'a'}, + {DIFF_DELETE, '123'}, + {DIFF_EQUAL, 'bc'} + }, dmp.diff_main('a123bc', 'abc', false)) + + -- Two insertions. + assertEquivalent({ + {DIFF_EQUAL, 'a'}, + {DIFF_INSERT, '123'}, + {DIFF_EQUAL, 'b'}, + {DIFF_INSERT, '456'}, + {DIFF_EQUAL, 'c'} + }, dmp.diff_main('abc', 'a123b456c', false)) + + -- Two deletions. + assertEquivalent({ + {DIFF_EQUAL, 'a'}, + {DIFF_DELETE, '123'}, + {DIFF_EQUAL, 'b'}, + {DIFF_DELETE, '456'}, + {DIFF_EQUAL, 'c'} + }, dmp.diff_main('a123b456c', 'abc', false)) + + -- Perform a real diff. + -- Switch off the timeout. + dmp.settings{ Diff_Timeout=0 } + + -- Simple cases. + assertEquivalent({ + {DIFF_DELETE, 'a'}, + {DIFF_INSERT, 'b'} + }, dmp.diff_main('a', 'b', false)) + + assertEquivalent({ + {DIFF_DELETE, 'Apple'}, + {DIFF_INSERT, 'Banana'}, + {DIFF_EQUAL, 's are a'}, + {DIFF_INSERT, 'lso'}, + {DIFF_EQUAL, ' fruit.'} + }, dmp.diff_main('Apples are a fruit.', 'Bananas are also fruit.', false)) + + --[[ + -- LUANOTE: No ability to handle Unicode. + assertEquivalent({ + {DIFF_DELETE, 'a'}, + {DIFF_INSERT, '\u0680'}, + {DIFF_EQUAL, 'x'}, + {DIFF_DELETE, '\t'}, + {DIFF_INSERT, '\0'} + }, dmp.diff_main('ax\t', '\u0680x\0', false)) + ]]-- + + -- Overlaps. + assertEquivalent({ + {DIFF_DELETE, '1'}, + {DIFF_EQUAL, 'a'}, + {DIFF_DELETE, 'y'}, + {DIFF_EQUAL, 'b'}, + {DIFF_DELETE, '2'}, + {DIFF_INSERT, 'xab'} + }, dmp.diff_main('1ayb2', 'abxab', false)) + + assertEquivalent({ + {DIFF_INSERT, 'xaxcx'}, + {DIFF_EQUAL, 'abc'}, + {DIFF_DELETE, 'y'} + }, dmp.diff_main('abcy', 'xaxcxabc', false)) + + assertEquivalent({ + {DIFF_DELETE, 'ABCD'}, + {DIFF_EQUAL, 'a'}, + {DIFF_DELETE, '='}, + {DIFF_INSERT, '-'}, + {DIFF_EQUAL, 'bcd'}, + {DIFF_DELETE, '='}, + {DIFF_INSERT, '-'}, + {DIFF_EQUAL, 'efghijklmnopqrs'}, + {DIFF_DELETE, 'EFGHIJKLMNOefg'} + }, dmp.diff_main('ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg', + 'a-bcd-efghijklmnopqrs', false)) + + -- Large equality. + assertEquivalent({ + {DIFF_INSERT, ' '}, + {DIFF_EQUAL, 'a'}, + {DIFF_INSERT, 'nd'}, + {DIFF_EQUAL, ' [[Pennsylvania]]'}, + {DIFF_DELETE, ' and [[New'} + }, dmp.diff_main('a [[Pennsylvania]] and [[New', + ' and [[Pennsylvania]]', false)) + + -- Timeout. + dmp.settings{Diff_Timeout = 0.1} -- 100ms + -- Increase the text lengths by 1024 times to ensure a timeout. + a = string.rep([[ +`Twas brillig, and the slithy toves +Did gyre and gimble in the wabe: +All mimsy were the borogoves, +And the mome raths outgrabe. +]], 1024) + b = string.rep([[ +I am the very model of a modern major general, +I've information vegetable, animal, and mineral, +I know the kings of England, and I quote the fights historical, +From Marathon to Waterloo, in order categorical. +]], 1024) + local startTime = os.clock() + dmp.diff_main(a, b) + local endTime = os.clock() + -- Test that we took at least the timeout period. + assertTrue(0.1 <= endTime - startTime) + -- Test that we didn't take forever (be forgiving). + -- Theoretically this test could fail very occasionally if the + -- OS task swaps or locks up for a second at the wrong moment. + assertTrue(0.1 * 2 > endTime - startTime) + dmp.settings{Diff_Timeout = 0} + + -- Test the linemode speedup. + -- Must be long to pass the 100 char cutoff. + -- Simple line-mode. + a = string.rep('1234567890\n', 13) + b = string.rep('abcdefghij\n', 13) + assertEquivalent(dmp.diff_main(a, b, false), dmp.diff_main(a, b, true)) + + -- Single line-mode. + a = string.rep('1234567890', 13) + b = string.rep('abcdefghij', 13) + assertEquivalent(dmp.diff_main(a, b, false), dmp.diff_main(a, b, true)) + + -- Overlap line-mode. + a = string.rep('1234567890\n', 13) + b = [[ +abcdefghij +1234567890 +1234567890 +1234567890 +abcdefghij +1234567890 +1234567890 +1234567890 +abcdefghij +1234567890 +1234567890 +1234567890 +abcdefghij +]] + local texts_linemode = diff_rebuildtexts(dmp.diff_main(a, b, true)) + local texts_textmode = diff_rebuildtexts(dmp.diff_main(a, b, false)) + assertEquivalent(texts_textmode, texts_linemode) + + -- Test null inputs. + success, result = pcall(dmp.diff_main, nil, nil) + assertEquals(false, success) +end + + +-- MATCH TEST FUNCTIONS + + +function testMatchAlphabet() + -- Initialise the bitmasks for Bitap. + -- Unique. + assertEquivalent({a=4, b=2, c=1}, dmp.match_alphabet('abc')) + + -- Duplicates. + assertEquivalent({a=37, b=18, c=8}, dmp.match_alphabet('abcaba')) +end + +function testMatchBitap() + -- Bitap algorithm. + dmp.settings{Match_Distance=100, Match_Threshold=0.5} + + -- Exact matches. + assertEquals(6, dmp.match_bitap('abcdefghijk', 'fgh', 6)) + + assertEquals(6, dmp.match_bitap('abcdefghijk', 'fgh', 1)) + + -- Fuzzy matches. + assertEquals(5, dmp.match_bitap('abcdefghijk', 'efxhi', 1)) + + assertEquals(3, dmp.match_bitap('abcdefghijk', 'cdefxyhijk', 6)) + + assertEquals(-1, dmp.match_bitap('abcdefghijk', 'bxy', 2)) + + -- Overflow. + assertEquals(3, dmp.match_bitap('123456789xx0', '3456789x0', 3)) + + -- Threshold test. + dmp.settings{Match_Threshold = 0.4} + assertEquals(5, dmp.match_bitap('abcdefghijk', 'efxyhi', 2)) + + dmp.settings{Match_Threshold = 0.3} + assertEquals(-1, dmp.match_bitap('abcdefghijk', 'efxyhi', 2)) + + dmp.settings{Match_Threshold = 0.0} + assertEquals(2, dmp.match_bitap('abcdefghijk', 'bcdef', 2)) + dmp.settings{Match_Threshold = 0.5} + + -- Multiple select. + assertEquals(1, dmp.match_bitap('abcdexyzabcde', 'abccde', 4)) + + assertEquals(9, dmp.match_bitap('abcdexyzabcde', 'abccde', 6)) + + -- Distance test. + + dmp.settings{Match_Distance = 10} -- Strict location. + + assertEquals(-1, + dmp.match_bitap('abcdefghijklmnopqrstuvwxyz', 'abcdefg', 25)) + + assertEquals(1, + dmp.match_bitap('abcdefghijklmnopqrstuvwxyz', 'abcdxxefg', 2)) + + dmp.settings{Match_Distance = 1000} -- Loose location. + + assertEquals(1, + dmp.match_bitap('abcdefghijklmnopqrstuvwxyz', 'abcdefg', 25)) +end + +function testMatchMain() + -- Full match. + -- Shortcut matches. + assertEquals(1, dmp.match_main('abcdef', 'abcdef', 1000)) + + assertEquals(-1, dmp.match_main('', 'abcdef', 2)) + + assertEquals(4, dmp.match_main('abcdef', '', 4)) + + assertEquals(4, dmp.match_main('abcdef', 'de', 4)) + + -- Beyond end match. + assertEquals(4, dmp.match_main("abcdef", "defy", 5)) + + -- Oversized pattern. + assertEquals(1, dmp.match_main("abcdef", "abcdefy", 1)) + + -- Complex match. + assertEquals(5, dmp.match_main( + 'I am the very model of a modern major general.', + ' that berry ', + 6 + )) + + -- Test null inputs. + success, result = pcall(dmp.match_main, nil, nil, 0) + assertEquals(false, success) +end + + +-- PATCH TEST FUNCTIONS + + +function testPatchObj() + -- Patch Object. + local p = dmp.new_patch_obj() + p.start1 = 21 + p.start2 = 22 + p.length1 = 18 + p.length2 = 17 + p.diffs = { + {DIFF_EQUAL, 'jump'}, + {DIFF_DELETE, 's'}, + {DIFF_INSERT, 'ed'}, + {DIFF_EQUAL, ' over '}, + {DIFF_DELETE, 'the'}, + {DIFF_INSERT, 'a'}, + {DIFF_EQUAL, '\nlaz'} + } + local strp = tostring(p) + assertEquals( + '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n', + strp + ) +end + +function testPatchFromText() + local strp + + strp = '' + assertEquivalent({}, dmp.patch_fromText(strp)) + + strp = '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n' + assertEquals(strp, tostring(dmp.patch_fromText(strp)[1])) + + assertEquals( + '@@ -1 +1 @@\n-a\n+b\n', + tostring(dmp.patch_fromText('@@ -1 +1 @@\n-a\n+b\n')[1]) + ) + + assertEquals( + '@@ -1,3 +0,0 @@\n-abc\n', + tostring(dmp.patch_fromText('@@ -1,3 +0,0 @@\n-abc\n')[1]) + ) + + assertEquals( + '@@ -0,0 +1,3 @@\n+abc\n', + tostring(dmp.patch_fromText('@@ -0,0 +1,3 @@\n+abc\n')[1]) + ) + + -- Generates error. + success, result = pcall(dmp.patch_fromText, 'Bad\nPatch\n') + assertEquals(false, success) +end + +function testPatchToText() + local strp, p + + strp = '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n' + p = dmp.patch_fromText(strp) + assertEquals(strp, dmp.patch_toText(p)) + + strp = '@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n' + .. '@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n' + p = dmp.patch_fromText(strp) + assertEquals(strp, dmp.patch_toText(p)) +end + +function testPatchAddContext() + local p + dmp.settings{Patch_Margin = 4} + + p = dmp.patch_fromText('@@ -21,4 +21,10 @@\n-jump\n+somersault\n')[1] + + dmp.patch_addContext(p, 'The quick brown fox jumps over the lazy dog.') + + assertEquals( + '@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n', + tostring(p) + ) + + -- Same, but not enough trailing context. + p = dmp.patch_fromText('@@ -21,4 +21,10 @@\n-jump\n+somersault\n')[1] + dmp.patch_addContext(p, 'The quick brown fox jumps.') + assertEquals( + '@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n', + tostring(p) + ) + + -- Same, but not enough leading context. + p = dmp.patch_fromText('@@ -3 +3,2 @@\n-e\n+at\n')[1] + dmp.patch_addContext(p, 'The quick brown fox jumps.') + assertEquals('@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n', tostring(p)) + + -- Same, but with ambiguity. + p = dmp.patch_fromText('@@ -3 +3,2 @@\n-e\n+at\n')[1] + dmp.patch_addContext(p, 'The quick brown fox jumps. The quick brown fox crashes.') + assertEquals('@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n', tostring(p)) +end + +function testPatchMake() + -- Null case. + local patches = dmp.patch_make('', '') + assertEquals('', dmp.patch_toText(patches)) + + local text1 = 'The quick brown fox jumps over the lazy dog.' + local text2 = 'That quick brown fox jumped over a lazy dog.' + -- Text2+Text1 inputs. + local expectedPatch = '@@ -1,8 +1,7 @@\n Th\n-at\n+e\n qui\n' + .. '@@ -21,17 +21,18 @@\n jump\n-ed\n+s\n over \n-a\n+the\n laz\n' + -- The second patch must be "-21,17 +21,18", + -- not "-22,17 +21,18" due to rolling context. + patches = dmp.patch_make(text2, text1) + assertEquals(expectedPatch, dmp.patch_toText(patches)) + + -- Text1+Text2 inputs. + expectedPatch = '@@ -1,11 +1,12 @@\n Th\n-e\n+at\n quick b\n' + .. '@@ -22,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n' + patches = dmp.patch_make(text1, text2) + assertEquals(expectedPatch, dmp.patch_toText(patches)) + + -- Diff input. + local diffs = dmp.diff_main(text1, text2, false) + patches = dmp.patch_make(diffs) + assertEquals(expectedPatch, dmp.patch_toText(patches)) + + -- Text1+Diff inputs. + patches = dmp.patch_make(text1, diffs) + assertEquals(expectedPatch, dmp.patch_toText(patches)) + + -- Text1+Text2+Diff inputs (deprecated). + patches = dmp.patch_make(text1, text2, diffs) + assertEquals(expectedPatch, dmp.patch_toText(patches)) + + -- Character encoding. + patches = dmp.patch_make('`1234567890-=[]\\;\',./', '~!@#$%^&*()_+{}|="<>?') + assertEquals('@@ -1,21 +1,21 @@\n' + .. '-%601234567890-=%5B%5D%5C;\',./\n' + .. '+~!@#$%25%5E&*()_+%7B%7D%7C=%22%3C%3E?\n', dmp.patch_toText(patches)) + + -- Character decoding. + diffs = { + {DIFF_DELETE, '`1234567890-=[]\\;\',./'}, + {DIFF_INSERT, '~!@#$%^&*()_+{}|="<>?'} + } + assertEquivalent(diffs, dmp.patch_fromText( + '@@ -1,21 +1,21 @@' + .. '\n-%601234567890-=%5B%5D%5C;\',./' + .. '\n+~!@#$%25%5E&*()_+%7B%7D%7C=%22%3C%3E?\n' + )[1].diffs) + + -- Long string with repeats. + text1 = string.rep('abcdef', 100) + text2 = text1 .. '123' + expectedPatch = '@@ -573,28 +573,31 @@\n' + .. ' cdefabcdefabcdefabcdefabcdef\n+123\n' + patches = dmp.patch_make(text1, text2) + assertEquals(expectedPatch, dmp.patch_toText(patches)) + + -- Test null inputs. + success, result = pcall(dmp.patch_make, nil, nil) + assertEquals(false, success) +end + +function testPatchSplitMax() + -- Assumes that dmp.Match_MaxBits is 32. + local patches = dmp.patch_make('abcdefghijklmnopqrstuvwxyz01234567890', + 'XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0') + dmp.patch_splitMax(patches) + assertEquals('@@ -1,32 +1,46 @@\n+X\n ab\n+X\n cd\n+X\n ef\n+X\n gh\n+X\n ij\n+X\n kl\n+X\n mn\n+X\n op\n+X\n qr\n+X\n st\n+X\n uv\n+X\n wx\n+X\n yz\n+X\n 012345\n@@ -25,13 +39,18 @@\n zX01\n+X\n 23\n+X\n 45\n+X\n 67\n+X\n 89\n+X\n 0\n', dmp.patch_toText(patches)) + + patches = dmp.patch_make('abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz', 'abcdefuvwxyz') + local oldToText = dmp.patch_toText(patches) + dmp.patch_splitMax(patches) + assertEquals(oldToText, dmp.patch_toText(patches)) + + patches = dmp.patch_make('1234567890123456789012345678901234567890123456789012345678901234567890', 'abc') + dmp.patch_splitMax(patches) + assertEquals('@@ -1,32 +1,4 @@\n-1234567890123456789012345678\n 9012\n@@ -29,32 +1,4 @@\n-9012345678901234567890123456\n 7890\n@@ -57,14 +1,3 @@\n-78901234567890\n+abc\n', dmp.patch_toText(patches)) + + patches = dmp.patch_make('abcdefghij , h = 0 , t = 1 abcdefghij , h = 0 , t = 1 abcdefghij , h = 0 , t = 1', 'abcdefghij , h = 1 , t = 1 abcdefghij , h = 1 , t = 1 abcdefghij , h = 0 , t = 1') + dmp.patch_splitMax(patches) + assertEquals('@@ -2,32 +2,32 @@\n bcdefghij , h = \n-0\n+1\n , t = 1 abcdef\n@@ -29,32 +29,32 @@\n bcdefghij , h = \n-0\n+1\n , t = 1 abcdef\n', dmp.patch_toText(patches)) +end + +function testPatchAddPadding() + -- Both edges full. + local patches = dmp.patch_make('', 'test') + assertEquals('@@ -0,0 +1,4 @@\n+test\n', dmp.patch_toText(patches)) + dmp.patch_addPadding(patches) + assertEquals('@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n', dmp.patch_toText(patches)) + + -- Both edges partial. + patches = dmp.patch_make('XY', 'XtestY') + assertEquals('@@ -1,2 +1,6 @@\n X\n+test\n Y\n', dmp.patch_toText(patches)) + dmp.patch_addPadding(patches) + assertEquals('@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n', dmp.patch_toText(patches)) + + -- Both edges none. + patches = dmp.patch_make('XXXXYYYY', 'XXXXtestYYYY') + assertEquals('@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n', dmp.patch_toText(patches)) + dmp.patch_addPadding(patches) + assertEquals('@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n', dmp.patch_toText(patches)) +end + +function testPatchApply() + local patches + + dmp.settings{Match_Distance = 1000} + dmp.settings{Match_Threshold = 0.5} + dmp.settings{Patch_DeleteThreshold = 0.5} + -- Null case. + patches = dmp.patch_make('', '') + assertEquivalent({'Hello world.', {}}, + {dmp.patch_apply(patches, 'Hello world.')}) + + -- Exact match. + patches = dmp.patch_make('The quick brown fox jumps over the lazy dog.', + 'That quick brown fox jumped over a lazy dog.') + assertEquivalent( + {'That quick brown fox jumped over a lazy dog.', {true, true}}, + {dmp.patch_apply(patches, 'The quick brown fox jumps over the lazy dog.')}) + -- Partial match. + assertEquivalent( + {'That quick red rabbit jumped over a tired tiger.', {true, true}}, + {dmp.patch_apply(patches, 'The quick red rabbit jumps over the tired tiger.')}) + -- Failed match. + assertEquivalent( + {'I am the very model of a modern major general.', {false, false}}, + {dmp.patch_apply(patches, 'I am the very model of a modern major general.')}) + -- Big delete, small change. + patches = dmp.patch_make( + 'x1234567890123456789012345678901234567890123456789012345678901234567890y', + 'xabcy') + assertEquivalent({'xabcy', {true, true}}, {dmp.patch_apply(patches, + 'x123456789012345678901234567890-----++++++++++-----' + .. '123456789012345678901234567890y')}) + -- Big delete, big change 1. + patches = dmp.patch_make('x1234567890123456789012345678901234567890123456789' + .. '012345678901234567890y', 'xabcy') + assertEquivalent({'xabc12345678901234567890' + .. '---------------++++++++++---------------' + .. '12345678901234567890y', {false, true}}, + {dmp.patch_apply(patches, 'x12345678901234567890' + .. '---------------++++++++++---------------' + .. '12345678901234567890y' + )}) + -- Big delete, big change 2. + dmp.settings{Patch_DeleteThreshold = 0.6} + patches = dmp.patch_make( + 'x1234567890123456789012345678901234567890123456789' + .. '012345678901234567890y', + 'xabcy' + ) + assertEquivalent({'xabcy', {true, true}}, {dmp.patch_apply( + patches, + 'x12345678901234567890---------------++++++++++---------------' + .. '12345678901234567890y' + )} +) + dmp.settings{Patch_DeleteThreshold = 0.5} + + -- Compensate for failed patch. + dmp.settings{Match_Threshold = 0, Match_Distance = 0} + patches = dmp.patch_make( + 'abcdefghijklmnopqrstuvwxyz--------------------1234567890', + 'abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------' + .. '1234567YYYYYYYYYY890' + ) + assertEquivalent({ + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890', + {false, true} + }, {dmp.patch_apply( + patches, + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890' + )}) + + dmp.settings{Match_Threshold = 0.5} + dmp.settings{Match_Distance = 1000} + + -- No side effects. + patches = dmp.patch_make('', 'test') + local patchstr = dmp.patch_toText(patches) + dmp.patch_apply(patches, '') + assertEquals(patchstr, dmp.patch_toText(patches)) + -- No side effects with major delete. + patches = dmp.patch_make('The quick brown fox jumps over the lazy dog.', + 'Woof') + patchstr = dmp.patch_toText(patches) + dmp.patch_apply(patches, 'The quick brown fox jumps over the lazy dog.') + assertEquals(patchstr, dmp.patch_toText(patches)) + -- Edge exact match. + patches = dmp.patch_make('', 'test') + assertEquivalent({'test', {true}}, {dmp.patch_apply(patches, '')}) + -- Near edge exact match. + patches = dmp.patch_make('XY', 'XtestY') + assertEquivalent({'XtestY', {true}}, {dmp.patch_apply(patches, 'XY')}) + -- Edge partial match. + patches = dmp.patch_make('y', 'y123') + assertEquivalent({'x123', {true}}, {dmp.patch_apply(patches, 'x')}) +end + +function runTests() + local passed = 0 + local failed = 0 + for name, func in pairs(_G) do + if (type(func) == 'function') and tostring(name):match("^test") then + local success, message = pcall(func) + if success then + print(name .. ' Ok.') + passed = passed + 1 + else + print('** ' .. name .. ' FAILED: ' .. tostring(message)) + failed = failed + 1 + end + end + end + print('Tests passed: ' .. passed) + print('Tests failed: ' .. failed) + if failed ~= 0 then + os.exit(1) + end +end + +runTests() diff --git a/maven/diff_match_patch/diff_match_patch/current/diff_match_patch-current-src.jar b/maven/diff_match_patch/diff_match_patch/current/diff_match_patch-current-src.jar new file mode 100644 index 0000000000000000000000000000000000000000..a92f711dd06016b9bade3e89f9e207a4e7b3f683 GIT binary patch literal 134290 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 }^4}f*|dusbp?Fz}M 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 Bbc+B0 literal 0 HcmV?d00001 diff --git a/maven/diff_match_patch/diff_match_patch/current/diff_match_patch-current.jar b/maven/diff_match_patch/diff_match_patch/current/diff_match_patch-current.jar new file mode 100755 index 0000000000000000000000000000000000000000..a4be2d166a3a7484737dc7d28d8120ad96a8d6c4 GIT binary patch literal 24454 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 YnW7BrGyDqyf&BcLeZI-b+Al}{1>r7u9{>OV literal 0 HcmV?d00001 diff --git a/maven/diff_match_patch/diff_match_patch/current/diff_match_patch-current.pom b/maven/diff_match_patch/diff_match_patch/current/diff_match_patch-current.pom new file mode 100644 index 0000000..23c0cd7 --- /dev/null +++ b/maven/diff_match_patch/diff_match_patch/current/diff_match_patch-current.pom @@ -0,0 +1 @@ +4.0.0diff_match_patchdiff_match_patchcurrentGoogleDiffMatchPatchhttp://code.google.com/p/google-diff-match-patchThe Diff Match and Patch libraries offer robust algorithms to perform the operations required for synchronizing plain text. Googlehttp://www.google.comApache 2.0http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/maven/diff_match_patch/diff_match_patch/maven-metadata.xml b/maven/diff_match_patch/diff_match_patch/maven-metadata.xml new file mode 100644 index 0000000..bb4e93b --- /dev/null +++ b/maven/diff_match_patch/diff_match_patch/maven-metadata.xml @@ -0,0 +1 @@ +diff_match_patchdiff_match_patchcurrentcurrent diff --git a/objectivec/Configurations/Base+SnowLeopard.xcconfig b/objectivec/Configurations/Base+SnowLeopard.xcconfig new file mode 100755 index 0000000..2226123 --- /dev/null +++ b/objectivec/Configurations/Base+SnowLeopard.xcconfig @@ -0,0 +1,3 @@ +#include "Base.xcconfig" + +SDKROOT = macosx10.6 diff --git a/objectivec/Configurations/Base.xcconfig b/objectivec/Configurations/Base.xcconfig new file mode 100755 index 0000000..a491c46 --- /dev/null +++ b/objectivec/Configurations/Base.xcconfig @@ -0,0 +1,35 @@ +#include "Version.xcconfig" + +SDKROOT = macosx10.5 +PREBINDING = NO +ARCHS = i386 x86_64 +//ARCHS = ppc i386 x86_64 // PPC works, but fails the timing tests under Rosetta + +GCC_VERSION = com.apple.compilers.llvm.clang.1_0 +GCC_C_LANGUAGE_STANDARD = gnu99 + +GCC_WARN_CHECK_SWITCH_STATEMENTS = YES +GCC_WARN_FOUR_CHARACTER_CONSTANTS = NO +GCC_WARN_SHADOW = YES +GCC_TREAT_WARNINGS_AS_ERRORS = YES +GCC_WARN_64_TO_32_BIT_CONVERSION = YES +GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES +GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES +GCC_WARN_ABOUT_RETURN_TYPE = YES +GCC_WARN_MISSING_PARENTHESES = YES +GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES +GCC_WARN_ABOUT_MISSING_NEWLINE = YES +GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES +GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES +GCC_WARN_SIGN_COMPARE = YES +GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES +GCC_WARN_UNDECLARED_SELECTOR = YES +GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES +GCC_WARN_UNINITIALIZED_AUTOS = YES +GCC_WARN_UNKNOWN_PRAGMAS = YES +GCC_WARN_UNUSED_FUNCTION = YES +GCC_WARN_UNUSED_LABEL = YES +GCC_WARN_UNUSED_PARAMETER = NO +GCC_WARN_UNUSED_VALUE = YES +GCC_WARN_UNUSED_VARIABLE = YES + diff --git a/objectivec/Configurations/Version.xcconfig b/objectivec/Configurations/Version.xcconfig new file mode 100755 index 0000000..03512e7 --- /dev/null +++ b/objectivec/Configurations/Version.xcconfig @@ -0,0 +1,2 @@ +MARKETING_VERSION = 1.0.3 +PROJECT_VERSION = 1000 diff --git a/objectivec/DiffMatchPatch.h b/objectivec/DiffMatchPatch.h new file mode 100755 index 0000000..b2dc083 --- /dev/null +++ b/objectivec/DiffMatchPatch.h @@ -0,0 +1,174 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#import + +/* + * Functions for diff, match and patch. + * Computes the difference between two texts to create a patch. + * Applies the patch onto another text, allowing for errors. + */ + +/* + * The data structure representing a diff is an NSMutableArray of Diff objects: + * {Diff(Operation.DIFF_DELETE, "Hello"), + * Diff(Operation.DIFF_INSERT, "Goodbye"), + * Diff(Operation.DIFF_EQUAL, " world.")} + * which means: delete "Hello", add "Goodbye" and keep " world." + */ + +typedef enum { + DIFF_DELETE = 1, + DIFF_INSERT = 2, + DIFF_EQUAL = 3 +} Operation; + + +/* + * Class representing one diff operation. + */ +@interface Diff : NSObject { + Operation operation; // One of: DIFF_INSERT, DIFF_DELETE or DIFF_EQUAL. + NSString *text; // The text associated with this diff operation. +} + +@property (nonatomic, assign) Operation operation; +@property (nonatomic, copy) NSString *text; + ++ (id)diffWithOperation:(Operation)anOperation andText:(NSString *)aText; + +- (id)initWithOperation:(Operation)anOperation andText:(NSString *)aText; + +@end + +/* + * Class representing one patch operation. + */ +@interface Patch : NSObject { + NSMutableArray *diffs; + NSUInteger start1; + NSUInteger start2; + NSUInteger length1; + NSUInteger length2; +} + +@property (nonatomic, retain) NSMutableArray *diffs; +@property (nonatomic, assign) NSUInteger start1; +@property (nonatomic, assign) NSUInteger start2; +@property (nonatomic, assign) NSUInteger length1; +@property (nonatomic, assign) NSUInteger length2; + +@end + + +/* + * Class containing the diff, match and patch methods. + * Also Contains the behaviour settings. + */ +@interface DiffMatchPatch : NSObject { + // Number of seconds to map a diff before giving up (0 for infinity). + NSTimeInterval Diff_Timeout; + + // Cost of an empty edit operation in terms of edit characters. + NSUInteger Diff_EditCost; + + // At what point is no match declared (0.0 = perfection, 1.0 = very loose). + double Match_Threshold; + + // How far to search for a match (0 = exact location, 1000+ = broad match). + // A match this many characters away from the expected location will add + // 1.0 to the score (0.0 is a perfect match). + NSInteger Match_Distance; + + // When deleting a large block of text (over ~64 characters), how close + // do the contents have to be to match the expected contents. (0.0 = + // perfection, 1.0 = very loose). Note that Match_Threshold controls + // how closely the end points of a delete need to match. + float Patch_DeleteThreshold; + + // Chunk size for context length. + uint16_t Patch_Margin; + + // The number of bits in an int. + NSUInteger Match_MaxBits; +} + +@property (nonatomic, assign) NSTimeInterval Diff_Timeout; +@property (nonatomic, assign) NSUInteger Diff_EditCost; +@property (nonatomic, assign) double Match_Threshold; +@property (nonatomic, assign) NSInteger Match_Distance; +@property (nonatomic, assign) float Patch_DeleteThreshold; +@property (nonatomic, assign) uint16_t Patch_Margin; + +- (NSMutableArray *)diff_mainOfOldString:(NSString *)text1 andNewString:(NSString *)text2; +- (NSMutableArray *)diff_mainOfOldString:(NSString *)text1 andNewString:(NSString *)text2 checkLines:(BOOL)checklines; +- (NSUInteger)diff_commonPrefixOfFirstString:(NSString *)text1 andSecondString:(NSString *)text2; +- (NSUInteger)diff_commonSuffixOfFirstString:(NSString *)text1 andSecondString:(NSString *)text2; +- (void)diff_cleanupSemantic:(NSMutableArray *)diffs; +- (void)diff_cleanupSemanticLossless:(NSMutableArray *)diffs; +- (void)diff_cleanupEfficiency:(NSMutableArray *)diffs; +- (void)diff_cleanupMerge:(NSMutableArray *)diffs; +- (NSUInteger)diff_xIndexIn:(NSMutableArray *)diffs location:(NSUInteger) loc; +- (NSString *)diff_prettyHtml:(NSMutableArray *)diffs; +- (NSString *)diff_text1:(NSMutableArray *)diffs; +- (NSString *)diff_text2:(NSMutableArray *)diffs; +- (NSUInteger)diff_levenshtein:(NSMutableArray *)diffs; +- (NSString *)diff_toDelta:(NSMutableArray *)diffs; +- (NSMutableArray *)diff_fromDeltaWithText:(NSString *)text1 andDelta:(NSString *)delta error:(NSError **)error; + +- (NSUInteger)match_mainForText:(NSString *)text pattern:(NSString *)pattern near:(NSUInteger)loc; +- (NSMutableDictionary *)match_alphabet:(NSString *)pattern; + +- (NSMutableArray *)patch_makeFromOldString:(NSString *)text1 andNewString:(NSString *)text2; +- (NSMutableArray *)patch_makeFromDiffs:(NSMutableArray *)diffs; +- (NSMutableArray *)patch_makeFromOldString:(NSString *)text1 newString:(NSString *)text2 diffs:(NSMutableArray *)diffs; +- (NSMutableArray *)patch_makeFromOldString:(NSString *)text1 andDiffs:(NSMutableArray *)diffs; +- (NSMutableArray *)patch_deepCopy:(NSArray *)patches; // Copy rule applies! +- (NSArray *)patch_apply:(NSArray *)sourcePatches toString:(NSString *)text; +- (NSString *)patch_addPadding:(NSMutableArray *)patches; +- (void)patch_splitMax:(NSMutableArray *)patches; +- (NSString *)patch_toText:(NSMutableArray *)patches; +- (NSMutableArray *)patch_fromText:(NSString *)textline error:(NSError **)error; + +@end + + +@interface DiffMatchPatch (PrivateMethods) + +- (NSMutableArray *)diff_mainOfOldString:(NSString *)text1 andNewString:(NSString *)text2 checkLines:(BOOL)checklines deadline:(NSTimeInterval)deadline; +- (NSMutableArray *)diff_computeFromOldString:(NSString *)text1 andNewString:(NSString *)text2 checkLines:(BOOL)checklines deadline:(NSTimeInterval)deadline; +- (NSMutableArray *)diff_lineModeFromOldString:(NSString *)text1 andNewString:(NSString *)text2 deadline:(NSTimeInterval)deadline; +- (NSArray *)diff_linesToCharsForFirstString:(NSString *)text1 andSecondString:(NSString *)text1; +- (NSString *)diff_linesToCharsMungeOfText:(NSString *)text lineArray:(NSMutableArray *)lineArray lineHash:(NSMutableDictionary *)lineHash; +- (void)diff_chars:(NSArray *)diffs toLines:(NSMutableArray *)lineArray; +- (NSMutableArray *)diff_bisectOfOldString:(NSString *)text1 andNewString:(NSString *)text2 deadline:(NSTimeInterval)deadline; +- (NSMutableArray *)diff_bisectSplitOfOldString:(NSString *)text1 andNewString:(NSString *)text2 x:(NSUInteger)x y:(NSUInteger)y deadline:(NSTimeInterval)deadline; +- (NSUInteger)diff_commonOverlapOfFirstString:(NSString *)text1 andSecondString:(NSString *)text2; +- (NSArray *)diff_halfMatchOfFirstString:(NSString *)text1 andSecondString:(NSString *)text2; +- (NSArray *)diff_halfMatchIOfLongString:(NSString *)longtext andShortString:(NSString *)shorttext; +- (NSInteger)diff_cleanupSemanticScoreOfFirstString:(NSString *)one andSecondString:(NSString *)two; + +- (NSUInteger)match_bitapOfText:(NSString *)text andPattern:(NSString *)pattern near:(NSUInteger)loc; +- (double)match_bitapScoreForErrorCount:(NSUInteger)e location:(NSUInteger)x near:(NSUInteger)loc pattern:(NSString *)pattern; + +- (void)patch_addContextToPatch:(Patch *)patch sourceText:(NSString *)text; + +@end diff --git a/objectivec/DiffMatchPatch.m b/objectivec/DiffMatchPatch.m new file mode 100755 index 0000000..cfe0c66 --- /dev/null +++ b/objectivec/DiffMatchPatch.m @@ -0,0 +1,2559 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#import "DiffMatchPatch.h" + +#import "NSString+JavaSubstring.h" +#import "NSString+UriCompatibility.h" +#import "NSMutableDictionary+DMPExtensions.h" +#import "DiffMatchPatchCFUtilities.h" + + +#if !defined(MAX_OF_CONST_AND_DIFF) + // Determines the maximum of two expressions: + // The first is a constant (first parameter) while the second expression is + // the difference between the second and third parameter. The way this is + // calculated prevents integer overflow in the result of the difference. + #define MAX_OF_CONST_AND_DIFF(A,B,C) ((B) <= (C) ? (A) : (B) - (C) + (A)) +#endif + + +// JavaScript-style splice function +void splice(NSMutableArray *input, NSUInteger start, NSUInteger count, NSArray *objects); + +/* NSMutableArray * */ void splice(NSMutableArray *input, NSUInteger start, NSUInteger count, NSArray *objects) { + NSRange deletionRange = NSMakeRange(start, count); + if (objects == nil) { + [input removeObjectsInRange:deletionRange]; + } else { + [input replaceObjectsInRange:deletionRange withObjectsFromArray:objects]; + } +} + +@implementation Diff + +@synthesize operation; +@synthesize text; + +/** + * Constructor. Initializes the diff with the provided values. + * @param operation One of DIFF_INSERT, DIFF_DELETE or DIFF_EQUAL. + * @param text The text being applied. + */ ++ (id)diffWithOperation:(Operation)anOperation + andText:(NSString *)aText; +{ + return [[[self alloc] initWithOperation:anOperation andText:aText] autorelease]; +} + +- (id)initWithOperation:(Operation)anOperation + andText:(NSString *)aText; +{ + self = [super init]; + if (self) { + self.operation = anOperation; + self.text = aText; + } + return self; + +} + +- (void)dealloc +{ + self.text = nil; + + [super dealloc]; +} + +- (id)copyWithZone:(NSZone *)zone +{ + id newDiff = [[[self class] allocWithZone:zone] + initWithOperation:self.operation + andText:self.text]; + + return newDiff; +} + + +/** + * Display a human-readable version of this Diff. + * @return text version. + */ +- (NSString *)description +{ + NSString *prettyText = [self.text stringByReplacingOccurrencesOfString:@"\n" withString:@"\u00b6"]; + NSString *operationName = nil; + switch (self.operation) { + case DIFF_DELETE: + operationName = @"DIFF_DELETE"; + break; + case DIFF_INSERT: + operationName = @"DIFF_INSERT"; + break; + case DIFF_EQUAL: + operationName = @"DIFF_EQUAL"; + break; + default: + break; + } + + return [NSString stringWithFormat:@"Diff(%@,\"%@\")", operationName, prettyText]; +} + +/** + * Is this Diff equivalent to another Diff? + * @param obj Another Diff to compare against. + * @return YES or NO. + */ +- (BOOL)isEqual:(id)obj +{ + // If parameter is nil return NO. + if (obj == nil) { + return NO; + } + + // If parameter cannot be cast to Diff return NO. + if (![obj isKindOfClass:[Diff class]]) { + return NO; + } + + // Return YES if the fields match. + Diff *p = (Diff *)obj; + return p.operation == self.operation && [p.text isEqualToString:self.text]; +} + +- (BOOL)isEqualToDiff:(Diff *)obj +{ + // If parameter is nil return NO. + if (obj == nil) { + return NO; + } + + // Return YES if the fields match. + return obj.operation == self.operation && [obj.text isEqualToString:self.text]; +} + +- (NSUInteger)hash +{ + return ([text hash] ^ (NSUInteger)operation); +} + +@end + + +@implementation Patch + +@synthesize diffs; +@synthesize start1; +@synthesize start2; +@synthesize length1; +@synthesize length2; + +- (id)init +{ + self = [super init]; + + if (self) { + self.diffs = [NSMutableArray array]; + } + + return self; +} + +- (void)dealloc +{ + self.diffs = nil; + + [super dealloc]; +} + +- (id)copyWithZone:(NSZone *)zone +{ + Patch *newPatch = [[[self class] allocWithZone:zone] init]; + + newPatch.diffs = [[NSMutableArray alloc] initWithArray:self.diffs copyItems:YES]; + newPatch.start1 = self.start1; + newPatch.start2 = self.start2; + newPatch.length1 = self.length1; + newPatch.length2 = self.length2; + + return newPatch; +} + + +/** + * Emulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indicies are printed as 1-based, not 0-based. + * @return The GNU diff NSString. + */ +- (NSString *)description +{ + NSString *coords1; + NSString *coords2; + + if (self.length1 == 0) { + coords1 = [NSString stringWithFormat:@"%lu,0", + (unsigned long)self.start1]; + } else if (self.length1 == 1) { + coords1 = [NSString stringWithFormat:@"%lu", + (unsigned long)self.start1 + 1]; + } else { + coords1 = [NSString stringWithFormat:@"%lu,%lu", + (unsigned long)self.start1 + 1, (unsigned long)self.length1]; + } + if (self.length2 == 0) { + coords2 = [NSString stringWithFormat:@"%lu,0", + (unsigned long)self.start2]; + } else if (self.length2 == 1) { + coords2 = [NSString stringWithFormat:@"%lu", + (unsigned long)self.start2 + 1]; + } else { + coords2 = [NSString stringWithFormat:@"%lu,%lu", + (unsigned long)self.start2 + 1, (unsigned long)self.length2]; + } + + NSMutableString *text = [NSMutableString stringWithFormat:@"@@ -%@ +%@ @@\n", + coords1, coords2]; + // Escape the body of the patch with %xx notation. + for (Diff *aDiff in self.diffs) { + switch (aDiff.operation) { + case DIFF_INSERT: + [text appendString:@"+"]; + break; + case DIFF_DELETE: + [text appendString:@"-"]; + break; + case DIFF_EQUAL: + [text appendString:@" "]; + break; + } + + [text appendString:[aDiff.text diff_stringByAddingPercentEscapesForEncodeUriCompatibility]]; + [text appendString:@"\n"]; + } + + return text; +} + +@end + + +@implementation DiffMatchPatch + +@synthesize Diff_Timeout; +@synthesize Diff_EditCost; +@synthesize Match_Threshold; +@synthesize Match_Distance; +@synthesize Patch_DeleteThreshold; +@synthesize Patch_Margin; + +- (id)init +{ + self = [super init]; + + if (self) { + Diff_Timeout = 1.0f; + Diff_EditCost = 4; + Match_Threshold = 0.5f; + Match_Distance = 1000; + Patch_DeleteThreshold = 0.5f; + Patch_Margin = 4; + + Match_MaxBits = 32; + } + + return self; +} + +- (void)dealloc +{ + [super dealloc]; +} + + +#pragma mark Diff Functions +// DIFF FUNCTIONS + + +/** + * Find the differences between two texts. + * Run a faster, slightly less optimal diff. + * This method allows the 'checklines' of diff_main() to be optional. + * Most of the time checklines is wanted, so default to YES. + * @param text1 Old NSString to be diffed. + * @param text2 New NSString to be diffed. + * @return NSMutableArray of Diff objects. + */ +- (NSMutableArray *)diff_mainOfOldString:(NSString *)text1 + andNewString:(NSString *)text2; +{ + return [self diff_mainOfOldString:text1 andNewString:text2 checkLines:YES]; +} + +/** + * Find the differences between two texts. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If NO, then don't run a + * line-level diff first to identify the changed areas. + * If YES, then run a faster slightly less optimal diff. + * @return NSMutableArray of Diff objects. + */ +- (NSMutableArray *)diff_mainOfOldString:(NSString *)text1 + andNewString:(NSString *)text2 + checkLines:(BOOL)checklines; +{ + // Set a deadline by which time the diff must be complete. + NSTimeInterval deadline; + if (Diff_Timeout <= 0) { + deadline = [[NSDate distantFuture] timeIntervalSinceReferenceDate]; + } else { + deadline = [[NSDate dateWithTimeIntervalSinceNow:Diff_Timeout] timeIntervalSinceReferenceDate]; + } + return [self diff_mainOfOldString:text1 andNewString:text2 checkLines:YES deadline:deadline]; +} + +/** + * Find the differences between two texts. Simplifies the problem by + * stripping any common prefix or suffix off the texts before diffing. + * @param text1 Old NSString to be diffed. + * @param text2 New NSString to be diffed. + * @param checklines Speedup flag. If NO, then don't run a + * line-level diff first to identify the changed areas. + * If YES, then run a faster slightly less optimal diff + * @param deadline Time when the diff should be complete by. Used + * internally for recursive calls. Users should set DiffTimeout + * instead. + * @return NSMutableArray of Diff objects. + */ +- (NSMutableArray *)diff_mainOfOldString:(NSString *)text1 + andNewString:(NSString *)text2 + checkLines:(BOOL)checklines + deadline:(NSTimeInterval)deadline; +{ + // Check for null inputs. + if (text1 == nil || text2 == nil) { + NSLog(@"Null inputs. (diff_main)"); + return nil; + } + + // Check for equality (speedup). + NSMutableArray *diffs; + if ([text1 isEqualToString:text2]) { + diffs = [NSMutableArray array]; + if (text1.length != 0) { + [diffs addObject:[Diff diffWithOperation:DIFF_EQUAL andText:text1]]; + } + return diffs; + } + + // Trim off common prefix (speedup). + NSUInteger commonlength = (NSUInteger)diff_commonPrefix((CFStringRef)text1, (CFStringRef)text2); + NSString *commonprefix = [text1 substringWithRange:NSMakeRange(0, commonlength)]; + text1 = [text1 substringFromIndex:commonlength]; + text2 = [text2 substringFromIndex:commonlength]; + + // Trim off common suffix (speedup). + commonlength = (NSUInteger)diff_commonSuffix((CFStringRef)text1, (CFStringRef)text2); + NSString *commonsuffix = [text1 substringFromIndex:text1.length - commonlength]; + text1 = [text1 substringWithRange:NSMakeRange(0, text1.length - commonlength)]; + text2 = [text2 substringWithRange:NSMakeRange(0, text2.length - commonlength)]; + + // Compute the diff on the middle block. + diffs = [self diff_computeFromOldString:text1 andNewString:text2 checkLines:checklines deadline:deadline]; + + // Restore the prefix and suffix. + if (commonprefix.length != 0) { + [diffs insertObject:[Diff diffWithOperation:DIFF_EQUAL andText:commonprefix] atIndex:0]; + } + if (commonsuffix.length != 0) { + [diffs addObject:[Diff diffWithOperation:DIFF_EQUAL andText:commonsuffix]]; + } + + [self diff_cleanupMerge:diffs]; + return diffs; +} + +/** + * Determine the common prefix of two strings. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the start of each string. + */ +- (NSUInteger)diff_commonPrefixOfFirstString:(NSString *)text1 + andSecondString:(NSString *)text2; +{ + return (NSUInteger)diff_commonPrefix((CFStringRef)text1, (CFStringRef)text2); +} + +/** + * Determine the common suffix of two strings. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of each string. + */ +- (NSUInteger)diff_commonSuffixOfFirstString:(NSString *)text1 + andSecondString:(NSString *)text2; +{ + return (NSUInteger)diff_commonSuffix((CFStringRef)text1, (CFStringRef)text2); +} + +/** + * Determine if the suffix of one CFStringRef is the prefix of another. + * @param text1 First NSString. + * @param text2 Second NSString. + * @return The number of characters common to the end of the first + * CFStringRef and the start of the second CFStringRef. + */ +- (NSUInteger)diff_commonOverlapOfFirstString:(NSString *)text1 + andSecondString:(NSString *)text2; +{ + return (NSUInteger)diff_commonOverlap((CFStringRef)text1, (CFStringRef)text2); +} + +/** + * Do the two texts share a substring which is at least half the length of + * the longer text? + * This speedup can produce non-minimal diffs. + * @param text1 First NSString. + * @param text2 Second NSString. + * @return Five element String array, containing the prefix of text1, the + * suffix of text1, the prefix of text2, the suffix of text2 and the + * common middle. Or NULL if there was no match. + */ +- (NSArray *)diff_halfMatchOfFirstString:(NSString *)text1 + andSecondString:(NSString *)text2; +{ + return [(NSArray *)diff_halfMatchCreate((CFStringRef)text1, (CFStringRef)text2, Diff_Timeout) autorelease]; +} + +/** + * Does a substring of shorttext exist within longtext such that the + * substring is at least half the length of longtext? + * @param longtext Longer NSString. + * @param shorttext Shorter NSString. + * @param i Start index of quarter length substring within longtext. + * @return Five element NSArray, containing the prefix of longtext, the + * suffix of longtext, the prefix of shorttext, the suffix of shorttext + * and the common middle. Or nil if there was no match. + */ +- (NSArray *)diff_halfMatchIOfLongString:(NSString *)longtext + andShortString:(NSString *)shorttext + index:(NSInteger)index; +{ + return [((NSArray *)diff_halfMatchICreate((CFStringRef)longtext, (CFStringRef)shorttext, (CFIndex)index)) autorelease]; +} + +/** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param text1 Old NSString to be diffed. + * @param text2 New NSString to be diffed. + * @param checklines Speedup flag. If NO, then don't run a + * line-level diff first to identify the changed areas. + * If YES, then run a faster slightly less optimal diff. + * @param deadline Time the diff should be complete by. + * @return NSMutableArray of Diff objects. + */ +- (NSMutableArray *)diff_computeFromOldString:(NSString *)text1 + andNewString:(NSString *)text2 + checkLines:(BOOL)checklines + deadline:(NSTimeInterval)deadline; +{ + NSMutableArray *diffs = [NSMutableArray array]; + + if (text1.length == 0) { + // Just add some text (speedup). + [diffs addObject:[Diff diffWithOperation:DIFF_INSERT andText:text2]]; + return diffs; + } + + if (text2.length == 0) { + // Just delete some text (speedup). + [diffs addObject:[Diff diffWithOperation:DIFF_DELETE andText:text1]]; + return diffs; + } + + NSString *longtext = text1.length > text2.length ? text1 : text2; + NSString *shorttext = text1.length > text2.length ? text2 : text1; + NSUInteger i = [longtext rangeOfString:shorttext].location; + if (i != NSNotFound) { + // Shorter text is inside the longer text (speedup). + Operation op = (text1.length > text2.length) ? DIFF_DELETE : DIFF_INSERT; + [diffs addObject:[Diff diffWithOperation:op andText:[longtext substringWithRange:NSMakeRange(0, i)]]]; + [diffs addObject:[Diff diffWithOperation:DIFF_EQUAL andText:shorttext]]; + [diffs addObject:[Diff diffWithOperation:op andText:[longtext substringFromIndex:(i + shorttext.length)]]]; + return diffs; + } + + if (shorttext.length == 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + [diffs addObject:[Diff diffWithOperation:DIFF_DELETE andText:text1]]; + [diffs addObject:[Diff diffWithOperation:DIFF_INSERT andText:text2]]; + return diffs; + } + + // Check to see if the problem can be split in two. + NSArray *hm = [(NSArray *)diff_halfMatchCreate((CFStringRef)text1, (CFStringRef)text2, Diff_Timeout) autorelease]; + if (hm != nil) { + NSAutoreleasePool *splitPool = [NSAutoreleasePool new]; + // A half-match was found, sort out the return data. + NSString *text1_a = [hm objectAtIndex:0]; + NSString *text1_b = [hm objectAtIndex:1]; + NSString *text2_a = [hm objectAtIndex:2]; + NSString *text2_b = [hm objectAtIndex:3]; + NSString *mid_common = [hm objectAtIndex:4]; + // Send both pairs off for separate processing. + NSMutableArray *diffs_a = [self diff_mainOfOldString:text1_a andNewString:text2_a checkLines:checklines deadline:deadline]; + NSMutableArray *diffs_b = [self diff_mainOfOldString:text1_b andNewString:text2_b checkLines:checklines deadline:deadline]; + // Merge the results. + diffs = [diffs_a retain]; + [diffs addObject:[Diff diffWithOperation:DIFF_EQUAL andText:mid_common]]; + [diffs addObjectsFromArray:diffs_b]; + [splitPool drain]; + return [diffs autorelease]; + } + + if (checklines && text1.length > 100 && text2.length > 100) { + return [self diff_lineModeFromOldString:text1 andNewString:text2 deadline:deadline]; + } + + NSAutoreleasePool *bisectPool = [NSAutoreleasePool new]; + diffs = [self diff_bisectOfOldString:text1 andNewString:text2 deadline:deadline]; + [diffs retain]; + [bisectPool drain]; + + return [diffs autorelease]; +} + +/** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param text1 Old NSString to be diffed. + * @param text2 New NSString to be diffed. + * @param deadline Time when the diff should be complete by. + * @return NSMutableArray of Diff objects. + */ +- (NSMutableArray *)diff_lineModeFromOldString:(NSString *)text1 + andNewString:(NSString *)text2 + deadline:(NSTimeInterval)deadline; +{ + // Scan the text on a line-by-line basis first. + NSArray *b = [self diff_linesToCharsForFirstString:text1 andSecondString:text2]; + text1 = (NSString *)[b objectAtIndex:0]; + text2 = (NSString *)[b objectAtIndex:1]; + NSMutableArray *linearray = (NSMutableArray *)[b objectAtIndex:2]; + + NSAutoreleasePool *recursePool = [NSAutoreleasePool new]; + NSMutableArray *diffs = [self diff_mainOfOldString:text1 andNewString:text2 checkLines:NO deadline:deadline]; + [diffs retain]; + [recursePool drain]; + + [diffs autorelease]; + + // Convert the diff back to original text. + [self diff_chars:diffs toLines:linearray]; + // Eliminate freak matches (e.g. blank lines) + [self diff_cleanupSemantic:diffs]; + + // Rediff any Replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + [diffs addObject:[Diff diffWithOperation:DIFF_EQUAL andText:@""]]; + NSUInteger thisPointer = 0; + NSUInteger count_delete = 0; + NSUInteger count_insert = 0; + NSString *text_delete = @""; + NSString *text_insert = @""; + while (thisPointer < diffs.count) { + switch (((Diff *)[diffs objectAtIndex:thisPointer]).operation) { + case DIFF_INSERT: + count_insert++; + text_insert = [text_insert stringByAppendingString:((Diff *)[diffs objectAtIndex:thisPointer]).text]; + break; + case DIFF_DELETE: + count_delete++; + text_delete = [text_delete stringByAppendingString:((Diff *)[diffs objectAtIndex:thisPointer]).text]; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + NSMutableArray *a = [self diff_mainOfOldString:text_delete andNewString:text_insert checkLines:NO deadline:deadline]; + [diffs removeObjectsInRange:NSMakeRange(thisPointer - count_delete - count_insert, + count_delete + count_insert)]; + thisPointer = thisPointer - count_delete - count_insert; + NSUInteger insertionIndex = thisPointer; + for (Diff *thisDiff in a) { + [diffs insertObject:thisDiff atIndex:insertionIndex]; + insertionIndex++; + } + thisPointer = thisPointer + a.count; + } + count_insert = 0; + count_delete = 0; + text_delete = @""; + text_insert = @""; + break; + } + thisPointer++; + } + [diffs removeLastObject]; // Remove the dummy entry at the end. + + return diffs; +} + +/** + * Split a text into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text NSString to encode. + * @param lineArray NSMutableArray of unique strings. + * @param lineHash Map of strings to indices. + * @return Encoded string. + */ +- (NSString *)diff_linesToCharsMungeOfText:(NSString *)text + lineArray:(NSMutableArray *)lineArray + lineHash:(NSMutableDictionary *)lineHash; +{ + return [((NSString *)diff_linesToCharsMungeCFStringCreate((CFStringRef)text, + (CFMutableArrayRef)lineArray, + (CFMutableDictionaryRef)lineHash)) autorelease]; +} + +/** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time at which to bail if not yet complete. + * @return NSMutableArray of Diff objects. + */ +- (NSMutableArray *)diff_bisectOfOldString:(NSString *)_text1 + andNewString:(NSString *)_text2 + deadline:(NSTimeInterval)deadline; +{ +#define text1CharacterAtIndex(A) text1_chars[(A)] +#define text2CharacterAtIndex(A) text2_chars[(A)] +#define freeTextBuffers() if (text1_buffer != NULL) free(text1_buffer);\ + if (text2_buffer != NULL) free(text2_buffer); + + CFStringRef text1 = (CFStringRef)_text1; + CFStringRef text2 = (CFStringRef)_text2; + + // Cache the text lengths to prevent multiple calls. + CFIndex text1_length = CFStringGetLength(text1); + CFIndex text2_length = CFStringGetLength(text2); + CFIndex max_d = (text1_length + text2_length + 1) / 2; + CFIndex v_offset = max_d; + CFIndex v_length = 2 * max_d; + CFIndex v1[v_length]; + CFIndex v2[v_length]; + for (CFIndex x = 0; x < v_length; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[v_offset + 1] = 0; + v2[v_offset + 1] = 0; + CFIndex delta = text1_length - text2_length; + + // Prepare access to chars arrays for text1 (massive speedup). + const UniChar *text1_chars; + UniChar *text1_buffer = NULL; + diff_CFStringPrepareUniCharBuffer(text1, &text1_chars, &text1_buffer, CFRangeMake(0, text1_length)); + + // Prepare access to chars arrays for text 2 (massive speedup). + const UniChar *text2_chars; + UniChar *text2_buffer = NULL; + diff_CFStringPrepareUniCharBuffer(text2, &text2_chars, &text2_buffer, CFRangeMake(0, text2_length)); + + // If the total number of characters is odd, then the front path will + // collide with the reverse path. + BOOL front = (delta % 2 != 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + CFIndex k1start = 0; + CFIndex k1end = 0; + CFIndex k2start = 0; + CFIndex k2end = 0; + NSMutableArray *diffs; + for (CFIndex d = 0; d < max_d; d++) { + // Bail out if deadline is reached. + if ([NSDate timeIntervalSinceReferenceDate] > deadline) { + break; + } + + // Walk the front path one step. + for (CFIndex k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + CFIndex k1_offset = v_offset + k1; + CFIndex x1; + if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { + x1 = v1[k1_offset + 1]; + } else { + x1 = v1[k1_offset - 1] + 1; + } + CFIndex y1 = x1 - k1; + while (x1 < text1_length && y1 < text2_length + && text1CharacterAtIndex(x1) == text2CharacterAtIndex(y1)) { + x1++; + y1++; + } + v1[k1_offset] = x1; + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + CFIndex k2_offset = v_offset + delta - k1; + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { + // Mirror x2 onto top-left coordinate system. + CFIndex x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + freeTextBuffers(); + + // Overlap detected. + return [self diff_bisectSplitOfOldString:_text1 + andNewString:_text2 + x:x1 + y:y1 + deadline:deadline]; + } + } + } + } + + // Walk the reverse path one step. + for (CFIndex k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + CFIndex k2_offset = v_offset + k2; + CFIndex x2; + if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { + x2 = v2[k2_offset + 1]; + } else { + x2 = v2[k2_offset - 1] + 1; + } + CFIndex y2 = x2 - k2; + while (x2 < text1_length && y2 < text2_length + && text1CharacterAtIndex(text1_length - x2 - 1) + == text2CharacterAtIndex(text2_length - y2 - 1)) { + x2++; + y2++; + } + v2[k2_offset] = x2; + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + CFIndex k1_offset = v_offset + delta - k2; + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { + CFIndex x1 = v1[k1_offset]; + CFIndex y1 = v_offset + x1 - k1_offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2; + if (x1 >= x2) { + // Overlap detected. + freeTextBuffers(); + + return [self diff_bisectSplitOfOldString:_text1 + andNewString:_text2 + x:x1 + y:y1 + deadline:deadline]; + } + } + } + } + } + + freeTextBuffers(); + + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + diffs = [NSMutableArray array]; + [diffs addObject:[Diff diffWithOperation:DIFF_DELETE andText:_text1]]; + [diffs addObject:[Diff diffWithOperation:DIFF_INSERT andText:_text2]]; + return diffs; + +#undef freeTextBuffers +#undef text1CharacterAtIndex +#undef text2CharacterAtIndex +} + +/** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param x Index of split point in text1. + * @param y Index of split point in text2. + * @param deadline Time at which to bail if not yet complete. + * @return NSMutableArray of Diff objects. + */ +- (NSMutableArray *)diff_bisectSplitOfOldString:(NSString *)text1 + andNewString:(NSString *)text2 + x:(NSUInteger)x + y:(NSUInteger)y + deadline:(NSTimeInterval)deadline; +{ + NSString *text1a = [text1 substringToIndex:x]; + NSString *text2a = [text2 substringToIndex:y]; + NSString *text1b = [text1 substringFromIndex:x]; + NSString *text2b = [text2 substringFromIndex:y]; + + // Compute both diffs serially. + NSMutableArray *diffs = [self diff_mainOfOldString:text1a + andNewString:text2a + checkLines:NO + deadline:deadline]; + NSMutableArray *diffsb = [self diff_mainOfOldString:text1b + andNewString:text2b + checkLines:NO + deadline:deadline]; + + [diffs addObjectsFromArray: diffsb]; + return diffs; +} + +/** + * Split two texts into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text1 First NSString. + * @param text2 Second NSString. + * @return Three element NSArray, containing the encoded text1, the + * encoded text2 and the NSMutableArray of unique strings. The zeroth element + * of the NSArray of unique strings is intentionally blank. + */ +- (NSArray *)diff_linesToCharsForFirstString:(NSString *)text1 + andSecondString:(NSString *)text2; +{ + NSMutableArray *lineArray = [NSMutableArray array]; // NSString objects + NSMutableDictionary *lineHash = [NSMutableDictionary dictionary]; // keys: NSString, values:NSNumber + // e.g. [lineArray objectAtIndex:4] == "Hello\n" + // e.g. [lineHash objectForKey:"Hello\n"] == 4 + + // "\x00" is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a nil character. + [lineArray addObject:@""]; + + NSString *chars1 = (NSString *)diff_linesToCharsMungeCFStringCreate((CFStringRef)text1, + (CFMutableArrayRef)lineArray, + (CFMutableDictionaryRef)lineHash); + NSString *chars2 = (NSString *)diff_linesToCharsMungeCFStringCreate((CFStringRef)text2, + (CFMutableArrayRef)lineArray, + (CFMutableDictionaryRef)lineHash); + + NSArray *result = [NSArray arrayWithObjects:chars1, chars2, lineArray, nil]; + + [chars1 release]; + [chars2 release]; + + return result; +} + +/** + * Rehydrate the text in a diff from an NSString of line hashes to real lines + * of text. + * @param NSArray of Diff objects. + * @param NSMutableArray of unique strings. + */ +- (void)diff_chars:(NSArray *)diffs toLines:(NSMutableArray *)lineArray; +{ + NSMutableString *text; + NSUInteger lineHash; + for (Diff *diff in diffs) { + text = [NSMutableString string]; + for (NSUInteger y = 0; y < [diff.text length]; y++) { + lineHash = (NSUInteger)[diff.text characterAtIndex:y]; + [text appendString:[lineArray objectAtIndex:lineHash]]; + } + diff.text = text; + } +} + +/** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param diffs NSMutableArray of Diff objects. + */ +- (void)diff_cleanupMerge:(NSMutableArray *)diffs; +{ +#define prevDiff ((Diff *)[diffs objectAtIndex:(thisPointer - 1)]) +#define thisDiff ((Diff *)[diffs objectAtIndex:thisPointer]) +#define nextDiff ((Diff *)[diffs objectAtIndex:(thisPointer + 1)]) + + if (diffs.count == 0) { + return; + } + + // Add a dummy entry at the end. + [diffs addObject:[Diff diffWithOperation:DIFF_EQUAL andText:@""]]; + NSUInteger thisPointer = 0; + NSUInteger count_delete = 0; + NSUInteger count_insert = 0; + NSString *text_delete = @""; + NSString *text_insert = @""; + NSUInteger commonlength; + while (thisPointer < diffs.count) { + switch (thisDiff.operation) { + case DIFF_INSERT: + count_insert++; + text_insert = [text_insert stringByAppendingString:thisDiff.text]; + thisPointer++; + break; + case DIFF_DELETE: + count_delete++; + text_delete = [text_delete stringByAppendingString:thisDiff.text]; + thisPointer++; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete + count_insert > 1) { + if (count_delete != 0 && count_insert != 0) { + // Factor out any common prefixes. + commonlength = (NSUInteger)diff_commonPrefix((CFStringRef)text_insert, (CFStringRef)text_delete); + if (commonlength != 0) { + if ((thisPointer - count_delete - count_insert) > 0 && + ((Diff *)[diffs objectAtIndex:(thisPointer - count_delete - count_insert - 1)]).operation + == DIFF_EQUAL) { + ((Diff *)[diffs objectAtIndex:(thisPointer - count_delete - count_insert - 1)]).text + = [((Diff *)[diffs objectAtIndex:(thisPointer - count_delete - count_insert - 1)]).text + stringByAppendingString:[text_insert substringWithRange:NSMakeRange(0, commonlength)]]; + } else { + [diffs insertObject:[Diff diffWithOperation:DIFF_EQUAL + andText:[text_insert substringWithRange:NSMakeRange(0, commonlength)]] + atIndex:0]; + thisPointer++; + } + text_insert = [text_insert substringFromIndex:commonlength]; + text_delete = [text_delete substringFromIndex:commonlength]; + } + // Factor out any common suffixes. + commonlength = (NSUInteger)diff_commonSuffix((CFStringRef)text_insert, (CFStringRef)text_delete); + if (commonlength != 0) { + thisDiff.text = [[text_insert substringFromIndex:(text_insert.length + - commonlength)] stringByAppendingString:thisDiff.text]; + text_insert = [text_insert substringWithRange:NSMakeRange(0, + text_insert.length - commonlength)]; + text_delete = [text_delete substringWithRange:NSMakeRange(0, + text_delete.length - commonlength)]; + } + } + // Delete the offending records and add the merged ones. + if (count_delete == 0) { + splice(diffs, thisPointer - count_insert, + count_delete + count_insert, + [NSMutableArray arrayWithObject:[Diff diffWithOperation:DIFF_INSERT andText:text_insert]]); + } else if (count_insert == 0) { + splice(diffs, thisPointer - count_delete, + count_delete + count_insert, + [NSMutableArray arrayWithObject:[Diff diffWithOperation:DIFF_DELETE andText:text_delete]]); + } else { + splice(diffs, thisPointer - count_delete - count_insert, + count_delete + count_insert, + [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_DELETE andText:text_delete], + [Diff diffWithOperation:DIFF_INSERT andText:text_insert], nil]); + } + thisPointer = thisPointer - count_delete - count_insert + + (count_delete != 0 ? 1 : 0) + (count_insert != 0 ? 1 : 0) + 1; + } else if (thisPointer != 0 && prevDiff.operation == DIFF_EQUAL) { + // Merge this equality with the previous one. + prevDiff.text = [prevDiff.text stringByAppendingString:thisDiff.text]; + [diffs removeObjectAtIndex:thisPointer]; + } else { + thisPointer++; + } + count_insert = 0; + count_delete = 0; + text_delete = @""; + text_insert = @""; + break; + } + } + if (((Diff *)diffs.lastObject).text.length == 0) { + [diffs removeLastObject]; // Remove the dummy entry at the end. + } + + // Second pass: look for single edits surrounded on both sides by + // equalities which can be shifted sideways to eliminate an equality. + // e.g: ABAC -> ABAC + BOOL changes = NO; + thisPointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (thisPointer < (diffs.count - 1)) { + if (prevDiff.operation == DIFF_EQUAL && + nextDiff.operation == DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + if ([thisDiff.text hasSuffix:prevDiff.text]) { + // Shift the edit over the previous equality. + thisDiff.text = [prevDiff.text stringByAppendingString: + [thisDiff.text substringWithRange:NSMakeRange(0, thisDiff.text.length - prevDiff.text.length)]]; + nextDiff.text = [prevDiff.text stringByAppendingString:nextDiff.text]; + splice(diffs, thisPointer - 1, 1, nil); + changes = YES; + } else if ([thisDiff.text hasPrefix:nextDiff.text]) { + // Shift the edit over the next equality. + prevDiff.text = [prevDiff.text stringByAppendingString:nextDiff.text]; + thisDiff.text = [[thisDiff.text substringFromIndex:nextDiff.text.length] stringByAppendingString:nextDiff.text]; + splice(diffs, thisPointer + 1, 1, nil); + changes = YES; + } + } + thisPointer++; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + [self diff_cleanupMerge:diffs]; + } + +#undef prevDiff +#undef thisDiff +#undef nextDiff +} + + +/** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The cat came. -> The cat came. + * @param diffs NSMutableArray of Diff objects. + */ +- (void)diff_cleanupSemanticLossless:(NSMutableArray *)diffs; +{ +#define prevDiff ((Diff *)[diffs objectAtIndex:(thisPointer - 1)]) +#define thisDiff ((Diff *)[diffs objectAtIndex:thisPointer]) +#define nextDiff ((Diff *)[diffs objectAtIndex:(thisPointer + 1)]) + + if (diffs.count == 0) { + return; + } + + NSUInteger thisPointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (thisPointer < (diffs.count - 1)) { + if (prevDiff.operation == DIFF_EQUAL && nextDiff.operation == DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + NSString *equality1 = prevDiff.text; + NSString *edit = thisDiff.text; + NSString *equality2 = nextDiff.text; + + // First, shift the edit as far left as possible. + NSUInteger commonOffset = (NSUInteger)diff_commonSuffix((CFStringRef)equality1, (CFStringRef)edit); + + if (commonOffset > 0) { + NSString *commonString = [edit substringFromIndex:(edit.length - commonOffset)]; + equality1 = [equality1 substringWithRange:NSMakeRange(0, (equality1.length - commonOffset))]; + edit = [commonString stringByAppendingString:[edit substringWithRange:NSMakeRange(0, (edit.length - commonOffset))]]; + equality2 = [commonString stringByAppendingString:equality2]; + } + + // Second, step right character by character, + // looking for the best fit. + NSString *bestEquality1 = equality1; + NSString *bestEdit = edit; + NSString *bestEquality2 = equality2; + CFIndex bestScore = diff_cleanupSemanticScore((CFStringRef)equality1, (CFStringRef)edit) + + diff_cleanupSemanticScore((CFStringRef)edit, (CFStringRef)equality2); + while (edit.length != 0 && equality2.length != 0 + && [edit characterAtIndex:0] == [equality2 characterAtIndex:0]) { + equality1 = [equality1 stringByAppendingString:[edit substringWithRange:NSMakeRange(0, 1)]]; + edit = [[edit substringFromIndex:1] stringByAppendingString:[equality2 substringWithRange:NSMakeRange(0, 1)]]; + equality2 = [equality2 substringFromIndex:1]; + CFIndex score = diff_cleanupSemanticScore((CFStringRef)equality1, (CFStringRef)edit) + + diff_cleanupSemanticScore((CFStringRef)edit, (CFStringRef)equality2); + // The >= encourages trailing rather than leading whitespace on edits. + if (score >= bestScore) { + bestScore = score; + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + } + } + + if (prevDiff.text != bestEquality1) { + // We have an improvement, save it back to the diff. + if (bestEquality1.length != 0) { + prevDiff.text = bestEquality1; + } else { + [diffs removeObjectAtIndex:thisPointer - 1]; + thisPointer--; + } + thisDiff.text = bestEdit; + if (bestEquality2.length != 0) { + nextDiff.text = bestEquality2; + } else { + [diffs removeObjectAtIndex:thisPointer + 1]; + thisPointer--; + } + } + } + thisPointer++; + } + +#undef prevDiff +#undef thisDiff +#undef nextDiff +} + +/** + * Given two strings, comAdde a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 5 (best) to 0 (worst). + * @param one First string. + * @param two Second string. + * @return The score. + */ +- (NSInteger)diff_cleanupSemanticScoreOfFirstString:(NSString *)one + andSecondString:(NSString *)two; +{ + return diff_cleanupSemanticScore((CFStringRef)one, (CFStringRef)two); +} + +/** + * Reduce the number of edits by eliminating operationally trivial + * equalities. + * @param diffs NSMutableArray of Diff objects. + */ +- (void)diff_cleanupEfficiency:(NSMutableArray *)diffs; +{ +#define thisDiff ((Diff *)[diffs objectAtIndex:thisPointer]) +#define equalitiesLastItem ((NSNumber *)equalities.lastObject) +#define equalitiesLastValue ((NSNumber *)equalities.lastObject).integerValue + if (diffs.count == 0) { + return; + } + + BOOL changes = NO; + // Stack of indices where equalities are found. + NSMutableArray *equalities = [NSMutableArray array]; + // Always equal to equalities.lastObject.text + NSString *lastequality = nil; + NSInteger thisPointer = 0; // Index of current position. + // Is there an insertion operation before the last equality. + BOOL pre_ins = NO; + // Is there a deletion operation before the last equality. + BOOL pre_del = NO; + // Is there an insertion operation after the last equality. + BOOL post_ins = NO; + // Is there a deletion operation after the last equality. + BOOL post_del = NO; + + NSUInteger indexToChange; + Diff *diffToChange; + + while (thisPointer < (NSInteger)diffs.count) { + if (thisDiff.operation == DIFF_EQUAL) { // Equality found. + if (thisDiff.text.length < Diff_EditCost && (post_ins || post_del)) { + // Candidate found. + [equalities addObject:[NSNumber numberWithInteger:thisPointer]]; + pre_ins = post_ins; + pre_del = post_del; + lastequality = thisDiff.text; + } else { + // Not a candidate, and can never become one. + [equalities removeAllObjects]; + lastequality = nil; + } + post_ins = post_del = NO; + } else { // An insertion or deletion. + if (thisDiff.operation == DIFF_DELETE) { + post_del = YES; + } else { + post_ins = YES; + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if (lastequality != nil + && ((pre_ins && pre_del && post_ins && post_del) + || ((lastequality.length < Diff_EditCost / 2) + && ((pre_ins ? 1 : 0) + (pre_del ? 1 : 0) + (post_ins ? 1 : 0) + + (post_del ? 1 : 0)) == 3))) { + // Duplicate record. + [diffs insertObject:[Diff diffWithOperation:DIFF_DELETE andText:lastequality] + atIndex:equalitiesLastValue]; + // Change second copy to insert. + // Hash values for objects must not change while in a collection + indexToChange = equalitiesLastValue + 1; + diffToChange = [[diffs objectAtIndex:indexToChange] retain]; + [diffs replaceObjectAtIndex:indexToChange withObject:[NSNull null]]; + diffToChange.operation = DIFF_INSERT; + [diffs replaceObjectAtIndex:indexToChange withObject:diffToChange]; + [diffToChange release]; + + [equalities removeLastObject]; // Throw away the equality we just deleted. + lastequality = nil; + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_ins = post_del = YES; + [equalities removeAllObjects]; + } else { + if (equalities.count > 0) { + [equalities removeLastObject]; + } + + thisPointer = equalities.count > 0 ? equalitiesLastValue : -1; + post_ins = post_del = NO; + } + changes = YES; + } + } + thisPointer++; + } + + if (changes) { + [self diff_cleanupMerge:diffs]; + } + +#undef thisDiff +#undef equalitiesLastItem +#undef equalitiesLastValue +} + +/** + * Convert a Diff list into a pretty HTML report. + * @param diffs NSMutableArray of Diff objects. + * @return HTML representation. + */ +- (NSString *)diff_prettyHtml:(NSMutableArray *)diffs; +{ + NSMutableString *html = [NSMutableString string]; + for (Diff *aDiff in diffs) { + NSMutableString *text = [[aDiff.text mutableCopy] autorelease]; + [text replaceOccurrencesOfString:@"&" withString:@"&" options:NSLiteralSearch range:NSMakeRange(0, text.length)]; + [text replaceOccurrencesOfString:@"<" withString:@"<" options:NSLiteralSearch range:NSMakeRange(0, text.length)]; + [text replaceOccurrencesOfString:@">" withString:@">" options:NSLiteralSearch range:NSMakeRange(0, text.length)]; + [text replaceOccurrencesOfString:@"\n" withString:@"¶
    " options:NSLiteralSearch range:NSMakeRange(0, text.length)]; + + switch (aDiff.operation) { + case DIFF_INSERT: + [html appendFormat:@"%@", text]; + break; + case DIFF_DELETE: + [html appendFormat:@"%@", text]; + break; + case DIFF_EQUAL: + [html appendFormat:@"%@", text]; + break; + } + } + return html; +} + +/** + * Compute and return the source text (all equalities and deletions). + * @param diffs NSMutableArray of Diff objects. + * @return Source text. + */ +- (NSString *)diff_text1:(NSMutableArray *)diffs; +{ + NSMutableString *text = [NSMutableString string]; + for (Diff *aDiff in diffs) { + if (aDiff.operation != DIFF_INSERT) { + [text appendString:aDiff.text]; + } + } + return text; +} + +/** + * Compute and return the destination text (all equalities and insertions). + * @param diffs NSMutableArray of Diff objects. + * @return Destination text. + */ +- (NSString *)diff_text2:(NSMutableArray *)diffs; +{ + NSMutableString *text = [NSMutableString string]; + for (Diff *aDiff in diffs) { + if (aDiff.operation != DIFF_DELETE) { + [text appendString:aDiff.text]; + } + } + return text; +} + +/** + * Crush the diff into an encoded NSString which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx + * notation. + * @param diffs NSMutableArray of Diff objects. + * @return Delta text. + */ +- (NSString *)diff_toDelta:(NSMutableArray *)diffs; +{ + NSMutableString *delta = [NSMutableString string]; + for (Diff *aDiff in diffs) { + switch (aDiff.operation) { + case DIFF_INSERT: + [delta appendFormat:@"+%@\t", [[aDiff.text diff_stringByAddingPercentEscapesForEncodeUriCompatibility] + stringByReplacingOccurrencesOfString:@"%20" withString:@" "]]; + break; + case DIFF_DELETE: + [delta appendFormat:@"-%" PRId32 "\t", (int32_t)aDiff.text.length]; + break; + case DIFF_EQUAL: + [delta appendFormat:@"=%" PRId32 "\t", (int32_t)aDiff.text.length]; + break; + } + } + + if (delta.length != 0) { + // Strip off trailing tab character. + return [delta substringWithRange:NSMakeRange(0, delta.length-1)]; + } + return delta; +} + +/** + * Given the original text1, and an encoded NSString which describes the + * operations required to transform text1 into text2, compute the full diff. + * @param text1 Source NSString for the diff. + * @param delta Delta text. + * @param error NSError if invalid input. + * @return NSMutableArray of Diff objects or nil if invalid. + */ +- (NSMutableArray *)diff_fromDeltaWithText:(NSString *)text1 + andDelta:(NSString *)delta + error:(NSError **)error; +{ + NSMutableArray *diffs = [NSMutableArray array]; + NSUInteger thisPointer = 0; // Cursor in text1 + NSArray *tokens = [delta componentsSeparatedByString:@"\t"]; + NSInteger n; + NSDictionary *errorDetail = nil; + for (NSString *token in tokens) { + if (token.length == 0) { + // Blank tokens are ok (from a trailing \t). + continue; + } + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + NSString *param = [token substringFromIndex:1]; + switch ([token characterAtIndex:0]) { + case '+': + param = [param diff_stringByReplacingPercentEscapesForEncodeUriCompatibility]; + if (param == nil) { + if (error != NULL) { + errorDetail = [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithFormat:NSLocalizedString(@"Invalid character in diff_fromDelta: %@", @"Error"), param], + NSLocalizedDescriptionKey, nil]; + *error = [NSError errorWithDomain:@"DiffMatchPatchErrorDomain" code:99 userInfo:errorDetail]; + } + return nil; + } + [diffs addObject:[Diff diffWithOperation:DIFF_INSERT andText:param]]; + break; + case '-': + // Fall through. + case '=': + n = [param integerValue]; + if (n == 0) { + if (error != NULL) { + errorDetail = [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithFormat:NSLocalizedString(@"Invalid number in diff_fromDelta: %@", @"Error"), param], + NSLocalizedDescriptionKey, nil]; + *error = [NSError errorWithDomain:@"DiffMatchPatchErrorDomain" code:100 userInfo:errorDetail]; + } + return nil; + } else if (n < 0) { + if (error != NULL) { + errorDetail = [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithFormat:NSLocalizedString(@"Negative number in diff_fromDelta: %@", @"Error"), param], + NSLocalizedDescriptionKey, nil]; + *error = [NSError errorWithDomain:@"DiffMatchPatchErrorDomain" code:101 userInfo:errorDetail]; + } + return nil; + } + NSString *text; + @try { + text = [text1 substringWithRange:NSMakeRange(thisPointer, (NSUInteger)n)]; + thisPointer += (NSUInteger)n; + } + @catch (NSException *e) { + if (error != NULL) { + // CHANGME: Pass on the information contained in e + errorDetail = [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithFormat:NSLocalizedString(@"Delta length (%lu) larger than source text length (%lu).", @"Error"), + (unsigned long)thisPointer, (unsigned long)text1.length], + NSLocalizedDescriptionKey, nil]; + *error = [NSError errorWithDomain:@"DiffMatchPatchErrorDomain" code:102 userInfo:errorDetail]; + } + return nil; + } + if ([token characterAtIndex:0] == '=') { + [diffs addObject:[Diff diffWithOperation:DIFF_EQUAL andText:text]]; + } else { + [diffs addObject:[Diff diffWithOperation:DIFF_DELETE andText:text]]; + } + break; + default: + // Anything else is an error. + if (error != NULL) { + errorDetail = [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithFormat:NSLocalizedString(@"Invalid diff operation in diff_fromDelta: %C", @"Error"), + [token characterAtIndex:0]], + NSLocalizedDescriptionKey, nil]; + *error = [NSError errorWithDomain:@"DiffMatchPatchErrorDomain" code:102 userInfo:errorDetail]; + } + return nil; + } + } + if (thisPointer != text1.length) { + if (error != NULL) { + errorDetail = [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithFormat:NSLocalizedString(@"Delta length (%lu) smaller than source text length (%lu).", @"Error"), + (unsigned long)thisPointer, (unsigned long)text1.length], + NSLocalizedDescriptionKey, nil]; + *error = [NSError errorWithDomain:@"DiffMatchPatchErrorDomain" code:103 userInfo:errorDetail]; + } + return nil; + } + return diffs; +} + +/** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. "The cat" vs "The big cat", 1->1, 5->8 + * @param diffs NSMutableArray of Diff objects. + * @param loc Location within text1. + * @return Location within text2. + */ +- (NSUInteger)diff_xIndexIn:(NSMutableArray *)diffs + location:(NSUInteger) loc; +{ + NSUInteger chars1 = 0; + NSUInteger chars2 = 0; + NSUInteger last_chars1 = 0; + NSUInteger last_chars2 = 0; + Diff *lastDiff = nil; + for (Diff *aDiff in diffs) { + if (aDiff.operation != DIFF_INSERT) { + // Equality or deletion. + chars1 += aDiff.text.length; + } + if (aDiff.operation != DIFF_DELETE) { + // Equality or insertion. + chars2 += aDiff.text.length; + } + if (chars1 > loc) { + // Overshot the location. + lastDiff = aDiff; + break; + } + last_chars1 = chars1; + last_chars2 = chars2; + } + if (lastDiff != nil && lastDiff.operation == DIFF_DELETE) { + // The location was deleted. + return last_chars2; + } + // Add the remaining character length. + return last_chars2 + (loc - last_chars1); +} + +/** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * @param diffs NSMutableArray of Diff objects. + * @return Number of changes. + */ +- (NSUInteger)diff_levenshtein:(NSMutableArray *)diffs; +{ + NSUInteger levenshtein = 0; + NSUInteger insertions = 0; + NSUInteger deletions = 0; + for (Diff *aDiff in diffs) { + switch (aDiff.operation) { + case DIFF_INSERT: + insertions += aDiff.text.length; + break; + case DIFF_DELETE: + deletions += aDiff.text.length; + break; + case DIFF_EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += MAX(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += MAX(insertions, deletions); + return levenshtein; +} + +/** + * Reduce the number of edits by eliminating semantically trivial + * equalities. + * @param diffs NSMutableArray of Diff objects. + */ +- (void)diff_cleanupSemantic:(NSMutableArray *)diffs; +{ +#define prevDiff ((Diff *)[diffs objectAtIndex:(thisPointer - 1)]) +#define thisDiff ((Diff *)[diffs objectAtIndex:thisPointer]) +#define nextDiff ((Diff *)[diffs objectAtIndex:(thisPointer + 1)]) +#define equalitiesLastItem ((NSNumber *)equalities.lastObject) +#define equalitiesLastValue ((NSNumber *)equalities.lastObject).integerValue + + if (diffs == nil || diffs.count == 0) { + return; + } + + BOOL changes = NO; + // Stack of indices where equalities are found. + NSMutableArray *equalities = [NSMutableArray array]; + // Always equal to equalities.lastObject.text + NSString *lastequality = nil; + NSUInteger thisPointer = 0; // Index of current position. + // Number of characters that changed prior to the equality. + NSUInteger length_insertions1 = 0; + NSUInteger length_deletions1 = 0; + // Number of characters that changed after the equality. + NSUInteger length_insertions2 = 0; + NSUInteger length_deletions2 = 0; + + NSUInteger indexToChange; + Diff *diffToChange; + + while (thisPointer < diffs.count) { + if (thisDiff.operation == DIFF_EQUAL) { // Equality found. + [equalities addObject:[NSNumber numberWithInteger:thisPointer]]; + length_insertions1 = length_insertions2; + length_deletions1 = length_deletions2; + length_insertions2 = 0; + length_deletions2 = 0; + lastequality = thisDiff.text; + } else { // an insertion or deletion + if (thisDiff.operation == DIFF_INSERT) { + length_insertions2 += thisDiff.text.length; + } else { + length_deletions2 += thisDiff.text.length; + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastequality != nil + && (lastequality.length <= MAX(length_insertions1, length_deletions1)) + && (lastequality.length <= MAX(length_insertions2, length_deletions2))) { + // Duplicate record. + [diffs insertObject:[Diff diffWithOperation:DIFF_DELETE andText:lastequality] atIndex:equalitiesLastValue]; + // Change second copy to insert. + // Hash values for objects must not change while in a collection. + indexToChange = equalitiesLastValue + 1; + diffToChange = [[diffs objectAtIndex:indexToChange] retain]; + [diffs replaceObjectAtIndex:indexToChange withObject:[NSNull null]]; + diffToChange.operation = DIFF_INSERT; + [diffs replaceObjectAtIndex:indexToChange withObject:diffToChange]; + [diffToChange release]; + + // Throw away the equality we just deleted. + [equalities removeLastObject]; + if (equalities.count > 0) { + [equalities removeLastObject]; + } + // Setting an unsigned value to -1 may seem weird to some, + // but we will pass thru a ++ below: + // => overflow => 0 + thisPointer = equalities.count > 0 ? equalitiesLastValue : -1; + length_insertions1 = 0; // Reset the counters. + length_deletions1 = 0; + length_insertions2 = 0; + length_deletions2 = 0; + lastequality = nil; + changes = YES; + } + } + thisPointer++; + } + + // Normalize the diff. + if (changes) { + [self diff_cleanupMerge:diffs]; + } + [self diff_cleanupSemanticLossless:diffs]; + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + thisPointer = 1; + while (thisPointer < diffs.count) { + if (prevDiff.operation == DIFF_DELETE && thisDiff.operation == DIFF_INSERT) { + NSString *deletion = prevDiff.text; + NSString *insertion = thisDiff.text; + NSUInteger overlap_length1 = (NSUInteger)diff_commonOverlap((CFStringRef)deletion, (CFStringRef)insertion); + NSUInteger overlap_length2 = (NSUInteger)diff_commonOverlap((CFStringRef)insertion, (CFStringRef)deletion); + if (overlap_length1 >= overlap_length2) { + if (overlap_length1 >= deletion.length / 2.0 || + overlap_length1 >= insertion.length / 2.0) { + // Overlap found. + // Insert an equality and trim the surrounding edits. + [diffs insertObject:[Diff diffWithOperation:DIFF_EQUAL + andText:[insertion substringWithRange:NSMakeRange(0, overlap_length1)]] + atIndex:thisPointer]; + prevDiff.text = [deletion substringWithRange:NSMakeRange(0, deletion.length - overlap_length1)]; + nextDiff.text = [insertion substringFromIndex:overlap_length1]; + thisPointer++; + } + } else { + if (overlap_length2 >= deletion.length / 2.0 || + overlap_length2 >= insertion.length / 2.0) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + [diffs insertObject:[Diff diffWithOperation:DIFF_EQUAL + andText:[deletion substringWithRange:NSMakeRange(0, overlap_length2)]] + atIndex:thisPointer]; + prevDiff.operation = DIFF_INSERT; + prevDiff.text = [insertion substringWithRange:NSMakeRange(0, insertion.length - overlap_length2)]; + nextDiff.operation = DIFF_DELETE; + nextDiff.text = [deletion substringFromIndex:overlap_length2]; + thisPointer++; + } + } + thisPointer++; + } + thisPointer++; + } + +#undef prevDiff +#undef thisDiff +#undef nextDiff +#undef equalitiesLastItem +#undef equalitiesLastValue +} + +#pragma mark Match Functions +// MATCH FUNCTIONS + + +/** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * Returns NSNotFound if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or NSNotFound. + */ +- (NSUInteger)match_mainForText:(NSString *)text + pattern:(NSString *)pattern + near:(NSUInteger)loc; +{ + // Check for null inputs. + if (text == nil || pattern == nil) { + NSLog(@"Null inputs. (match_main)"); + return NSNotFound; + } + if (text.length == 0) { + NSLog(@"Empty text. (match_main)"); + return NSNotFound; + } + + NSUInteger new_loc; + new_loc = MIN(loc, text.length); + new_loc = MAX((NSUInteger)0, new_loc); + + if ([text isEqualToString:pattern]) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (text.length == 0) { + // Nothing to match. + return NSNotFound; + } else if (new_loc + pattern.length <= text.length + && [[text substringWithRange:NSMakeRange(new_loc, pattern.length)] isEqualToString:pattern]) { + // Perfect match at the perfect spot! (Includes case of empty pattern) + return new_loc; + } else { + // Do a fuzzy compare. + return [self match_bitapOfText:text andPattern:pattern near:new_loc]; + } +} + +/** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. Returns NSNotFound if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or NSNotFound. + */ +- (NSUInteger)match_bitapOfText:(NSString *)text + andPattern:(NSString *)pattern + near:(NSUInteger)loc; +{ + NSAssert((Match_MaxBits == 0 || pattern.length <= Match_MaxBits), + @"Pattern too long for this application."); + + // Initialise the alphabet. + NSMutableDictionary *s = [self match_alphabet:pattern]; + + // Highest score beyond which we give up. + double score_threshold = Match_Threshold; + // Is there a nearby exact match? (speedup) + NSUInteger best_loc = [text rangeOfString:pattern options:NSLiteralSearch range:NSMakeRange(loc, text.length - loc)].location; + if (best_loc != NSNotFound) { + score_threshold = MIN([self match_bitapScoreForErrorCount:0 location:best_loc near:loc pattern:pattern], score_threshold); + // What about in the other direction? (speedup) + NSUInteger searchRangeLoc = MIN(loc + pattern.length, text.length); + NSRange searchRange = NSMakeRange(0, searchRangeLoc); + best_loc = [text rangeOfString:pattern options:(NSLiteralSearch | NSBackwardsSearch) range:searchRange].location; + if (best_loc != NSNotFound) { + score_threshold = MIN([self match_bitapScoreForErrorCount:0 location:best_loc near:loc pattern:pattern], score_threshold); + } + } + + // Initialise the bit arrays. + NSUInteger matchmask = 1 << (pattern.length - 1); + best_loc = NSNotFound; + + NSUInteger bin_min, bin_mid; + NSUInteger bin_max = pattern.length + text.length; + NSUInteger *rd = NULL; + NSUInteger *last_rd = NULL; + for (NSUInteger d = 0; d < pattern.length; d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at + // this error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + double score = [self match_bitapScoreForErrorCount:d location:(loc + bin_mid) near:loc pattern:pattern]; + if (score <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = (bin_max - bin_min) / 2 + bin_min; + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + NSUInteger start = MAX_OF_CONST_AND_DIFF(1, loc, bin_mid); + NSUInteger finish = MIN(loc + bin_mid, text.length) + pattern.length; + + rd = (NSUInteger *)calloc((finish + 2), sizeof(NSUInteger)); + rd[finish + 1] = (1 << d) - 1; + + for (NSUInteger j = finish; j >= start; j--) { + NSUInteger charMatch; + if (text.length <= j - 1 || ![s diff_containsObjectForUnicharKey:[text characterAtIndex:(j - 1)]]) { + // Out of range. + charMatch = 0; + } else { + charMatch = [s diff_unsignedIntegerForUnicharKey:[text characterAtIndex:(j - 1)]]; + } + if (d == 0) { + // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { + // Subsequent passes: fuzzy match. + rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) + | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]; + } + if ((rd[j] & matchmask) != 0) { + double score = [self match_bitapScoreForErrorCount:d location:(j - 1) near:loc pattern:pattern]; + // This match will almost certainly be better than any existing match. + // But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = MAX_OF_CONST_AND_DIFF(1, 2 * loc, best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + if ([self match_bitapScoreForErrorCount:(d + 1) location:loc near:loc pattern:pattern] > score_threshold) { + // No hope for a (better) match at greater error levels. + break; + } + + if (last_rd != NULL) { + free(last_rd); + } + last_rd = rd; + } + + if (rd != NULL && last_rd != rd) { + free(rd); + } + if (last_rd != NULL) { + free(last_rd); + } + + return best_loc; +} + +/** + * Compute and return the score for a match with e errors and x location. + * @param e Number of errors in match. + * @param x Location of match. + * @param loc Expected location of match. + * @param pattern Pattern being sought. + * @return Overall score for match (0.0 = good, 1.0 = bad). + */ +- (double)match_bitapScoreForErrorCount:(NSUInteger)e + location:(NSUInteger)x + near:(NSUInteger)loc + pattern:(NSString *)pattern; +{ + double score; + + double accuracy = (double)e / pattern.length; + NSUInteger proximity = (NSUInteger)ABS((long long)loc - (long long)x); + if (Match_Distance == 0) { + // Dodge divide by zero error. + return proximity == 0 ? accuracy : 1.0; + } + score = accuracy + (proximity / (double) Match_Distance); + + return score; +} + +/** + * Initialise the alphabet for the Bitap algorithm. + * @param pattern The text to encode. + * @return Hash of character locations + * (NSMutableDictionary: keys:NSString/unichar, values:NSNumber/NSUInteger). + */ +- (NSMutableDictionary *)match_alphabet:(NSString *)pattern; +{ + NSMutableDictionary *s = [NSMutableDictionary dictionary]; + CFStringRef str = (CFStringRef)pattern; + CFStringInlineBuffer inlineBuffer; + CFIndex length; + CFIndex cnt; + + length = CFStringGetLength(str); + CFStringInitInlineBuffer(str, &inlineBuffer, CFRangeMake(0, length)); + + UniChar ch; + CFStringRef c; + for (cnt = 0; cnt < length; cnt++) { + ch = CFStringGetCharacterFromInlineBuffer(&inlineBuffer, cnt); + c = diff_CFStringCreateFromUnichar(ch); + if (![s diff_containsObjectForKey:(NSString *)c]) { + [s diff_setUnsignedIntegerValue:0 forKey:(NSString *)c]; + } + CFRelease(c); + } + + NSUInteger i = 0; + for (cnt = 0; cnt < length; cnt++) { + ch = CFStringGetCharacterFromInlineBuffer(&inlineBuffer, cnt); + c = diff_CFStringCreateFromUnichar(ch); + NSUInteger value = [s diff_unsignedIntegerForKey:(NSString *)c] | (1 << (pattern.length - i - 1)); + [s diff_setUnsignedIntegerValue:value forKey:(NSString *)c]; + i++; + CFRelease(c); + } + return s; +} + + +#pragma mark Patch Functions +// PATCH FUNCTIONS + + +/** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * @param patch The patch to grow. + * @param text Source text. + */ +- (void)patch_addContextToPatch:(Patch *)patch + sourceText:(NSString *)text; +{ + if (text.length == 0) { + return; + } + NSString *pattern = [text substringWithRange:NSMakeRange(patch.start2, patch.length1)]; + NSUInteger padding = 0; + + // Look for the first and last matches of pattern in text. If two + // different matches are found, increase the pattern length. + while ([text rangeOfString:pattern options:NSLiteralSearch].location + != [text rangeOfString:pattern options:(NSLiteralSearch | NSBackwardsSearch)].location + && pattern.length < (Match_MaxBits - Patch_Margin - Patch_Margin)) { + padding += Patch_Margin; + pattern = [text diff_javaSubstringFromStart:MAX_OF_CONST_AND_DIFF(0, patch.start2, padding) + toEnd:MIN(text.length, patch.start2 + patch.length1 + padding)]; + } + // Add one chunk for good luck. + padding += Patch_Margin; + + // Add the prefix. + NSString *prefix = [text diff_javaSubstringFromStart:MAX_OF_CONST_AND_DIFF(0, patch.start2, padding) + toEnd:patch.start2]; + if (prefix.length != 0) { + [patch.diffs insertObject:[Diff diffWithOperation:DIFF_EQUAL andText:prefix] atIndex:0]; + } + // Add the suffix. + NSString *suffix = [text diff_javaSubstringFromStart:(patch.start2 + patch.length1) + toEnd:MIN(text.length, patch.start2 + patch.length1 + padding)]; + if (suffix.length != 0) { + [patch.diffs addObject:[Diff diffWithOperation:DIFF_EQUAL andText:suffix]]; + } + + // Roll back the start points. + patch.start1 -= prefix.length; + patch.start2 -= prefix.length; + // Extend the lengths. + patch.length1 += prefix.length + suffix.length; + patch.length2 += prefix.length + suffix.length; +} + +/** + * Compute a list of patches to turn text1 into text2. + * A set of diffs will be computed. + * @param text1 Old text. + * @param text2 New text. + * @return NSMutableArray of Patch objects. + */ +- (NSMutableArray *)patch_makeFromOldString:(NSString *)text1 + andNewString:(NSString *)text2; +{ + // Check for null inputs. + if (text1 == nil || text2 == nil) { + NSLog(@"Null inputs. (patch_make)"); + return nil; + } + + // No diffs provided, compute our own. + NSMutableArray *diffs = [self diff_mainOfOldString:text1 andNewString:text2 checkLines:YES]; + if (diffs.count > 2) { + [self diff_cleanupSemantic:diffs]; + [self diff_cleanupEfficiency:diffs]; + } + + return [self patch_makeFromOldString:text1 andDiffs:diffs]; +} + +/** + * Compute a list of patches to turn text1 into text2. + * text1 will be derived from the provided diffs. + * @param diffs NSMutableArray of Diff objects for text1 to text2. + * @return NSMutableArray of Patch objects. + */ +- (NSMutableArray *)patch_makeFromDiffs:(NSMutableArray *)diffs; +{ + // Check for nil inputs not needed since nil can't be passed in C#. + // No origin NSString *provided, comAdde our own. + NSString *text1 = [self diff_text1:diffs]; + return [self patch_makeFromOldString:text1 andDiffs:diffs]; +} + +/** + * Compute a list of patches to turn text1 into text2. + * text2 is ignored, diffs are the delta between text1 and text2. + * @param text1 Old text + * @param text2 New text + * @param diffs NSMutableArray of Diff objects for text1 to text2. + * @return NSMutableArray of Patch objects. + * @deprecated Prefer -patch_makeFromOldString:diffs:. + */ +- (NSMutableArray *)patch_makeFromOldString:(NSString *)text1 + newString:(NSString *)text2 + diffs:(NSMutableArray *)diffs; +{ + // Check for null inputs. + if (text1 == nil || text2 == nil) { + NSLog(@"Null inputs. (patch_make)"); + return nil; + } + + return [self patch_makeFromOldString:text1 andDiffs:diffs]; +} + +/** + * Compute a list of patches to turn text1 into text2. + * text2 is not provided, diffs are the delta between text1 and text2. + * @param text1 Old text. + * @param diffs NSMutableArray of Diff objects for text1 to text2. + * @return NSMutableArray of Patch objects. + */ +- (NSMutableArray *)patch_makeFromOldString:(NSString *)text1 + andDiffs:(NSMutableArray *)diffs; +{ + // Check for null inputs. + if (text1 == nil) { + NSLog(@"Null inputs. (patch_make)"); + return nil; + } + + NSMutableArray *patches = [NSMutableArray array]; + if (diffs.count == 0) { + return patches; // Get rid of the nil case. + } + Patch *patch = [[Patch new] autorelease]; + NSUInteger char_count1 = 0; // Number of characters into the text1 NSString. + NSUInteger char_count2 = 0; // Number of characters into the text2 NSString. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + NSString *prepatch_text = [text1 retain]; + NSMutableString *postpatch_text = [text1 mutableCopy]; + for (Diff *aDiff in diffs) { + if (patch.diffs.count == 0 && aDiff.operation != DIFF_EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (aDiff.operation) { + case DIFF_INSERT: + [patch.diffs addObject:aDiff]; + patch.length2 += aDiff.text.length; + [postpatch_text insertString:aDiff.text atIndex:char_count2]; + break; + case DIFF_DELETE: + patch.length1 += aDiff.text.length; + [patch.diffs addObject:aDiff]; + [postpatch_text deleteCharactersInRange:NSMakeRange(char_count2, aDiff.text.length)]; + break; + case DIFF_EQUAL: + if (aDiff.text.length <= 2 * Patch_Margin + && [patch.diffs count] != 0 && aDiff != diffs.lastObject) { + // Small equality inside a patch. + [patch.diffs addObject:aDiff]; + patch.length1 += aDiff.text.length; + patch.length2 += aDiff.text.length; + } + + if (aDiff.text.length >= 2 * Patch_Margin) { + // Time for a new patch. + if (patch.diffs.count != 0) { + [self patch_addContextToPatch:patch sourceText:prepatch_text]; + [patches addObject:patch]; + patch = [[Patch new] autorelease]; + // Unlike Unidiff, our patch lists have a rolling context. + // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + [prepatch_text release]; + prepatch_text = [postpatch_text copy]; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (aDiff.operation != DIFF_INSERT) { + char_count1 += aDiff.text.length; + } + if (aDiff.operation != DIFF_DELETE) { + char_count2 += aDiff.text.length; + } + } + // Pick up the leftover patch if not empty. + if (patch.diffs.count != 0) { + [self patch_addContextToPatch:patch sourceText:prepatch_text]; + [patches addObject:patch]; + } + + [prepatch_text release]; + [postpatch_text release]; + + return patches; +} + +/** + * Given an array of patches, return another array that is identical. + * @param patches NSArray of Patch objects. + * @return NSMutableArray of Patch objects. + */ +- (NSMutableArray *)patch_deepCopy:(NSArray *)patches; +{ + NSMutableArray *patchesCopy = [[NSMutableArray alloc] initWithArray:patches copyItems:YES]; + return patchesCopy; +} + +/** + * Merge a set of patches onto the text. Return a patched text, as well + * as an array of YES/NO values indicating which patches were applied. + * @param patches NSMutableArray of Patch objects + * @param text Old text. + * @return Two element NSArray, containing the new text and an array of + * BOOL values. + */ +- (NSArray *)patch_apply:(NSArray *)sourcePatches + toString:(NSString *)text; +{ + if (sourcePatches.count == 0) { + return [NSArray arrayWithObjects:text, [NSMutableArray array], nil]; + } + + // Deep copy the patches so that no changes are made to originals. + NSMutableArray *patches = [self patch_deepCopy:sourcePatches]; + + NSMutableString *textMutable = [[text mutableCopy] autorelease]; + + NSString *nullPadding = [self patch_addPadding:patches]; + [textMutable insertString:nullPadding atIndex:0]; + [textMutable appendString:nullPadding]; + [self patch_splitMax:patches]; + + NSUInteger x = 0; + // delta keeps track of the offset between the expected and actual + // location of the previous patch. If there are patches expected at + // positions 10 and 20, but the first patch was found at 12, delta is 2 + // and the second patch has an effective expected position of 22. + NSUInteger delta = 0; + BOOL *results = (BOOL *)calloc(patches.count, sizeof(BOOL)); + for (Patch *aPatch in patches) { + NSUInteger expected_loc = aPatch.start2 + delta; + NSString *text1 = [self diff_text1:aPatch.diffs]; + NSUInteger start_loc; + NSUInteger end_loc = NSNotFound; + if (text1.length > Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern + // in the case of a monster delete. + start_loc = [self match_mainForText:textMutable + pattern:[text1 substringWithRange:NSMakeRange(0, Match_MaxBits)] + near:expected_loc]; + if (start_loc != NSNotFound) { + end_loc = [self match_mainForText:textMutable + pattern:[text1 substringFromIndex:text1.length - Match_MaxBits] + near:(expected_loc + text1.length - Match_MaxBits)]; + if (end_loc == NSNotFound || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = NSNotFound; + } + } + } else { + start_loc = [self match_mainForText:textMutable pattern:text1 near:expected_loc]; + } + if (start_loc == NSNotFound) { + // No match found. :( + results[x] = NO; + // Subtract the delta for this failed patch from subsequent patches. + delta -= aPatch.length2 - aPatch.length1; + } else { + // Found a match. :) + results[x] = YES; + delta = start_loc - expected_loc; + NSString *text2; + if (end_loc == NSNotFound) { + text2 = [textMutable diff_javaSubstringFromStart:start_loc + toEnd:MIN(start_loc + text1.length, textMutable.length)]; + } else { + text2 = [textMutable diff_javaSubstringFromStart:start_loc + toEnd:MIN(end_loc + Match_MaxBits, textMutable.length)]; + } + if (text1 == text2) { + // Perfect match, just shove the Replacement text in. + [textMutable replaceCharactersInRange:NSMakeRange(start_loc, text1.length) withString:[self diff_text2:aPatch.diffs]]; + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + NSMutableArray *diffs = [self diff_mainOfOldString:text1 andNewString:text2 checkLines:NO]; + if (text1.length > Match_MaxBits + && ([self diff_levenshtein:diffs] / (float)text1.length) + > Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = NO; + } else { + [self diff_cleanupSemanticLossless:diffs]; + NSUInteger index1 = 0; + for (Diff *aDiff in aPatch.diffs) { + if (aDiff.operation != DIFF_EQUAL) { + NSUInteger index2 = [self diff_xIndexIn:diffs location:index1]; + if (aDiff.operation == DIFF_INSERT) { + // Insertion + [textMutable insertString:aDiff.text atIndex:(start_loc + index2)]; + } else if (aDiff.operation == DIFF_DELETE) { + // Deletion + [textMutable deleteCharactersInRange:NSMakeRange(start_loc + index2, + ([self diff_xIndexIn:diffs + location:(index1 + aDiff.text.length)] - index2))]; + } + } + if (aDiff.operation != DIFF_DELETE) { + index1 += aDiff.text.length; + } + } + } + } + } + x++; + } + + NSMutableArray *resultsArray = [NSMutableArray arrayWithCapacity:patches.count]; + for (NSUInteger i = 0; i < patches.count; i++) { + [resultsArray addObject:[NSNumber numberWithBool:(results[i])]]; + } + + if (results != NULL) { + free(results); + } + + // Strip the padding off. + text = [textMutable substringWithRange:NSMakeRange(nullPadding.length, + textMutable.length - 2 * nullPadding.length)]; + [patches release]; + return [NSArray arrayWithObjects:text, resultsArray, nil]; +} + +/** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * @param patches NSMutableArray of Patch objects. + * @return The padding NSString added to each side. + */ +- (NSString *)patch_addPadding:(NSMutableArray *)patches; +{ + uint16_t paddingLength = Patch_Margin; + NSMutableString *nullPadding = [NSMutableString string]; + for (UniChar x = 1; x <= paddingLength; x++) { + CFStringAppendCharacters((CFMutableStringRef)nullPadding, &x, 1); + } + + // Bump all the patches forward. + for (Patch *aPatch in patches) { + aPatch.start1 += paddingLength; + aPatch.start2 += paddingLength; + } + + // Add some padding on start of first diff. + Patch *patch = [patches objectAtIndex:0]; + NSMutableArray *diffs = patch.diffs; + if (diffs.count == 0 || ((Diff *)[diffs objectAtIndex:0]).operation != DIFF_EQUAL) { + // Add nullPadding equality. + [diffs insertObject:[Diff diffWithOperation:DIFF_EQUAL andText:nullPadding] atIndex:0]; + patch.start1 -= paddingLength; // Should be 0. + patch.start2 -= paddingLength; // Should be 0. + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > ((Diff *)[diffs objectAtIndex:0]).text.length) { + // Grow first equality. + Diff *firstDiff = [diffs objectAtIndex:0]; + NSUInteger extraLength = paddingLength - firstDiff.text.length; + firstDiff.text = [[nullPadding substringFromIndex:(firstDiff.text.length)] + stringByAppendingString:firstDiff.text]; + patch.start1 -= extraLength; + patch.start2 -= extraLength; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + // Add some padding on end of last diff. + patch = patches.lastObject; + diffs = patch.diffs; + if (diffs.count == 0 || ((Diff *)diffs.lastObject).operation != DIFF_EQUAL) { + // Add nullPadding equality. + [diffs addObject:[Diff diffWithOperation:DIFF_EQUAL andText:nullPadding]]; + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > ((Diff *)diffs.lastObject).text.length) { + // Grow last equality. + Diff *lastDiff = diffs.lastObject; + NSUInteger extraLength = paddingLength - lastDiff.text.length; + lastDiff.text = [lastDiff.text stringByAppendingString:[nullPadding substringWithRange:NSMakeRange(0, extraLength)]]; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + return nullPadding; +} + +/** + * Look through the patches and break up any which are longer than the + * maximum limit of the match algorithm. + * Intended to be called only from within patch_apply. + * @param patches NSMutableArray of Patch objects. + */ +- (void)patch_splitMax:(NSMutableArray *)patches; +{ + NSUInteger patch_size = Match_MaxBits; + for (NSUInteger x = 0; x < patches.count; x++) { + if (((Patch *)[patches objectAtIndex:x]).length1 <= patch_size) { + continue; + } + Patch *bigpatch = [[patches objectAtIndex:x] retain]; + // Remove the big old patch. + splice(patches, x--, 1, nil); + NSUInteger start1 = bigpatch.start1; + NSUInteger start2 = bigpatch.start2; + NSString *precontext = @""; + while (bigpatch.diffs.count != 0) { + // Create one of several smaller patches. + Patch *patch = [[Patch new] autorelease]; + BOOL empty = YES; + patch.start1 = start1 - precontext.length; + patch.start2 = start2 - precontext.length; + if (precontext.length != 0) { + patch.length1 = patch.length2 = precontext.length; + [patch.diffs addObject:[Diff diffWithOperation:DIFF_EQUAL andText:precontext]]; + } + while (bigpatch.diffs.count != 0 + && patch.length1 < patch_size - self.Patch_Margin) { + Operation diff_type = ((Diff *)[bigpatch.diffs objectAtIndex:0]).operation; + NSString *diff_text = ((Diff *)[bigpatch.diffs objectAtIndex:0]).text; + if (diff_type == DIFF_INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.length; + start2 += diff_text.length; + [patch.diffs addObject:[bigpatch.diffs objectAtIndex:0]]; + [bigpatch.diffs removeObjectAtIndex:0]; + empty = NO; + } else if (diff_type == DIFF_DELETE && patch.diffs.count == 1 + && ((Diff *)[patch.diffs objectAtIndex:0]).operation == DIFF_EQUAL + && diff_text.length > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.length; + start1 += diff_text.length; + empty = NO; + [patch.diffs addObject:[Diff diffWithOperation:diff_type andText:diff_text]]; + [bigpatch.diffs removeObjectAtIndex:0]; + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = [diff_text substringWithRange:NSMakeRange(0, + MIN(diff_text.length, + (patch_size - patch.length1 - Patch_Margin)))]; + patch.length1 += diff_text.length; + start1 += diff_text.length; + if (diff_type == DIFF_EQUAL) { + patch.length2 += diff_text.length; + start2 += diff_text.length; + } else { + empty = NO; + } + [patch.diffs addObject:[Diff diffWithOperation:diff_type andText:diff_text]]; + if (diff_text == ((Diff *)[bigpatch.diffs objectAtIndex:0]).text) { + [bigpatch.diffs removeObjectAtIndex:0]; + } else { + Diff *firstDiff = [bigpatch.diffs objectAtIndex:0]; + firstDiff.text = [firstDiff.text substringFromIndex:diff_text.length]; + } + } + } + // Compute the head context for the next patch. + precontext = [self diff_text2:patch.diffs]; + precontext = [precontext substringFromIndex:MAX_OF_CONST_AND_DIFF(0, precontext.length, Patch_Margin)]; + + NSString *postcontext = nil; + // Append the end context for this patch. + if ([self diff_text1:bigpatch.diffs].length > Patch_Margin) { + postcontext = [[self diff_text1:bigpatch.diffs] + substringWithRange:NSMakeRange(0, Patch_Margin)]; + } else { + postcontext = [self diff_text1:bigpatch.diffs]; + } + + if (postcontext.length != 0) { + patch.length1 += postcontext.length; + patch.length2 += postcontext.length; + if (patch.diffs.count != 0 + && ((Diff *)[patch.diffs objectAtIndex:(patch.diffs.count - 1)]).operation + == DIFF_EQUAL) { + Diff *lastDiff = [patch.diffs lastObject]; + lastDiff.text = [lastDiff.text stringByAppendingString:postcontext]; + } else { + [patch.diffs addObject:[Diff diffWithOperation:DIFF_EQUAL andText:postcontext]]; + } + } + if (!empty) { + splice(patches, ++x, 0, [NSMutableArray arrayWithObject:patch]); + } + } + + [bigpatch release]; + + } +} + +/** + * Take a list of patches and return a textual representation. + * @param patches NSMutableArray of Patch objects. + * @return Text representation of patches. + */ +- (NSString *)patch_toText:(NSMutableArray *)patches; +{ + NSMutableString *text = [NSMutableString string]; + for (Patch *aPatch in patches) { + [text appendString:[aPatch description]]; + } + return text; +} + +/** + * Parse a textual representation of patches and return a NSMutableArray of + * Patch objects. + * @param textline Text representation of patches. + * @param error NSError if invalid input. + * @return NSMutableArray of Patch objects. + */ +- (NSMutableArray *)patch_fromText:(NSString *)textline + error:(NSError **)error; +{ + NSMutableArray *patches = [NSMutableArray array]; + if (textline.length == 0) { + return patches; + } + NSArray *text = [textline componentsSeparatedByString:@"\n"]; + NSUInteger textPointer = 0; + Patch *patch; + //NSString *patchHeader = @"^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$"; + NSString *patchHeaderStart = @"@@ -"; + NSString *patchHeaderMid = @"+"; + NSString *patchHeaderEnd = @"@@"; + NSString *optionalValueDelimiter = @","; + BOOL scanSuccess, hasOptional; + NSInteger scannedValue, optionalValue; + NSDictionary *errorDetail = nil; + + unichar sign; + NSString *line; + while (textPointer < text.count) { + NSString *thisLine = [text objectAtIndex:textPointer]; + NSScanner *theScanner = [NSScanner scannerWithString:thisLine]; + patch = [[Patch new] autorelease]; + + scanSuccess = ([theScanner scanString:patchHeaderStart intoString:NULL] + && [theScanner scanInteger:&scannedValue]); + + if (scanSuccess) { + patch.start1 = scannedValue; + + hasOptional = [theScanner scanString:optionalValueDelimiter intoString:NULL]; + + if (hasOptional) { + // First set has an optional value. + scanSuccess = [theScanner scanInteger:&optionalValue]; + if (scanSuccess) { + if (optionalValue == 0) { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = optionalValue; + } + } + } else { + patch.start1--; + patch.length1 = 1; + } + + if (scanSuccess) { + scanSuccess = ([theScanner scanString:patchHeaderMid intoString:NULL] + && [theScanner scanInteger:&scannedValue]); + + if (scanSuccess) { + patch.start2 = scannedValue; + + hasOptional = [theScanner scanString:optionalValueDelimiter intoString:NULL]; + + if (hasOptional) { + // Second set has an optional value. + scanSuccess = [theScanner scanInteger:&optionalValue]; + if (scanSuccess) { + if (optionalValue == 0) { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = optionalValue; + } + } + } else { + patch.start2--; + patch.length2 = 1; + } + + if (scanSuccess) { + scanSuccess = ([theScanner scanString:patchHeaderEnd intoString:NULL] + && [theScanner isAtEnd] == YES); + } + } + } + } + + if (!scanSuccess) { + if (error != NULL) { + errorDetail = [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithFormat:NSLocalizedString(@"Invalid patch string: %@", @"Error"), + [text objectAtIndex:textPointer]], + NSLocalizedDescriptionKey, nil]; + *error = [NSError errorWithDomain:@"DiffMatchPatchErrorDomain" code:104 userInfo:errorDetail]; + } + return nil; + } + + [patches addObject:patch]; + + textPointer++; + + while (textPointer < text.count) { + @try { + sign = [[text objectAtIndex:textPointer] characterAtIndex:0]; + } + @catch (NSException *e) { + // Blank line? Whatever. + textPointer++; + continue; + } + line = [[[text objectAtIndex:textPointer] substringFromIndex:1] + diff_stringByReplacingPercentEscapesForEncodeUriCompatibility]; + if (sign == '-') { + // Deletion. + [patch.diffs addObject:[Diff diffWithOperation:DIFF_DELETE andText:line]]; + } else if (sign == '+') { + // Insertion. + [patch.diffs addObject:[Diff diffWithOperation:DIFF_INSERT andText:line]]; + } else if (sign == ' ') { + // Minor equality. + [patch.diffs addObject:[Diff diffWithOperation:DIFF_EQUAL andText:line]]; + } else if (sign == '@') { + // Start of next patch. + break; + } else { + // WTF? + if (error != NULL) { + errorDetail = [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithFormat:NSLocalizedString(@"Invalid patch mode '%C' in: %@", @"Error"), sign, line], + NSLocalizedDescriptionKey, nil]; + *error = [NSError errorWithDomain:@"DiffMatchPatchErrorDomain" code:104 userInfo:errorDetail]; + } + return nil; + } + textPointer++; + } + } + return patches; +} + +@end diff --git a/objectivec/DiffMatchPatch.xcodeproj/project.pbxproj b/objectivec/DiffMatchPatch.xcodeproj/project.pbxproj new file mode 100755 index 0000000..f2f7637 --- /dev/null +++ b/objectivec/DiffMatchPatch.xcodeproj/project.pbxproj @@ -0,0 +1,771 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 45; + objects = { + +/* Begin PBXBuildFile section */ + 3D08D25412A71B9C007A5316 /* NSString+UnicharUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D08D25212A71B9C007A5316 /* NSString+UnicharUtilities.m */; }; + 3D0D3FAB128CBD350093B0C7 /* DiffMatchPatch.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DC2EF5B0486A6940098B216 /* DiffMatchPatch.framework */; }; + 3D5BBFDF128C416900B8F5FF /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0867D69BFE84028FC02AAC07 /* Foundation.framework */; }; + 3D5BC080128C44A700B8F5FF /* DiffMatchPatch.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D5BC07E128C44A700B8F5FF /* DiffMatchPatch.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3D5BC081128C44A700B8F5FF /* DiffMatchPatch.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D5BC07F128C44A700B8F5FF /* DiffMatchPatch.m */; }; + 3D70BCC9128EDAF80078D1A6 /* NSString+UriCompatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D70BCC7128EDAF80078D1A6 /* NSString+UriCompatibility.m */; }; + 3D96F3BC12AFC6D800C3E5C0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0867D69BFE84028FC02AAC07 /* Foundation.framework */; }; + 3D96F44A12AFC82F00C3E5C0 /* speedtest.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D96F44912AFC82F00C3E5C0 /* speedtest.m */; }; + 3D96F44F12AFC96A00C3E5C0 /* DiffMatchPatch.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DC2EF5B0486A6940098B216 /* DiffMatchPatch.framework */; }; + 3DA641D1128DE21D00B33CE9 /* DiffMatchPatchCFUtilities.c in Sources */ = {isa = PBXBuildFile; fileRef = 3DA64170128DDA3400B33CE9 /* DiffMatchPatchCFUtilities.c */; }; + 3DA64278128DE8C900B33CE9 /* DiffMatchPatchTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D5BC0DE128CAAE400B8F5FF /* DiffMatchPatchTest.m */; }; + 3DC87026129FF4B6001F602B /* NSString+JavaSubstring.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DC87024129FF4B6001F602B /* NSString+JavaSubstring.m */; }; + 3DEE4C73129D484D00885485 /* NSMutableDictionary+DMPExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DEE4C71129D484D00885485 /* NSMutableDictionary+DMPExtensions.m */; }; + 3DFDB2FA12AEC7700084EFE3 /* libstdc++.6.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DFDB2F912AEC7700084EFE3 /* libstdc++.6.dylib */; }; + 3DFDB2FB12AEC77E0084EFE3 /* libstdc++.6.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DFDB2F912AEC7700084EFE3 /* libstdc++.6.dylib */; }; + 8DC2EF530486A6940098B216 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C1666FE841158C02AAC07 /* InfoPlist.strings */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 3D96F3B812AFC6CD00C3E5C0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8DC2EF4F0486A6940098B216; + remoteInfo = DiffMatchPatch; + }; + 3DA64279128DE8CD00B33CE9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8DC2EF4F0486A6940098B216; + remoteInfo = DiffMatchPatch; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0867D69BFE84028FC02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; + 0867D6A5FE840307C02AAC07 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; + 089C1667FE841158C02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; + 1058C7B1FEA5585E11CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; + 32DBCF5E0370ADEE00C91783 /* DiffMatchPatch_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DiffMatchPatch_Prefix.pch; sourceTree = ""; }; + 3D08D25112A71B9C007A5316 /* NSString+UnicharUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+UnicharUtilities.h"; sourceTree = ""; }; + 3D08D25212A71B9C007A5316 /* NSString+UnicharUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+UnicharUtilities.m"; sourceTree = ""; }; + 3D105A0912DA5F9D002111E1 /* Base+SnowLeopard.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Base+SnowLeopard.xcconfig"; sourceTree = ""; }; + 3D510E1012BEBA44008C7CE7 /* Base.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = ""; }; + 3D510E1112BEBA44008C7CE7 /* Version.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = ""; }; + 3D5BC07E128C44A700B8F5FF /* DiffMatchPatch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DiffMatchPatch.h; sourceTree = ""; }; + 3D5BC07F128C44A700B8F5FF /* DiffMatchPatch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DiffMatchPatch.m; sourceTree = ""; }; + 3D5BC0AF128CA8E200B8F5FF /* DiffMatchPatchTest.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DiffMatchPatchTest.octest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3D5BC0B0128CA8E200B8F5FF /* DiffMatchPatchTest-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "DiffMatchPatchTest-Info.plist"; sourceTree = ""; }; + 3D5BC0DD128CAAE400B8F5FF /* DiffMatchPatchTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DiffMatchPatchTest.h; sourceTree = ""; }; + 3D5BC0DE128CAAE400B8F5FF /* DiffMatchPatchTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DiffMatchPatchTest.m; sourceTree = ""; }; + 3D5BC104128CAD6900B8F5FF /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; }; + 3D70BCC6128EDAF80078D1A6 /* NSString+UriCompatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+UriCompatibility.h"; sourceTree = ""; }; + 3D70BCC7128EDAF80078D1A6 /* NSString+UriCompatibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+UriCompatibility.m"; sourceTree = ""; }; + 3D96F3A212AFC68900C3E5C0 /* speedtest */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = speedtest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3D96F43E12AFC76600C3E5C0 /* speedtest_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = speedtest_Prefix.pch; path = ../speedtest_Prefix.pch; sourceTree = ""; }; + 3D96F44912AFC82F00C3E5C0 /* speedtest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = speedtest.m; sourceTree = ""; }; + 3DA6416F128DDA3400B33CE9 /* DiffMatchPatchCFUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DiffMatchPatchCFUtilities.h; sourceTree = ""; }; + 3DA64170128DDA3400B33CE9 /* DiffMatchPatchCFUtilities.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = DiffMatchPatchCFUtilities.c; sourceTree = ""; }; + 3DA642BC128DEF4900B33CE9 /* MinMaxMacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MinMaxMacros.h; sourceTree = ""; }; + 3DC87023129FF4B6001F602B /* NSString+JavaSubstring.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+JavaSubstring.h"; sourceTree = ""; }; + 3DC87024129FF4B6001F602B /* NSString+JavaSubstring.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+JavaSubstring.m"; sourceTree = ""; }; + 3DEE4C70129D484D00885485 /* NSMutableDictionary+DMPExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMutableDictionary+DMPExtensions.h"; sourceTree = ""; }; + 3DEE4C71129D484D00885485 /* NSMutableDictionary+DMPExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMutableDictionary+DMPExtensions.m"; sourceTree = ""; }; + 3DFDB2F912AEC7700084EFE3 /* libstdc++.6.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "libstdc++.6.dylib"; path = "usr/lib/libstdc++.6.dylib"; sourceTree = SDKROOT; }; + 8DC2EF5A0486A6940098B216 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8DC2EF5B0486A6940098B216 /* DiffMatchPatch.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DiffMatchPatch.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3D5BC0AC128CA8E200B8F5FF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D0D3FAB128CBD350093B0C7 /* DiffMatchPatch.framework in Frameworks */, + 3DFDB2FA12AEC7700084EFE3 /* libstdc++.6.dylib in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3D96F3A012AFC68900C3E5C0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D96F44F12AFC96A00C3E5C0 /* DiffMatchPatch.framework in Frameworks */, + 3D96F3BC12AFC6D800C3E5C0 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8DC2EF560486A6940098B216 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D5BBFDF128C416900B8F5FF /* Foundation.framework in Frameworks */, + 3DFDB2FB12AEC77E0084EFE3 /* libstdc++.6.dylib in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 034768DFFF38A50411DB9C8B /* Products */ = { + isa = PBXGroup; + children = ( + 8DC2EF5B0486A6940098B216 /* DiffMatchPatch.framework */, + 3D5BC0AF128CA8E200B8F5FF /* DiffMatchPatchTest.octest */, + 3D96F3A212AFC68900C3E5C0 /* speedtest */, + ); + name = Products; + sourceTree = ""; + }; + 0867D691FE84028FC02AAC07 /* DiffMatchPatch */ = { + isa = PBXGroup; + children = ( + 08FB77AEFE84172EC02AAC07 /* Classes */, + 32C88DFF0371C24200C91783 /* Other Sources */, + 089C1665FE841158C02AAC07 /* Resources */, + 3D510E0F12BEBA44008C7CE7 /* Configurations */, + 0867D69AFE84028FC02AAC07 /* External Frameworks and Libraries */, + 3D5BC0D9128CAA7600B8F5FF /* Tests */, + 034768DFFF38A50411DB9C8B /* Products */, + ); + name = DiffMatchPatch; + sourceTree = ""; + }; + 0867D69AFE84028FC02AAC07 /* External Frameworks and Libraries */ = { + isa = PBXGroup; + children = ( + 1058C7B0FEA5585E11CA2CBB /* Linked Frameworks */, + 1058C7B2FEA5585E11CA2CBB /* Other Frameworks */, + ); + name = "External Frameworks and Libraries"; + sourceTree = ""; + }; + 089C1665FE841158C02AAC07 /* Resources */ = { + isa = PBXGroup; + children = ( + 8DC2EF5A0486A6940098B216 /* Info.plist */, + 089C1666FE841158C02AAC07 /* InfoPlist.strings */, + ); + name = Resources; + sourceTree = ""; + }; + 08FB77AEFE84172EC02AAC07 /* Classes */ = { + isa = PBXGroup; + children = ( + 3D5BC07E128C44A700B8F5FF /* DiffMatchPatch.h */, + 3D5BC07F128C44A700B8F5FF /* DiffMatchPatch.m */, + 3DC87023129FF4B6001F602B /* NSString+JavaSubstring.h */, + 3DC87024129FF4B6001F602B /* NSString+JavaSubstring.m */, + 3D08D25112A71B9C007A5316 /* NSString+UnicharUtilities.h */, + 3D08D25212A71B9C007A5316 /* NSString+UnicharUtilities.m */, + 3D70BCC6128EDAF80078D1A6 /* NSString+UriCompatibility.h */, + 3D70BCC7128EDAF80078D1A6 /* NSString+UriCompatibility.m */, + 3DEE4C70129D484D00885485 /* NSMutableDictionary+DMPExtensions.h */, + 3DEE4C71129D484D00885485 /* NSMutableDictionary+DMPExtensions.m */, + ); + name = Classes; + sourceTree = ""; + }; + 1058C7B0FEA5585E11CA2CBB /* Linked Frameworks */ = { + isa = PBXGroup; + children = ( + 1058C7B1FEA5585E11CA2CBB /* Cocoa.framework */, + ); + name = "Linked Frameworks"; + sourceTree = ""; + }; + 1058C7B2FEA5585E11CA2CBB /* Other Frameworks */ = { + isa = PBXGroup; + children = ( + 0867D6A5FE840307C02AAC07 /* AppKit.framework */, + 0867D69BFE84028FC02AAC07 /* Foundation.framework */, + 3DFDB2F912AEC7700084EFE3 /* libstdc++.6.dylib */, + ); + name = "Other Frameworks"; + sourceTree = ""; + }; + 32C88DFF0371C24200C91783 /* Other Sources */ = { + isa = PBXGroup; + children = ( + 32DBCF5E0370ADEE00C91783 /* DiffMatchPatch_Prefix.pch */, + 3DA642BC128DEF4900B33CE9 /* MinMaxMacros.h */, + 3DA6416F128DDA3400B33CE9 /* DiffMatchPatchCFUtilities.h */, + 3DA64170128DDA3400B33CE9 /* DiffMatchPatchCFUtilities.c */, + ); + name = "Other Sources"; + sourceTree = ""; + }; + 3D510E0F12BEBA44008C7CE7 /* Configurations */ = { + isa = PBXGroup; + children = ( + 3D510E1112BEBA44008C7CE7 /* Version.xcconfig */, + 3D510E1012BEBA44008C7CE7 /* Base.xcconfig */, + 3D105A0912DA5F9D002111E1 /* Base+SnowLeopard.xcconfig */, + ); + path = Configurations; + sourceTree = ""; + }; + 3D5BC0D9128CAA7600B8F5FF /* Tests */ = { + isa = PBXGroup; + children = ( + 3D5BC104128CAD6900B8F5FF /* SenTestingKit.framework */, + 3D5BC0B0128CA8E200B8F5FF /* DiffMatchPatchTest-Info.plist */, + 3D5BC0DD128CAAE400B8F5FF /* DiffMatchPatchTest.h */, + 3D5BC0DE128CAAE400B8F5FF /* DiffMatchPatchTest.m */, + 3D96F43E12AFC76600C3E5C0 /* speedtest_Prefix.pch */, + 3D96F44912AFC82F00C3E5C0 /* speedtest.m */, + ); + path = Tests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 8DC2EF500486A6940098B216 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D5BC080128C44A700B8F5FF /* DiffMatchPatch.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 3D5BC0AE128CA8E200B8F5FF /* DiffMatchPatchTest */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3D5BC0B3128CA8E500B8F5FF /* Build configuration list for PBXNativeTarget "DiffMatchPatchTest" */; + buildPhases = ( + 3D5BC0AA128CA8E200B8F5FF /* Resources */, + 3D5BC0AB128CA8E200B8F5FF /* Sources */, + 3D5BC0AC128CA8E200B8F5FF /* Frameworks */, + 3D5BC0AD128CA8E200B8F5FF /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 3DA6427A128DE8CD00B33CE9 /* PBXTargetDependency */, + ); + name = DiffMatchPatchTest; + productName = DiffMatchPatchTest; + productReference = 3D5BC0AF128CA8E200B8F5FF /* DiffMatchPatchTest.octest */; + productType = "com.apple.product-type.bundle"; + }; + 3D96F3A112AFC68900C3E5C0 /* speedtest */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3D96F3B512AFC68E00C3E5C0 /* Build configuration list for PBXNativeTarget "speedtest" */; + buildPhases = ( + 3D96F39F12AFC68900C3E5C0 /* Sources */, + 3D96F3A012AFC68900C3E5C0 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 3D96F3B912AFC6CD00C3E5C0 /* PBXTargetDependency */, + ); + name = speedtest; + productName = speedtest; + productReference = 3D96F3A212AFC68900C3E5C0 /* speedtest */; + productType = "com.apple.product-type.tool"; + }; + 8DC2EF4F0486A6940098B216 /* DiffMatchPatch */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1DEB91AD08733DA50010E9CD /* Build configuration list for PBXNativeTarget "DiffMatchPatch" */; + buildPhases = ( + 8DC2EF500486A6940098B216 /* Headers */, + 8DC2EF520486A6940098B216 /* Resources */, + 8DC2EF540486A6940098B216 /* Sources */, + 8DC2EF560486A6940098B216 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = DiffMatchPatch; + productInstallPath = "$(HOME)/Library/Frameworks"; + productName = DiffMatchPatch; + productReference = 8DC2EF5B0486A6940098B216 /* DiffMatchPatch.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0867D690FE84028FC02AAC07 /* Project object */ = { + isa = PBXProject; + buildConfigurationList = 1DEB91B108733DA50010E9CD /* Build configuration list for PBXProject "DiffMatchPatch" */; + compatibilityVersion = "Xcode 3.1"; + developmentRegion = English; + hasScannedForEncodings = 1; + knownRegions = ( + English, + Japanese, + French, + German, + ); + mainGroup = 0867D691FE84028FC02AAC07 /* DiffMatchPatch */; + productRefGroup = 034768DFFF38A50411DB9C8B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8DC2EF4F0486A6940098B216 /* DiffMatchPatch */, + 3D5BC0AE128CA8E200B8F5FF /* DiffMatchPatchTest */, + 3D96F3A112AFC68900C3E5C0 /* speedtest */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3D5BC0AA128CA8E200B8F5FF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8DC2EF520486A6940098B216 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DC2EF530486A6940098B216 /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3D5BC0AD128CA8E200B8F5FF /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3D5BC0AB128CA8E200B8F5FF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3DA64278128DE8C900B33CE9 /* DiffMatchPatchTest.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3D96F39F12AFC68900C3E5C0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D96F44A12AFC82F00C3E5C0 /* speedtest.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8DC2EF540486A6940098B216 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D5BC081128C44A700B8F5FF /* DiffMatchPatch.m in Sources */, + 3DA641D1128DE21D00B33CE9 /* DiffMatchPatchCFUtilities.c in Sources */, + 3D70BCC9128EDAF80078D1A6 /* NSString+UriCompatibility.m in Sources */, + 3DEE4C73129D484D00885485 /* NSMutableDictionary+DMPExtensions.m in Sources */, + 3DC87026129FF4B6001F602B /* NSString+JavaSubstring.m in Sources */, + 3D08D25412A71B9C007A5316 /* NSString+UnicharUtilities.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 3D96F3B912AFC6CD00C3E5C0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8DC2EF4F0486A6940098B216 /* DiffMatchPatch */; + targetProxy = 3D96F3B812AFC6CD00C3E5C0 /* PBXContainerItemProxy */; + }; + 3DA6427A128DE8CD00B33CE9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8DC2EF4F0486A6940098B216 /* DiffMatchPatch */; + targetProxy = 3DA64279128DE8CD00B33CE9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 089C1666FE841158C02AAC07 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 089C1667FE841158C02AAC07 /* English */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 1DEB91AE08733DA50010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = NO; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_VERSION = A; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = DiffMatchPatch_Prefix.pch; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "@executable_path/../Frameworks"; + PRODUCT_NAME = DiffMatchPatch; + WRAPPER_EXTENSION = framework; + }; + name = Debug; + }; + 1DEB91AF08733DA50010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_VERSION = A; + GCC_MODEL_TUNING = G5; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = DiffMatchPatch_Prefix.pch; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "@executable_path/../Frameworks"; + PRODUCT_NAME = DiffMatchPatch; + WRAPPER_EXTENSION = framework; + }; + name = Release; + }; + 1DEB91B208733DA50010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3D510E1012BEBA44008C7CE7 /* Base.xcconfig */; + buildSettings = { + GCC_OPTIMIZATION_LEVEL = 0; + GCC_WARN_UNINITIALIZED_AUTOS = NO; + ONLY_ACTIVE_ARCH = YES; + }; + name = Debug; + }; + 1DEB91B308733DA50010E9CD /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3D510E1012BEBA44008C7CE7 /* Base.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 3D105A0C12DA601C002111E1 /* Debug 10.6 */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3D105A0912DA5F9D002111E1 /* Base+SnowLeopard.xcconfig */; + buildSettings = { + GCC_OPTIMIZATION_LEVEL = 0; + GCC_WARN_UNINITIALIZED_AUTOS = NO; + ONLY_ACTIVE_ARCH = YES; + }; + name = "Debug 10.6"; + }; + 3D105A0D12DA601C002111E1 /* Debug 10.6 */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = NO; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_VERSION = A; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = DiffMatchPatch_Prefix.pch; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "@executable_path/../Frameworks"; + PRODUCT_NAME = DiffMatchPatch; + WRAPPER_EXTENSION = framework; + }; + name = "Debug 10.6"; + }; + 3D105A0E12DA601C002111E1 /* Debug 10.6 */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = NO; + FRAMEWORK_SEARCH_PATHS = "$(DEVELOPER_LIBRARY_DIR)/Frameworks"; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/Cocoa.framework/Headers/Cocoa.h"; + INFOPLIST_FILE = "Tests/DiffMatchPatchTest-Info.plist"; + INSTALL_PATH = "$(USER_LIBRARY_DIR)/Bundles"; + OTHER_LDFLAGS = ( + "-framework", + Cocoa, + "-framework", + SenTestingKit, + ); + PREBINDING = NO; + PRODUCT_NAME = DiffMatchPatchTest; + WRAPPER_EXTENSION = octest; + }; + name = "Debug 10.6"; + }; + 3D105A0F12DA601C002111E1 /* Debug 10.6 */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = NO; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = speedtest_Prefix.pch; + INSTALL_PATH = /usr/local/bin; + OTHER_LDFLAGS = ( + "-framework", + Foundation, + "-framework", + AppKit, + ); + PREBINDING = NO; + PRODUCT_NAME = speedtest; + }; + name = "Debug 10.6"; + }; + 3D105A1012DA6024002111E1 /* Release 10.6 */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3D105A0912DA5F9D002111E1 /* Base+SnowLeopard.xcconfig */; + buildSettings = { + }; + name = "Release 10.6"; + }; + 3D105A1112DA6024002111E1 /* Release 10.6 */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_VERSION = A; + GCC_MODEL_TUNING = G5; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = DiffMatchPatch_Prefix.pch; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "@executable_path/../Frameworks"; + PRODUCT_NAME = DiffMatchPatch; + WRAPPER_EXTENSION = framework; + }; + name = "Release 10.6"; + }; + 3D105A1212DA6024002111E1 /* Release 10.6 */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + FRAMEWORK_SEARCH_PATHS = "$(DEVELOPER_LIBRARY_DIR)/Frameworks"; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_MODEL_TUNING = G5; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/Cocoa.framework/Headers/Cocoa.h"; + INFOPLIST_FILE = "Tests/DiffMatchPatchTest-Info.plist"; + INSTALL_PATH = "$(USER_LIBRARY_DIR)/Bundles"; + OTHER_LDFLAGS = ( + "-framework", + Cocoa, + "-framework", + SenTestingKit, + ); + PREBINDING = NO; + PRODUCT_NAME = DiffMatchPatchTest; + WRAPPER_EXTENSION = octest; + ZERO_LINK = NO; + }; + name = "Release 10.6"; + }; + 3D105A1312DA6024002111E1 /* Release 10.6 */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_MODEL_TUNING = G5; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = speedtest_Prefix.pch; + INSTALL_PATH = /usr/local/bin; + OTHER_LDFLAGS = ( + "-framework", + Foundation, + "-framework", + AppKit, + ); + PREBINDING = NO; + PRODUCT_NAME = speedtest; + ZERO_LINK = NO; + }; + name = "Release 10.6"; + }; + 3D5BC0B1128CA8E500B8F5FF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = NO; + FRAMEWORK_SEARCH_PATHS = "$(DEVELOPER_LIBRARY_DIR)/Frameworks"; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/Cocoa.framework/Headers/Cocoa.h"; + INFOPLIST_FILE = "Tests/DiffMatchPatchTest-Info.plist"; + INSTALL_PATH = "$(USER_LIBRARY_DIR)/Bundles"; + OTHER_LDFLAGS = ( + "-framework", + Cocoa, + "-framework", + SenTestingKit, + ); + PREBINDING = NO; + PRODUCT_NAME = DiffMatchPatchTest; + WRAPPER_EXTENSION = octest; + }; + name = Debug; + }; + 3D5BC0B2128CA8E500B8F5FF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + FRAMEWORK_SEARCH_PATHS = "$(DEVELOPER_LIBRARY_DIR)/Frameworks"; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_MODEL_TUNING = G5; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/Cocoa.framework/Headers/Cocoa.h"; + INFOPLIST_FILE = "Tests/DiffMatchPatchTest-Info.plist"; + INSTALL_PATH = "$(USER_LIBRARY_DIR)/Bundles"; + OTHER_LDFLAGS = ( + "-framework", + Cocoa, + "-framework", + SenTestingKit, + ); + PREBINDING = NO; + PRODUCT_NAME = DiffMatchPatchTest; + WRAPPER_EXTENSION = octest; + ZERO_LINK = NO; + }; + name = Release; + }; + 3D96F3A412AFC68B00C3E5C0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = NO; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = speedtest_Prefix.pch; + INSTALL_PATH = /usr/local/bin; + OTHER_LDFLAGS = ( + "-framework", + Foundation, + "-framework", + AppKit, + ); + PREBINDING = NO; + PRODUCT_NAME = speedtest; + }; + name = Debug; + }; + 3D96F3A512AFC68B00C3E5C0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_MODEL_TUNING = G5; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = speedtest_Prefix.pch; + INSTALL_PATH = /usr/local/bin; + OTHER_LDFLAGS = ( + "-framework", + Foundation, + "-framework", + AppKit, + ); + PREBINDING = NO; + PRODUCT_NAME = speedtest; + ZERO_LINK = NO; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1DEB91AD08733DA50010E9CD /* Build configuration list for PBXNativeTarget "DiffMatchPatch" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB91AE08733DA50010E9CD /* Debug */, + 3D105A0D12DA601C002111E1 /* Debug 10.6 */, + 1DEB91AF08733DA50010E9CD /* Release */, + 3D105A1112DA6024002111E1 /* Release 10.6 */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1DEB91B108733DA50010E9CD /* Build configuration list for PBXProject "DiffMatchPatch" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB91B208733DA50010E9CD /* Debug */, + 3D105A0C12DA601C002111E1 /* Debug 10.6 */, + 1DEB91B308733DA50010E9CD /* Release */, + 3D105A1012DA6024002111E1 /* Release 10.6 */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3D5BC0B3128CA8E500B8F5FF /* Build configuration list for PBXNativeTarget "DiffMatchPatchTest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3D5BC0B1128CA8E500B8F5FF /* Debug */, + 3D105A0E12DA601C002111E1 /* Debug 10.6 */, + 3D5BC0B2128CA8E500B8F5FF /* Release */, + 3D105A1212DA6024002111E1 /* Release 10.6 */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3D96F3B512AFC68E00C3E5C0 /* Build configuration list for PBXNativeTarget "speedtest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3D96F3A412AFC68B00C3E5C0 /* Debug */, + 3D105A0F12DA601C002111E1 /* Debug 10.6 */, + 3D96F3A512AFC68B00C3E5C0 /* Release */, + 3D105A1312DA6024002111E1 /* Release 10.6 */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 0867D690FE84028FC02AAC07 /* Project object */; +} diff --git a/objectivec/DiffMatchPatchCFUtilities.c b/objectivec/DiffMatchPatchCFUtilities.c new file mode 100755 index 0000000..d21a4e6 --- /dev/null +++ b/objectivec/DiffMatchPatchCFUtilities.c @@ -0,0 +1,586 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#include + +#include "DiffMatchPatchCFUtilities.h" + +#include "MinMaxMacros.h" +#include +#include +#include + +CFStringRef diff_CFStringCreateSubstring(CFStringRef text, CFIndex start_index, CFIndex length); +CFRange diff_RightSubstringRange(CFIndex text_length, CFIndex new_length); +CFStringRef diff_CFStringCreateRightSubstring(CFStringRef text, CFIndex text_length, CFIndex new_length); +CFRange diff_LeftSubstringRange(CFIndex new_length); +CFStringRef diff_CFStringCreateLeftSubstring(CFStringRef text, CFIndex new_length); +CFStringRef diff_CFStringCreateSubstringWithStartIndex(CFStringRef text, CFIndex start_index); +CFStringRef diff_CFStringCreateJavaSubstring(CFStringRef s, CFIndex begin, CFIndex end); +CFStringRef diff_CFStringCreateByCombiningTwoStrings(CFStringRef best_common_part1, CFStringRef best_common_part2); +Boolean diff_regExMatch(CFStringRef text, const regex_t *re); + +CFArrayRef diff_halfMatchICreate(CFStringRef longtext, CFStringRef shorttext, CFIndex i); + +// Utility functions +CFStringRef diff_CFStringCreateFromUnichar(UniChar ch) { + CFStringRef c = CFStringCreateWithCharacters(kCFAllocatorDefault, &ch, 1); + CFMakeCollectable(c); + return c; +} + +CFStringRef diff_CFStringCreateSubstring(CFStringRef text, CFIndex start_index, CFIndex length) { + CFRange substringRange; + substringRange.length = length; + substringRange.location = start_index; + + CFStringRef substring = CFStringCreateWithSubstring(kCFAllocatorDefault, text, substringRange); + CFMakeCollectable(substring); + + return substring; +} + +CFRange diff_RightSubstringRange(CFIndex text_length, CFIndex new_length) { + CFRange substringRange; + substringRange.length = new_length; + substringRange.location = text_length - new_length; + return substringRange; +} + +CFStringRef diff_CFStringCreateRightSubstring(CFStringRef text, CFIndex text_length, CFIndex new_length) { + return diff_CFStringCreateSubstring(text, text_length - new_length, new_length); +} + +CFRange diff_LeftSubstringRange(CFIndex new_length) { + CFRange substringRange; + substringRange.length = new_length; + substringRange.location = 0; + return substringRange; +} + +CFStringRef diff_CFStringCreateLeftSubstring(CFStringRef text, CFIndex new_length) { + return diff_CFStringCreateSubstring(text, 0, new_length); +} + +CFStringRef diff_CFStringCreateSubstringWithStartIndex(CFStringRef text, CFIndex start_index) { + return diff_CFStringCreateSubstring(text, start_index, (CFStringGetLength(text) - start_index)); +} + +CFStringRef diff_CFStringCreateJavaSubstring(CFStringRef s, CFIndex begin, CFIndex end) { + return diff_CFStringCreateSubstring(s, begin, end - begin); +} + +CFStringRef diff_CFStringCreateByCombiningTwoStrings(CFStringRef best_common_part1, CFStringRef best_common_part2) { + CFIndex best_common_length; + CFMutableStringRef best_common_mutable; + best_common_length = CFStringGetLength(best_common_part1) + CFStringGetLength(best_common_part2); + best_common_mutable = CFStringCreateMutableCopy(kCFAllocatorDefault, best_common_length, best_common_part1); + CFMakeCollectable(best_common_mutable); + CFStringAppend(best_common_mutable, best_common_part2); + return best_common_mutable; +} + +Boolean diff_regExMatch(CFStringRef text, const regex_t *re) { + //TODO(jan): Using regex.h is far from optimal. Find an alternative. + Boolean isMatch; + const char *bytes; + char *localBuffer = NULL; + char *textCString = NULL; + // We are only interested in line endings anyway so ASCII is fine. + CFStringEncoding encoding = kCFStringEncodingASCII; + + bytes = CFStringGetCStringPtr(text, encoding); + + if (bytes == NULL) { + Boolean success; + CFIndex length; + CFIndex usedBufferLength; + CFIndex textLength = CFStringGetLength(text); + CFRange rangeToProcess = CFRangeMake(0, textLength); + + success = (CFStringGetBytes(text, rangeToProcess, encoding, '?', false, NULL, LONG_MAX, &usedBufferLength) > 0); + if (success) { + length = usedBufferLength + 1; + + localBuffer = calloc(length, sizeof(char)); + success = (CFStringGetBytes(text, rangeToProcess, encoding, '?', false, (UInt8 *)localBuffer, length, NULL) > 0); + + if (success) { + textCString = localBuffer; + } + } + } else { + textCString = (char *)bytes; + } + + if (textCString != NULL) { + isMatch = (regexec(re, textCString, 0, NULL, 0) == 0); + } else { + isMatch = false; + //assert(0); + } + + if (localBuffer != NULL) { + free(localBuffer); + } + + return isMatch; +} + + +/** + * Determine the common prefix of two strings. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the start of each string. + */ +CFIndex diff_commonPrefix(CFStringRef text1, CFStringRef text2) { + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + CFIndex text1_length = CFStringGetLength(text1); + CFIndex text2_length = CFStringGetLength(text2); + + CFStringInlineBuffer text1_inlineBuffer, text2_inlineBuffer; + CFStringInitInlineBuffer(text1, &text1_inlineBuffer, CFRangeMake(0, text1_length)); + CFStringInitInlineBuffer(text2, &text2_inlineBuffer, CFRangeMake(0, text2_length)); + + UniChar char1, char2; + CFIndex n = MIN(text1_length, text2_length); + + for (CFIndex i = 0; i < n; i++) { + char1 = CFStringGetCharacterFromInlineBuffer(&text1_inlineBuffer, i); + char2 = CFStringGetCharacterFromInlineBuffer(&text2_inlineBuffer, i); + + if (char1 != char2) { + return i; + } + } + + return n; +} + +/** + * Determine the common suffix of two strings. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of each string. + */ +CFIndex diff_commonSuffix(CFStringRef text1, CFStringRef text2) { + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + CFIndex text1_length = CFStringGetLength(text1); + CFIndex text2_length = CFStringGetLength(text2); + + CFStringInlineBuffer text1_inlineBuffer, text2_inlineBuffer; + CFStringInitInlineBuffer(text1, &text1_inlineBuffer, CFRangeMake(0, text1_length)); + CFStringInitInlineBuffer(text2, &text2_inlineBuffer, CFRangeMake(0, text2_length)); + + UniChar char1, char2; + CFIndex n = MIN(text1_length, text2_length); + + for (CFIndex i = 1; i <= n; i++) { + char1 = CFStringGetCharacterFromInlineBuffer(&text1_inlineBuffer, (text1_length - i)); + char2 = CFStringGetCharacterFromInlineBuffer(&text2_inlineBuffer, (text2_length - i)); + + if (char1 != char2) { + return i - 1; + } + } + return n; +} + +/** + * Determine if the suffix of one CFStringRef is the prefix of another. + * @param text1 First CFStringRef. + * @param text2 Second CFStringRef. + * @return The number of characters common to the end of the first + * CFStringRef and the start of the second CFStringRef. + */ +CFIndex diff_commonOverlap(CFStringRef text1, CFStringRef text2) { + CFIndex common_overlap = 0; + + // Cache the text lengths to prevent multiple calls. + CFIndex text1_length = CFStringGetLength(text1); + CFIndex text2_length = CFStringGetLength(text2); + + // Eliminate the nil case. + if (text1_length == 0 || text2_length == 0) { + return 0; + } + + // Truncate the longer CFStringRef. + CFStringRef text1_trunc; + CFStringRef text2_trunc; + CFIndex text1_trunc_length; + if (text1_length > text2_length) { + text1_trunc_length = text2_length; + text1_trunc = diff_CFStringCreateRightSubstring(text1, text1_length, text1_trunc_length); + + text2_trunc = CFRetain(text2); + } else if (text1_length < text2_length) { + text1_trunc_length = text1_length; + text1_trunc = CFRetain(text1); + + CFIndex text2_trunc_length = text1_length; + text2_trunc = diff_CFStringCreateLeftSubstring(text2, text2_trunc_length); + } else { + text1_trunc_length = text1_length; + text1_trunc = CFRetain(text1); + + text2_trunc = CFRetain(text2); + } + + CFIndex text_length = MIN(text1_length, text2_length); + // Quick check for the worst case. + if (text1_trunc == text2_trunc) { + common_overlap = text_length; + } else { + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: http://neil.fraser.name/news/2010/11/04/ + CFIndex best = 0; + CFIndex length = 1; + while (true) { + CFStringRef pattern = diff_CFStringCreateRightSubstring(text1_trunc, text1_trunc_length, length); + CFRange foundRange = CFStringFind(text2_trunc, pattern, 0); + CFRelease(pattern); + + CFIndex found = foundRange.location; + if (found == kCFNotFound) { + common_overlap = best; + break; + } + length += found; + + CFStringRef text1_sub = diff_CFStringCreateRightSubstring(text1_trunc, text1_trunc_length, length); + CFStringRef text2_sub = diff_CFStringCreateLeftSubstring(text2_trunc, length); + + if (found == 0 || (CFStringCompare(text1_sub, text2_sub, 0) == kCFCompareEqualTo)) { + best = length; + length++; + } + + CFRelease(text1_sub); + CFRelease(text2_sub); + } + } + + CFRelease(text1_trunc); + CFRelease(text2_trunc); + return common_overlap; +} + +/** + * Do the two texts share a Substring which is at least half the length of + * the longer text? + * This speedup can produce non-minimal diffs. + * @param text1 First CFStringRef. + * @param text2 Second CFStringRef. + * @param diffTimeout Time limit for diff. + * @return Five element String array, containing the prefix of text1, the + * suffix of text1, the prefix of text2, the suffix of text2 and the + * common middle. Or NULL if there was no match. + */ +CFArrayRef diff_halfMatchCreate(CFStringRef text1, CFStringRef text2, const float diffTimeout) { + if (diffTimeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return NULL; + } + CFStringRef longtext = CFStringGetLength(text1) > CFStringGetLength(text2) ? text1 : text2; + CFStringRef shorttext = CFStringGetLength(text1) > CFStringGetLength(text2) ? text2 : text1; + if (CFStringGetLength(longtext) < 4 || CFStringGetLength(shorttext) * 2 < CFStringGetLength(longtext)) { + return NULL; // Pointless. + } + + // First check if the second quarter is the seed for a half-match. + CFArrayRef hm1 = diff_halfMatchICreate(longtext, shorttext, + (CFStringGetLength(longtext) + 3) / 4); + // Check again based on the third quarter. + CFArrayRef hm2 = diff_halfMatchICreate(longtext, shorttext, + (CFStringGetLength(longtext) + 1) / 2); + CFArrayRef hm; + if (hm1 == NULL && hm2 == NULL) { + return NULL; + } else if (hm2 == NULL) { + hm = CFRetain(hm1); + } else if (hm1 == NULL) { + hm = CFRetain(hm2); + } else { + // Both matched. Select the longest. + hm = CFStringGetLength(CFArrayGetValueAtIndex(hm1, 4)) > CFStringGetLength(CFArrayGetValueAtIndex(hm2, 4)) ? CFRetain(hm1) : CFRetain(hm2); + } + + if (hm1 != NULL) { + CFRelease(hm1); + } + if (hm2 != NULL) { + CFRelease(hm2); + } + + // A half-match was found, sort out the return data. + if (CFStringGetLength(text1) > CFStringGetLength(text2)) { + return hm; + //return new CFStringRef[]{hm[0], hm[1], hm[2], hm[3], hm[4]}; + } else { + // { hm[0], hm[1], hm[2], hm[3], hm[4] } + // => { hm[2], hm[3], hm[0], hm[1], hm[4] } + + CFMutableArrayRef hm_mutable = CFArrayCreateMutableCopy(kCFAllocatorDefault, CFArrayGetCount(hm), hm); + CFMakeCollectable(hm_mutable); + + CFRelease(hm); + + CFArrayExchangeValuesAtIndices(hm_mutable, 0, 2); + CFArrayExchangeValuesAtIndices(hm_mutable, 1, 3); + return hm_mutable; + } +} + +/** + * Does a Substring of shorttext exist within longtext such that the + * Substring is at least half the length of longtext? + * @param longtext Longer CFStringRef. + * @param shorttext Shorter CFStringRef. + * @param i Start index of quarter length Substring within longtext. + * @return Five element CFStringRef array, containing the prefix of longtext, the + * suffix of longtext, the prefix of shorttext, the suffix of shorttext + * and the common middle. Or NULL if there was no match. + */ +CFArrayRef diff_halfMatchICreate(CFStringRef longtext, CFStringRef shorttext, CFIndex i) { + // Start with a 1/4 length Substring at position i as a seed. + CFStringRef seed = diff_CFStringCreateSubstring(longtext, i, CFStringGetLength(longtext) / 4); + CFIndex j = -1; + CFStringRef best_common = CFSTR(""); + CFStringRef best_longtext_a = CFSTR(""), best_longtext_b = CFSTR(""); + CFStringRef best_shorttext_a = CFSTR(""), best_shorttext_b = CFSTR(""); + + CFStringRef best_common_part1, best_common_part2; + + CFStringRef longtext_substring, shorttext_substring; + CFIndex shorttext_length = CFStringGetLength(shorttext); + CFRange resultRange; + CFRange rangeToSearch; + rangeToSearch.length = shorttext_length - (j + 1); + rangeToSearch.location = j + 1; + + while (j < CFStringGetLength(shorttext) + && (CFStringFindWithOptions(shorttext, seed, rangeToSearch, 0, &resultRange) == true)) { + j = resultRange.location; + rangeToSearch.length = shorttext_length - (j + 1); + rangeToSearch.location = j + 1; + + longtext_substring = diff_CFStringCreateSubstringWithStartIndex(longtext, i); + shorttext_substring = diff_CFStringCreateSubstringWithStartIndex(shorttext, j); + + CFIndex prefixLength = diff_commonPrefix(longtext_substring, shorttext_substring); + + CFRelease(longtext_substring); + CFRelease(shorttext_substring); + + longtext_substring = diff_CFStringCreateLeftSubstring(longtext, i); + shorttext_substring = diff_CFStringCreateLeftSubstring(shorttext, j); + + CFIndex suffixLength = diff_commonSuffix(longtext_substring, shorttext_substring); + + CFRelease(longtext_substring); + CFRelease(shorttext_substring); + + if (CFStringGetLength(best_common) < suffixLength + prefixLength) { + CFRelease(best_common); + CFRelease(best_longtext_a); + CFRelease(best_longtext_b); + CFRelease(best_shorttext_a); + CFRelease(best_shorttext_b); + + best_common_part1 = diff_CFStringCreateSubstring(shorttext, j - suffixLength, suffixLength); + best_common_part2 = diff_CFStringCreateSubstring(shorttext, j, prefixLength); + + best_common = diff_CFStringCreateByCombiningTwoStrings(best_common_part1, best_common_part2); + + CFRelease(best_common_part1); + CFRelease(best_common_part2); + + best_longtext_a = diff_CFStringCreateLeftSubstring(longtext, i - suffixLength); + best_longtext_b = diff_CFStringCreateSubstringWithStartIndex(longtext, i + prefixLength); + best_shorttext_a = diff_CFStringCreateLeftSubstring(shorttext, j - suffixLength); + best_shorttext_b = diff_CFStringCreateSubstringWithStartIndex(shorttext, j + prefixLength); + } + } + + CFRelease(seed); + + CFArrayRef halfMatchIArray; + if (CFStringGetLength(best_common) * 2 >= CFStringGetLength(longtext)) { + const CFStringRef values[] = { best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common }; + halfMatchIArray = CFArrayCreate(kCFAllocatorDefault, (const void **)values, (sizeof(values) / sizeof(values[0])), &kCFTypeArrayCallBacks); + CFMakeCollectable(halfMatchIArray); + } else { + halfMatchIArray = NULL; + } + + CFRelease(best_common); + CFRelease(best_longtext_a); + CFRelease(best_longtext_b); + CFRelease(best_shorttext_a); + CFRelease(best_shorttext_b); + + return halfMatchIArray; +} + +/** + * Split a text into a list of strings. Reduce the texts to a CFStringRef of + * hashes where each Unicode character represents one line. + * @param text CFString to encode. + * @param lineArray CFMutableArray of unique strings. + * @param lineHash Map of strings to indices. + * @return Encoded CFStringRef. + */ +CFStringRef diff_linesToCharsMungeCFStringCreate(CFStringRef text, CFMutableArrayRef lineArray, CFMutableDictionaryRef lineHash) { + #define lineStart lineStartRange.location + #define lineEnd lineEndRange.location + + CFRange lineStartRange; + CFRange lineEndRange; + lineStart = 0; + lineEnd = -1; + CFStringRef line; + CFMutableStringRef chars = CFStringCreateMutable(kCFAllocatorDefault, 0); + + CFIndex textLength = CFStringGetLength(text); + CFIndex hash; + CFNumberRef hashNumber; + + // Walk the text, pulling out a Substring for each line. + // CFStringCreateArrayBySeparatingStrings(kCFAllocatorDefault, text, CFSTR("\n")) would temporarily double our memory footprint. + // Modifying text would create many large strings. + while (lineEnd < textLength - 1) { + lineStartRange.length = textLength - lineStart; + + if (CFStringFindWithOptions(text, CFSTR("\n"), lineStartRange, 0, &lineEndRange) == false) { + lineEnd = textLength - 1; + } /* else { + lineEnd = lineEndRange.location; + }*/ + + line = diff_CFStringCreateJavaSubstring(text, lineStart, lineEnd + 1); + lineStart = lineEnd + 1; + + if (CFDictionaryContainsKey(lineHash, line)) { + CFDictionaryGetValueIfPresent(lineHash, line, (const void **)&hashNumber); + CFNumberGetValue(hashNumber, kCFNumberCFIndexType, &hash); + const UniChar hashChar = (UniChar)hash; + CFStringAppendCharacters(chars, &hashChar, 1); + } else { + CFArrayAppendValue(lineArray, line); + hash = CFArrayGetCount(lineArray) - 1; + hashNumber = CFNumberCreate(kCFAllocatorDefault, kCFNumberCFIndexType, &hash); + CFMakeCollectable(hashNumber); + CFDictionaryAddValue(lineHash, line, hashNumber); + CFRelease(hashNumber); + const UniChar hashChar = (UniChar)hash; + CFStringAppendCharacters(chars, &hashChar, 1); + } + + CFRelease(line); + } + return chars; + + #undef lineStart + #undef lineEnd +} + +/** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 6 (best) to 0 (worst). + * @param one First CFStringRef. + * @param two Second CFStringRef. + * @return The score. + */ +CFIndex diff_cleanupSemanticScore(CFStringRef one, CFStringRef two) { + static Boolean firstRun = true; + static CFCharacterSetRef alphaNumericSet = NULL; + static CFCharacterSetRef whiteSpaceSet = NULL; + static CFCharacterSetRef controlSet = NULL; + static regex_t blankLineEndRegEx; + static regex_t blankLineStartRegEx; + + if (firstRun) { + // Define some regex patterns for matching boundaries. + alphaNumericSet = CFCharacterSetGetPredefined(kCFCharacterSetAlphaNumeric); + whiteSpaceSet = CFCharacterSetGetPredefined(kCFCharacterSetWhitespaceAndNewline); + controlSet = CFCharacterSetGetPredefined(kCFCharacterSetControl); + int status; + status = regcomp(&blankLineEndRegEx, "\n\r?\n$", REG_EXTENDED | REG_NOSUB); + assert(status == 0); + status = regcomp(&blankLineStartRegEx, "^\r?\n\r?\n", REG_EXTENDED | REG_NOSUB); + assert(status == 0); + firstRun = false; + } + + if (CFStringGetLength(one) == 0 || CFStringGetLength(two) == 0) { + // Edges are the best. + return 6; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + UniChar char1 = + CFStringGetCharacterAtIndex(one, (CFStringGetLength(one) - 1)); + UniChar char2 = + CFStringGetCharacterAtIndex(two, 0); + Boolean nonAlphaNumeric1 = + !CFCharacterSetIsCharacterMember(alphaNumericSet, char1); + Boolean nonAlphaNumeric2 = + !CFCharacterSetIsCharacterMember(alphaNumericSet, char2); + Boolean whitespace1 = + nonAlphaNumeric1 && CFCharacterSetIsCharacterMember(whiteSpaceSet, char1); + Boolean whitespace2 = + nonAlphaNumeric2 && CFCharacterSetIsCharacterMember(whiteSpaceSet, char2); + Boolean lineBreak1 = + whitespace1 && CFCharacterSetIsCharacterMember(controlSet, char1); + Boolean lineBreak2 = + whitespace2 && CFCharacterSetIsCharacterMember(controlSet, char2); + Boolean blankLine1 = + lineBreak1 && diff_regExMatch(one, &blankLineEndRegEx); + Boolean blankLine2 = + lineBreak2 && diff_regExMatch(two, &blankLineStartRegEx); + + if (blankLine1 || blankLine2) { + // Five points for blank lines. + return 5; + } else if (lineBreak1 || lineBreak2) { + // Four points for line breaks. + return 4; + } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { + // Three points for end of sentences. + return 3; + } else if (whitespace1 || whitespace2) { + // Two points for whitespace. + return 2; + } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { + // One point for non-alphanumeric. + return 1; + } + return 0; +} diff --git a/objectivec/DiffMatchPatchCFUtilities.h b/objectivec/DiffMatchPatchCFUtilities.h new file mode 100755 index 0000000..af75506 --- /dev/null +++ b/objectivec/DiffMatchPatchCFUtilities.h @@ -0,0 +1,48 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#ifndef _DIFFMATCHPATCHCFUTILITIES_H +#define _DIFFMATCHPATCHCFUTILITIES_H + +CFStringRef diff_CFStringCreateFromUnichar(UniChar ch); +CFStringRef diff_CFStringCreateJavaSubstring(CFStringRef s, CFIndex begin, CFIndex end); + +CFIndex diff_commonPrefix(CFStringRef text1, CFStringRef text2); +CFIndex diff_commonSuffix(CFStringRef text1, CFStringRef text2); +CFIndex diff_commonOverlap(CFStringRef text1, CFStringRef text2); +CFArrayRef diff_halfMatchCreate(CFStringRef text1, CFStringRef text2, const float diffTimeout); +CFArrayRef diff_halfMatchICreate(CFStringRef longtext, CFStringRef shorttext, CFIndex i); + +CFStringRef diff_linesToCharsMungeCFStringCreate(CFStringRef text, CFMutableArrayRef lineArray, CFMutableDictionaryRef lineHash); + +CFIndex diff_cleanupSemanticScore(CFStringRef one, CFStringRef two); + +CF_INLINE void diff_CFStringPrepareUniCharBuffer(CFStringRef string, const UniChar **string_chars, UniChar **string_buffer, CFRange string_range) { + *string_chars = CFStringGetCharactersPtr(string); + if (*string_chars == NULL) { + // Fallback in case CFStringGetCharactersPtr() didn’t work. + *string_buffer = malloc(string_range.length * sizeof(UniChar)); + CFStringGetCharacters(string, string_range, *string_buffer); + *string_chars = *string_buffer; + } +} + +#endif //ifndef _DIFFMATCHPATCHCFUTILITIES_H diff --git a/objectivec/DiffMatchPatch_Prefix.pch b/objectivec/DiffMatchPatch_Prefix.pch new file mode 100755 index 0000000..2077572 --- /dev/null +++ b/objectivec/DiffMatchPatch_Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'DiffMatchPatch' target in the 'DiffMatchPatch' project. +// + +#ifdef __OBJC__ + #import +#endif diff --git a/objectivec/English.lproj/InfoPlist.strings b/objectivec/English.lproj/InfoPlist.strings new file mode 100755 index 0000000..88f65cf --- /dev/null +++ b/objectivec/English.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/objectivec/Info.plist b/objectivec/Info.plist new file mode 100755 index 0000000..5d7c307 --- /dev/null +++ b/objectivec/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + de.geheimwerk.${PRODUCT_NAME:rfc1034Identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + ${MARKETING_VERSION} + CFBundleSignature + ???? + CFBundleVersion + ${PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/objectivec/MinMaxMacros.h b/objectivec/MinMaxMacros.h new file mode 100755 index 0000000..2765e0f --- /dev/null +++ b/objectivec/MinMaxMacros.h @@ -0,0 +1,40 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#if !defined(MIN) + #define MIN(A,B) \ + ({__typeof__(A) a = (A); \ + __typeof__(B) b = (B); \ + (a < b) ? a : b; }) +#endif + +#if !defined(MAX) + #define MAX(A,B) \ + ({__typeof__(A) a = (A); \ + __typeof__(B) b = (B); \ + (a > b) ? a : b; }) +#endif + +#if !defined(ABS) + #define ABS(A) \ + ({__typeof__(A) a = (A); \ + (a > 0) ? a : -a; }) +#endif diff --git a/objectivec/NSMutableDictionary+DMPExtensions.h b/objectivec/NSMutableDictionary+DMPExtensions.h new file mode 100755 index 0000000..6230424 --- /dev/null +++ b/objectivec/NSMutableDictionary+DMPExtensions.h @@ -0,0 +1,46 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#import + + +@interface NSMutableDictionary (DMPExtensions) + +- (id)diff_objectForIntegerKey:(NSInteger)keyInteger; +- (id)diff_objectForUnsignedIntegerKey:(NSUInteger)keyUInteger; +- (id)diff_objectForUnicharKey:(unichar)aUnicharKey; + +- (NSInteger)diff_integerForKey:(id)aKey; +- (NSUInteger)diff_unsignedIntegerForKey:(id)aKey; +- (NSInteger)diff_integerForIntegerKey:(NSInteger)keyInteger; +- (NSUInteger)diff_unsignedIntegerForUnicharKey:(unichar)aUnicharKey; + +- (BOOL)diff_containsObjectForKey:(id)aKey; +- (BOOL)diff_containsObjectForUnicharKey:(unichar)aUnicharKey; + +- (void)diff_setIntegerValue:(NSInteger)anInteger forKey:(id)aKey; +- (void)diff_setIntegerValue:(NSInteger)anInteger forIntegerKey:(NSInteger)keyInteger; + +- (void)diff_setUnsignedIntegerValue:(NSUInteger)anUInteger forKey:(id)aKey; +- (void)diff_setUnsignedIntegerValue:(NSUInteger)anUInteger forUnsignedIntegerKey:(NSUInteger)keyUInteger; +- (void)diff_setUnsignedIntegerValue:(NSUInteger)anUInteger forUnicharKey:(unichar)aUnicharKey; + +@end diff --git a/objectivec/NSMutableDictionary+DMPExtensions.m b/objectivec/NSMutableDictionary+DMPExtensions.m new file mode 100755 index 0000000..dc092be --- /dev/null +++ b/objectivec/NSMutableDictionary+DMPExtensions.m @@ -0,0 +1,108 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#import "NSMutableDictionary+DMPExtensions.h" + +#import "NSString+UnicharUtilities.h" + + +@implementation NSMutableDictionary (DMPExtensions) + +- (id)diff_objectForIntegerKey:(NSInteger)keyInteger; +{ + return [self objectForKey:[NSNumber numberWithInteger:keyInteger]]; +} + +- (id)diff_objectForUnsignedIntegerKey:(NSUInteger)keyUInteger; +{ + return [self objectForKey:[NSNumber numberWithUnsignedInteger:keyUInteger]]; +} + +- (id)diff_objectForUnicharKey:(unichar)aUnicharKey; +{ + return [self objectForKey:[NSString diff_stringFromUnichar:aUnicharKey]]; +} + + +- (NSInteger)diff_integerForKey:(id)aKey; +{ + return [((NSNumber *)[self objectForKey:aKey]) integerValue]; +} + +- (NSUInteger)diff_unsignedIntegerForKey:(id)aKey; +{ + return [((NSNumber *)[self objectForKey:aKey]) unsignedIntegerValue]; +} + +- (NSInteger)diff_integerForIntegerKey:(NSInteger)keyInteger; +{ + return [((NSNumber *)[self objectForKey:[NSNumber numberWithInteger:keyInteger]]) integerValue]; +} + +- (NSUInteger)diff_unsignedIntegerForUnicharKey:(unichar)aUnicharKey; +{ + return [((NSNumber *)[self diff_objectForUnicharKey:aUnicharKey]) unsignedIntegerValue]; +} + + +- (BOOL)diff_containsObjectForKey:(id)aKey; +{ + return ([self objectForKey:aKey] != nil); +} + +- (BOOL)containsObjectForIntegerKey:(NSInteger)keyInteger; +{ + return ([self objectForKey:[NSNumber numberWithInteger:keyInteger]] != nil); +} + +- (BOOL)diff_containsObjectForUnicharKey:(unichar)aUnicharKey; +{ + return ([self diff_objectForUnicharKey:aUnicharKey] != nil); +} + + +- (void)diff_setIntegerValue:(NSInteger)anInteger forKey:(id)aKey; +{ + [self setObject:[NSNumber numberWithInteger:anInteger] forKey:aKey]; +} + +- (void)diff_setIntegerValue:(NSInteger)anInteger forIntegerKey:(NSInteger)keyInteger; +{ + [self setObject:[NSNumber numberWithInteger:anInteger] forKey:[NSNumber numberWithInteger:keyInteger]]; +} + + +- (void)diff_setUnsignedIntegerValue:(NSUInteger)anUInteger forKey:(id)aKey; +{ + [self setObject:[NSNumber numberWithUnsignedInteger:anUInteger] forKey:aKey]; +} + +- (void)diff_setUnsignedIntegerValue:(NSUInteger)anUInteger forUnsignedIntegerKey:(NSUInteger)keyUInteger; +{ + [self setObject:[NSNumber numberWithUnsignedInteger:anUInteger] forKey:[NSNumber numberWithUnsignedInteger:keyUInteger]]; +} + +- (void)diff_setUnsignedIntegerValue:(NSUInteger)anUInteger forUnicharKey:(unichar)aUnicharKey; +{ + [self setObject:[NSNumber numberWithUnsignedInteger:anUInteger] forKey:[NSString diff_stringFromUnichar:aUnicharKey]]; +} + +@end diff --git a/objectivec/NSString+JavaSubstring.h b/objectivec/NSString+JavaSubstring.h new file mode 100755 index 0000000..3f6485c --- /dev/null +++ b/objectivec/NSString+JavaSubstring.h @@ -0,0 +1,29 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#import + + +@interface NSString (JavaSubstring) + +- (NSString *)diff_javaSubstringFromStart:(NSUInteger)start toEnd:(NSUInteger)end; + +@end diff --git a/objectivec/NSString+JavaSubstring.m b/objectivec/NSString+JavaSubstring.m new file mode 100755 index 0000000..8c17dbd --- /dev/null +++ b/objectivec/NSString+JavaSubstring.m @@ -0,0 +1,35 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#import "NSString+JavaSubstring.h" + +#import "DiffMatchPatchCFUtilities.h" + +@implementation NSString (JavaSubstring) + +- (NSString *)diff_javaSubstringFromStart:(NSUInteger)start toEnd:(NSUInteger)end; +{ + CFStringRef c = diff_CFStringCreateJavaSubstring((CFStringRef)self, (CFIndex)start, (CFIndex)end); + CFMakeCollectable(c); + return [(NSString *)c autorelease]; +} + +@end diff --git a/objectivec/NSString+UnicharUtilities.h b/objectivec/NSString+UnicharUtilities.h new file mode 100755 index 0000000..867196e --- /dev/null +++ b/objectivec/NSString+UnicharUtilities.h @@ -0,0 +1,30 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#import + + +@interface NSString (UnicharUtilities) + ++ (NSString *)diff_stringFromUnichar:(unichar)ch; +- (NSString *)diff_substringWithCharacterAtIndex:(NSUInteger)anIndex; + +@end diff --git a/objectivec/NSString+UnicharUtilities.m b/objectivec/NSString+UnicharUtilities.m new file mode 100755 index 0000000..a45d366 --- /dev/null +++ b/objectivec/NSString+UnicharUtilities.m @@ -0,0 +1,39 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#import "NSString+UnicharUtilities.h" + + +@implementation NSString (UnicharUtilities) + ++ (NSString *)diff_stringFromUnichar:(unichar)ch; +{ + CFStringRef c = CFStringCreateWithCharacters(kCFAllocatorDefault, &ch, 1); + CFMakeCollectable(c); + return [(NSString *)c autorelease]; +} + +- (NSString *)diff_substringWithCharacterAtIndex:(NSUInteger)anIndex; +{ + return [self substringWithRange:NSMakeRange(anIndex, 1)]; +} + +@end diff --git a/objectivec/NSString+UriCompatibility.h b/objectivec/NSString+UriCompatibility.h new file mode 100755 index 0000000..75c7873 --- /dev/null +++ b/objectivec/NSString+UriCompatibility.h @@ -0,0 +1,30 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#import + + +@interface NSString (UriCompatibility) + +- (NSString *)diff_stringByAddingPercentEscapesForEncodeUriCompatibility; +- (NSString *)diff_stringByReplacingPercentEscapesForEncodeUriCompatibility; + +@end diff --git a/objectivec/NSString+UriCompatibility.m b/objectivec/NSString+UriCompatibility.m new file mode 100755 index 0000000..44d4a78 --- /dev/null +++ b/objectivec/NSString+UriCompatibility.m @@ -0,0 +1,62 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#import "NSString+UriCompatibility.h" + + +@implementation NSString (UriCompatibility) + +/** + * Escape excluding selected chars for compatability with JavaScript's encodeURI. + * This method produces uppercase hex. + * + * @param str The CFStringRef to escape. + * @return The escaped CFStringRef. + */ +- (NSString *)diff_stringByAddingPercentEscapesForEncodeUriCompatibility; +{ + CFStringRef urlString = CFURLCreateStringByAddingPercentEscapes(NULL, + (CFStringRef)self, + CFSTR(" !~*'();/?:@&=+$,#"), + NULL, + kCFStringEncodingUTF8); + CFMakeCollectable(urlString); + return [(NSString *)urlString autorelease]; +} + +/** + * Unescape all percent escapes. + * + * Example: "%3f" -> "?", "%24" -> "$", etc. + * + * @return The unescaped NSString. + */ +- (NSString *)diff_stringByReplacingPercentEscapesForEncodeUriCompatibility; +{ + CFStringRef decodedString = CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, + (CFStringRef)self, + CFSTR(""), + kCFStringEncodingUTF8); + CFMakeCollectable(decodedString); + return [(NSString *)decodedString autorelease]; +} + +@end diff --git a/objectivec/Speedtest1.txt b/objectivec/Speedtest1.txt new file mode 100644 index 0000000..54b438f --- /dev/null +++ b/objectivec/Speedtest1.txt @@ -0,0 +1,230 @@ +This is a '''list of newspapers published by [[Journal Register Company]]'''. + +The company owns daily and weekly newspapers, other print media properties and newspaper-affiliated local Websites in the [[U.S.]] states of [[Connecticut]], [[Michigan]], [[New York]], [[Ohio]] and [[Pennsylvania]], organized in six geographic "clusters":[http://www.journalregister.com/newspapers.html Journal Register Company: Our Newspapers], accessed February 10, 2008. + +== Capital-Saratoga == +Three dailies, associated weeklies and [[pennysaver]]s in greater [[Albany, New York]]; also [http://www.capitalcentral.com capitalcentral.com] and [http://www.jobsinnewyork.com JobsInNewYork.com]. + +* ''The Oneida Daily Dispatch'' {{WS|oneidadispatch.com}} of [[Oneida, New York]] +* ''[[The Record (Troy)|The Record]]'' {{WS|troyrecord.com}} of [[Troy, New York]] +* ''[[The Saratogian]]'' {{WS|saratogian.com}} of [[Saratoga Springs, New York]] +* Weeklies: +** ''Community News'' {{WS|cnweekly.com}} weekly of [[Clifton Park, New York]] +** ''Rome Observer'' of [[Rome, New York]] +** ''Life & Times of Utica'' of [[Utica, New York]] + +== Connecticut == +Five dailies, associated weeklies and [[pennysaver]]s in the state of [[Connecticut]]; also [http://www.ctcentral.com CTcentral.com], [http://www.ctcarsandtrucks.com CTCarsAndTrucks.com] and [http://www.jobsinct.com JobsInCT.com]. + +* ''The Middletown Press'' {{WS|middletownpress.com}} of [[Middletown, Connecticut|Middletown]] +* ''[[New Haven Register]]'' {{WS|newhavenregister.com}} of [[New Haven, Connecticut|New Haven]] +* ''The Register Citizen'' {{WS|registercitizen.com}} of [[Torrington, Connecticut|Torrington]] + +* [[New Haven Register#Competitors|Elm City Newspapers]] {{WS|ctcentral.com}} +** ''The Advertiser'' of [[East Haven, Connecticut|East Haven]] +** ''Hamden Chronicle'' of [[Hamden, Connecticut|Hamden]] +** ''Milford Weekly'' of [[Milford, Connecticut|Milford]] +** ''The Orange Bulletin'' of [[Orange, Connecticut|Orange]] +** ''The Post'' of [[North Haven, Connecticut|North Haven]] +** ''Shelton Weekly'' of [[Shelton, Connecticut|Shelton]] +** ''The Stratford Bard'' of [[Stratford, Connecticut|Stratford]] +** ''Wallingford Voice'' of [[Wallingford, Connecticut|Wallingford]] +** ''West Haven News'' of [[West Haven, Connecticut|West Haven]] +* Housatonic Publications +** ''The New Milford Times'' {{WS|newmilfordtimes.com}} of [[New Milford, Connecticut|New Milford]] +** ''The Brookfield Journal'' of [[Brookfield, Connecticut|Brookfield]] +** ''The Kent Good Times Dispatch'' of [[Kent, Connecticut|Kent]] +** ''The Bethel Beacon'' of [[Bethel, Connecticut|Bethel]] +** ''The Litchfield Enquirer'' of [[Litchfield, Connecticut|Litchfield]] +** ''Litchfield County Times'' of [[Litchfield, Connecticut|Litchfield]] +* Imprint Newspapers {{WS|imprintnewspapers.com}} +** ''West Hartford News'' of [[West Hartford, Connecticut|West Hartford]] +** ''Windsor Journal'' of [[Windsor, Connecticut|Windsor]] +** ''Windsor Locks Journal'' of [[Windsor Locks, Connecticut|Windsor Locks]] +** ''Avon Post'' of [[Avon, Connecticut|Avon]] +** ''Farmington Post'' of [[Farmington, Connecticut|Farmington]] +** ''Simsbury Post'' of [[Simsbury, Connecticut|Simsbury]] +** ''Tri-Town Post'' of [[Burlington, Connecticut|Burlington]], [[Canton, Connecticut|Canton]] and [[Harwinton, Connecticut|Harwinton]] +* Minuteman Publications +** ''[[Fairfield Minuteman]]'' of [[Fairfield, Connecticut|Fairfield]] +** ''The Westport Minuteman'' {{WS|westportminuteman.com}} of [[Westport, Connecticut|Westport]] +* Shoreline Newspapers weeklies: +** ''Branford Review'' of [[Branford, Connecticut|Branford]] +** ''Clinton Recorder'' of [[Clinton, Connecticut|Clinton]] +** ''The Dolphin'' of [[Naval Submarine Base New London]] in [[New London, Connecticut|New London]] +** ''Main Street News'' {{WS|ctmainstreetnews.com}} of [[Essex, Connecticut|Essex]] +** ''Pictorial Gazette'' of [[Old Saybrook, Connecticut|Old Saybrook]] +** ''Regional Express'' of [[Colchester, Connecticut|Colchester]] +** ''Regional Standard'' of [[Colchester, Connecticut|Colchester]] +** ''Shoreline Times'' {{WS|shorelinetimes.com}} of [[Guilford, Connecticut|Guilford]] +** ''Shore View East'' of [[Madison, Connecticut|Madison]] +** ''Shore View West'' of [[Guilford, Connecticut|Guilford]] +* Other weeklies: +** ''Registro'' {{WS|registroct.com}} of [[New Haven, Connecticut|New Haven]] +** ''Thomaston Express'' {{WS|thomastownexpress.com}} of [[Thomaston, Connecticut|Thomaston]] +** ''Foothills Traders'' {{WS|foothillstrader.com}} of Torrington, Bristol, Canton + +== Michigan == +Four dailies, associated weeklies and [[pennysaver]]s in the state of [[Michigan]]; also [http://www.micentralhomes.com MIcentralhomes.com] and [http://www.micentralautos.com MIcentralautos.com] +* ''[[Oakland Press]]'' {{WS|theoaklandpress.com}} of [[Oakland, Michigan|Oakland]] +* ''Daily Tribune'' {{WS|dailytribune.com}} of [[Royal Oak, Michigan|Royal Oak]] +* ''Macomb Daily'' {{WS|macombdaily.com}} of [[Mt. Clemens, Michigan|Mt. Clemens]] +* ''[[Morning Sun]]'' {{WS|themorningsun.com}} of [[Mount Pleasant, Michigan|Mount Pleasant]] +* Heritage Newspapers {{WS|heritage.com}} +** ''Belleville View'' +** ''Ile Camera'' +** ''Monroe Guardian'' +** ''Ypsilanti Courier'' +** ''News-Herald'' +** ''Press & Guide'' +** ''Chelsea Standard & Dexter Leader'' +** ''Manchester Enterprise'' +** ''Milan News-Leader'' +** ''Saline Reporter'' +* Independent Newspapers {{WS|sourcenewspapers.com}} +** ''Advisor'' +** ''Source'' +* Morning Star {{WS|morningstarpublishing.com}} +** ''Alma Reminder'' +** ''Alpena Star'' +** ''Antrim County News'' +** ''Carson City Reminder'' +** ''The Leader & Kalkaskian'' +** ''Ogemaw/Oscoda County Star'' +** ''Petoskey/Charlevoix Star'' +** ''Presque Isle Star'' +** ''Preview Community Weekly'' +** ''Roscommon County Star'' +** ''St. Johns Reminder'' +** ''Straits Area Star'' +** ''The (Edmore) Advertiser'' +* Voice Newspapers {{WS|voicenews.com}} +** ''Armada Times'' +** ''Bay Voice'' +** ''Blue Water Voice'' +** ''Downriver Voice'' +** ''Macomb Township Voice'' +** ''North Macomb Voice'' +** ''Weekend Voice'' +** ''Suburban Lifestyles'' {{WS|suburbanlifestyles.com}} + +== Mid-Hudson == +One daily, associated magazines in the [[Hudson River Valley]] of [[New York]]; also [http://www.midhudsoncentral.com MidHudsonCentral.com] and [http://www.jobsinnewyork.com JobsInNewYork.com]. + +* ''[[Daily Freeman]]'' {{WS|dailyfreeman.com}} of [[Kingston, New York]] + +== Ohio == +Two dailies, associated magazines and three shared Websites, all in the state of [[Ohio]]: [http://www.allaroundcleveland.com AllAroundCleveland.com], [http://www.allaroundclevelandcars.com AllAroundClevelandCars.com] and [http://www.allaroundclevelandjobs.com AllAroundClevelandJobs.com]. + +* ''[[The News-Herald (Ohio)|The News-Herald]]'' {{WS|news-herald.com}} of [[Willoughby, Ohio|Willoughby]] +* ''[[The Morning Journal]]'' {{WS|morningjournal.com}} of [[Lorain, Ohio|Lorain]] + +== Philadelphia area == +Seven dailies and associated weeklies and magazines in [[Pennsylvania]] and [[New Jersey]], and associated Websites: [http://www.allaroundphilly.com AllAroundPhilly.com], [http://www.jobsinnj.com JobsInNJ.com], [http://www.jobsinpa.com JobsInPA.com], and [http://www.phillycarsearch.com PhillyCarSearch.com]. + +* ''The Daily Local'' {{WS|dailylocal.com}} of [[West Chester, Pennsylvania|West Chester]] +* ''[[Delaware County Daily and Sunday Times]] {{WS|delcotimes.com}} of Primos +* ''[[The Mercury (Pennsylvania)|The Mercury]]'' {{WS|pottstownmercury.com}} of [[Pottstown, Pennsylvania|Pottstown]] +* ''The Phoenix'' {{WS|phoenixvillenews.com}} of [[Phoenixville, Pennsylvania|Phoenixville]] +* ''[[The Reporter (Lansdale)|The Reporter]]'' {{WS|thereporteronline.com}} of [[Lansdale, Pennsylvania|Lansdale]] +* ''The Times Herald'' {{WS|timesherald.com}} of [[Norristown, Pennsylvania|Norristown]] +* ''[[The Trentonian]]'' {{WS|trentonian.com}} of [[Trenton, New Jersey]] + +* Weeklies +** ''El Latino Expreso'' of [[Trenton, New Jersey]] +** ''La Voz'' of [[Norristown, Pennsylvania]] +** ''The Village News'' of [[Downingtown, Pennsylvania]] +** ''The Times Record'' of [[Kennett Square, Pennsylvania]] +** ''The Tri-County Record'' {{WS|tricountyrecord.com}} of [[Morgantown, Pennsylvania]] +** ''News of Delaware County'' {{WS|newsofdelawarecounty.com}}of [[Havertown, Pennsylvania]] +** ''Main Line Times'' {{WS|mainlinetimes.com}}of [[Ardmore, Pennsylvania]] +** ''Penny Pincher'' of [[Pottstown, Pennsylvania]] +** ''Town Talk'' {{WS|towntalknews.com}} of [[Ridley, Pennsylvania]] +* Chesapeake Publishing {{WS|pa8newsgroup.com}} +** ''Solanco Sun Ledger'' of [[Quarryville, Pennsylvania]] +** ''Columbia Ledger'' of [[Columbia, Pennsylvania]] +** ''Coatesville Ledger'' of [[Downingtown, Pennsylvania]] +** ''Parkesburg Post Ledger'' of [[Quarryville, Pennsylvania]] +** ''Downingtown Ledger'' of [[Downingtown, Pennsylvania]] +** ''The Kennett Paper'' of [[Kennett Square, Pennsylvania]] +** ''Avon Grove Sun'' of [[West Grove, Pennsylvania]] +** ''Oxford Tribune'' of [[Oxford, Pennsylvania]] +** ''Elizabethtown Chronicle'' of [[Elizabethtown, Pennsylvania]] +** ''Donegal Ledger'' of [[Donegal, Pennsylvania]] +** ''Chadds Ford Post'' of [[Chadds Ford, Pennsylvania]] +** ''The Central Record'' of [[Medford, New Jersey]] +** ''Maple Shade Progress'' of [[Maple Shade, New Jersey]] +* Intercounty Newspapers {{WS|buckslocalnews.com}} +** ''The Review'' of Roxborough, Pennsylvania +** ''The Recorder'' of [[Conshohocken, Pennsylvania]] +** ''The Leader'' of [[Mount Airy, Pennsylvania|Mount Airy]] and West Oak Lake, Pennsylvania +** ''The Pennington Post'' of [[Pennington, New Jersey]] +** ''The Bristol Pilot'' of [[Bristol, Pennsylvania]] +** ''Yardley News'' of [[Yardley, Pennsylvania]] +** ''New Hope Gazette'' of [[New Hope, Pennsylvania]] +** ''Doylestown Patriot'' of [[Doylestown, Pennsylvania]] +** ''Newtown Advance'' of [[Newtown, Pennsylvania]] +** ''The Plain Dealer'' of [[Williamstown, New Jersey]] +** ''News Report'' of [[Sewell, New Jersey]] +** ''Record Breeze'' of [[Berlin, New Jersey]] +** ''Newsweekly'' of [[Moorestown, New Jersey]] +** ''Haddon Herald'' of [[Haddonfield, New Jersey]] +** ''New Egypt Press'' of [[New Egypt, New Jersey]] +** ''Community News'' of [[Pemberton, New Jersey]] +** ''Plymouth Meeting Journal'' of [[Plymouth Meeting, Pennsylvania]] +** ''Lafayette Hill Journal'' of [[Lafayette Hill, Pennsylvania]] +* Montgomery Newspapers {{WS|montgomerynews.com}} +** ''Ambler Gazette'' of [[Ambler, Pennsylvania]] +** ''Central Bucks Life'' of [[Bucks County, Pennsylvania]] +** ''The Colonial'' of [[Plymouth Meeting, Pennsylvania]] +** ''Glenside News'' of [[Glenside, Pennsylvania]] +** ''The Globe'' of [[Lower Moreland Township, Pennsylvania]] +** ''Main Line Life'' of [[Ardmore, Pennsylvania]] +** ''Montgomery Life'' of [[Fort Washington, Pennsylvania]] +** ''North Penn Life'' of [[Lansdale, Pennsylvania]] +** ''Perkasie News Herald'' of [[Perkasie, Pennsylvania]] +** ''Public Spirit'' of [[Hatboro, Pennsylvania]] +** ''Souderton Independent'' of [[Souderton, Pennsylvania]] +** ''Springfield Sun'' of [[Springfield, Pennsylvania]] +** ''Spring-Ford Reporter'' of [[Royersford, Pennsylvania]] +** ''Times Chronicle'' of [[Jenkintown, Pennsylvania]] +** ''Valley Item'' of [[Perkiomenville, Pennsylvania]] +** ''Willow Grove Guide'' of [[Willow Grove, Pennsylvania]] +* News Gleaner Publications (closed December 2008) {{WS|newsgleaner.com}} +** ''Life Newspapers'' of [[Philadelphia, Pennsylvania]] +* Suburban Publications +** ''The Suburban & Wayne Times'' {{WS|waynesuburban.com}} of [[Wayne, Pennsylvania]] +** ''The Suburban Advertiser'' of [[Exton, Pennsylvania]] +** ''The King of Prussia Courier'' of [[King of Prussia, Pennsylvania]] +* Press Newspapers {{WS|countypressonline.com}} +** ''County Press'' of [[Newtown Square, Pennsylvania]] +** ''Garnet Valley Press'' of [[Glen Mills, Pennsylvania]] +** ''Haverford Press'' of [[Newtown Square, Pennsylvania]] (closed January 2009) +** ''Hometown Press'' of [[Glen Mills, Pennsylvania]] (closed January 2009) +** ''Media Press'' of [[Newtown Square, Pennsylvania]] (closed January 2009) +** ''Springfield Press'' of [[Springfield, Pennsylvania]] +* Berks-Mont Newspapers {{WS|berksmontnews.com}} +** ''The Boyertown Area Times'' of [[Boyertown, Pennsylvania]] +** ''The Kutztown Area Patriot'' of [[Kutztown, Pennsylvania]] +** ''The Hamburg Area Item'' of [[Hamburg, Pennsylvania]] +** ''The Southern Berks News'' of [[Exeter Township, Berks County, Pennsylvania]] +** ''The Free Press'' of [[Quakertown, Pennsylvania]] +** ''The Saucon News'' of [[Quakertown, Pennsylvania]] +** ''Westside Weekly'' of [[Reading, Pennsylvania]] + +* Magazines +** ''Bucks Co. Town & Country Living'' +** ''Chester Co. Town & Country Living'' +** ''Montomgery Co. Town & Country Living'' +** ''Garden State Town & Country Living'' +** ''Montgomery Homes'' +** ''Philadelphia Golfer'' +** ''Parents Express'' +** ''Art Matters'' + +{{JRC}} + +==References== + + +[[Category:Journal Register publications|*]] diff --git a/objectivec/Speedtest2.txt b/objectivec/Speedtest2.txt new file mode 100644 index 0000000..8f25a80 --- /dev/null +++ b/objectivec/Speedtest2.txt @@ -0,0 +1,188 @@ +This is a '''list of newspapers published by [[Journal Register Company]]'''. + +The company owns daily and weekly newspapers, other print media properties and newspaper-affiliated local Websites in the [[U.S.]] states of [[Connecticut]], [[Michigan]], [[New York]], [[Ohio]], [[Pennsylvania]] and [[New Jersey]], organized in six geographic "clusters":[http://www.journalregister.com/publications.html Journal Register Company: Our Publications], accessed April 21, 2010. + +== Capital-Saratoga == +Three dailies, associated weeklies and [[pennysaver]]s in greater [[Albany, New York]]; also [http://www.capitalcentral.com capitalcentral.com] and [http://www.jobsinnewyork.com JobsInNewYork.com]. + +* ''The Oneida Daily Dispatch'' {{WS|oneidadispatch.com}} of [[Oneida, New York]] +* ''[[The Record (Troy)|The Record]]'' {{WS|troyrecord.com}} of [[Troy, New York]] +* ''[[The Saratogian]]'' {{WS|saratogian.com}} of [[Saratoga Springs, New York]] +* Weeklies: +** ''Community News'' {{WS|cnweekly.com}} weekly of [[Clifton Park, New York]] +** ''Rome Observer'' {{WS|romeobserver.com}} of [[Rome, New York]] +** ''WG Life '' {{WS|saratogian.com/wglife/}} of [[Wilton, New York]] +** ''Ballston Spa Life '' {{WS|saratogian.com/bspalife}} of [[Ballston Spa, New York]] +** ''Greenbush Life'' {{WS|troyrecord.com/greenbush}} of [[Troy, New York]] +** ''Latham Life'' {{WS|troyrecord.com/latham}} of [[Latham, New York]] +** ''River Life'' {{WS|troyrecord.com/river}} of [[Troy, New York]] + +== Connecticut == +Three dailies, associated weeklies and [[pennysaver]]s in the state of [[Connecticut]]; also [http://www.ctcentral.com CTcentral.com], [http://www.ctcarsandtrucks.com CTCarsAndTrucks.com] and [http://www.jobsinct.com JobsInCT.com]. + +* ''The Middletown Press'' {{WS|middletownpress.com}} of [[Middletown, Connecticut|Middletown]] +* ''[[New Haven Register]]'' {{WS|newhavenregister.com}} of [[New Haven, Connecticut|New Haven]] +* ''The Register Citizen'' {{WS|registercitizen.com}} of [[Torrington, Connecticut|Torrington]] + +* Housatonic Publications +** ''The Housatonic Times'' {{WS|housatonictimes.com}} of [[New Milford, Connecticut|New Milford]] +** ''Litchfield County Times'' {{WS|countytimes.com}} of [[Litchfield, Connecticut|Litchfield]] + +* Minuteman Publications +** ''[[Fairfield Minuteman]]'' {{WS|fairfieldminuteman.com}}of [[Fairfield, Connecticut|Fairfield]] +** ''The Westport Minuteman'' {{WS|westportminuteman.com}} of [[Westport, Connecticut|Westport]] + +* Shoreline Newspapers +** ''The Dolphin'' {{WS|dolphin-news.com}} of [[Naval Submarine Base New London]] in [[New London, Connecticut|New London]] +** ''Shoreline Times'' {{WS|shorelinetimes.com}} of [[Guilford, Connecticut|Guilford]] + +* Foothills Media Group {{WS|foothillsmediagroup.com}} +** ''Thomaston Express'' {{WS|thomastonexpress.com}} of [[Thomaston, Connecticut|Thomaston]] +** ''Good News About Torrington'' {{WS|goodnewsabouttorrington.com}} of [[Torrington, Connecticut|Torrington]] +** ''Granby News'' {{WS|foothillsmediagroup.com/granby}} of [[Granby, Connecticut|Granby]] +** ''Canton News'' {{WS|foothillsmediagroup.com/canton}} of [[Canton, Connecticut|Canton]] +** ''Avon News'' {{WS|foothillsmediagroup.com/avon}} of [[Avon, Connecticut|Avon]] +** ''Simsbury News'' {{WS|foothillsmediagroup.com/simsbury}} of [[Simsbury, Connecticut|Simsbury]] +** ''Litchfield News'' {{WS|foothillsmediagroup.com/litchfield}} of [[Litchfield, Connecticut|Litchfield]] +** ''Foothills Trader'' {{WS|foothillstrader.com}} of Torrington, Bristol, Canton + +* Other weeklies +** ''The Milford-Orange Bulletin'' {{WS|ctbulletin.com}} of [[Orange, Connecticut|Orange]] +** ''The Post-Chronicle'' {{WS|ctpostchronicle.com}} of [[North Haven, Connecticut|North Haven]] +** ''West Hartford News'' {{WS|westhartfordnews.com}} of [[West Hartford, Connecticut|West Hartford]] + +* Magazines +** ''The Connecticut Bride'' {{WS|connecticutmag.com}} +** ''Connecticut Magazine'' {{WS|theconnecticutbride.com}} +** ''Passport Magazine'' {{WS|passport-mag.com}} + +== Michigan == +Four dailies, associated weeklies and [[pennysaver]]s in the state of [[Michigan]]; also [http://www.micentralhomes.com MIcentralhomes.com] and [http://www.micentralautos.com MIcentralautos.com] +* ''[[Oakland Press]]'' {{WS|theoaklandpress.com}} of [[Oakland, Michigan|Oakland]] +* ''Daily Tribune'' {{WS|dailytribune.com}} of [[Royal Oak, Michigan|Royal Oak]] +* ''Macomb Daily'' {{WS|macombdaily.com}} of [[Mt. Clemens, Michigan|Mt. Clemens]] +* ''[[Morning Sun]]'' {{WS|themorningsun.com}} of [[Mount Pleasant, Michigan|Mount Pleasant]] + +* Heritage Newspapers {{WS|heritage.com}} +** ''Belleville View'' {{WS|bellevilleview.com}} +** ''Ile Camera'' {{WS|thenewsherald.com/ile_camera}} +** ''Monroe Guardian'' {{WS|monreguardian.com}} +** ''Ypsilanti Courier'' {{WS|ypsilanticourier.com}} +** ''News-Herald'' {{WS|thenewsherald.com}} +** ''Press & Guide'' {{WS|pressandguide.com}} +** ''Chelsea Standard & Dexter Leader'' {{WS|chelseastandard.com}} +** ''Manchester Enterprise'' {{WS|manchesterguardian.com}} +** ''Milan News-Leader'' {{WS|milannews.com}} +** ''Saline Reporter'' {{WS|salinereporter.com}} +* Independent Newspapers +** ''Advisor'' {{WS|sourcenewspapers.com}} +** ''Source'' {{WS|sourcenewspapers.com}} +* Morning Star {{WS|morningstarpublishing.com}} +** ''The Leader & Kalkaskian'' {{WS|leaderandkalkaskian.com}} +** ''Grand Traverse Insider'' {{WS|grandtraverseinsider.com}} +** ''Alma Reminder'' +** ''Alpena Star'' +** ''Ogemaw/Oscoda County Star'' +** ''Presque Isle Star'' +** ''St. Johns Reminder'' + +* Voice Newspapers {{WS|voicenews.com}} +** ''Armada Times'' +** ''Bay Voice'' +** ''Blue Water Voice'' +** ''Downriver Voice'' +** ''Macomb Township Voice'' +** ''North Macomb Voice'' +** ''Weekend Voice'' + +== Mid-Hudson == +One daily, associated magazines in the [[Hudson River Valley]] of [[New York]]; also [http://www.midhudsoncentral.com MidHudsonCentral.com] and [http://www.jobsinnewyork.com JobsInNewYork.com]. + +* ''[[Daily Freeman]]'' {{WS|dailyfreeman.com}} of [[Kingston, New York]] +* ''Las Noticias'' {{WS|lasnoticiasny.com}} of [[Kingston, New York]] + +== Ohio == +Two dailies, associated magazines and three shared Websites, all in the state of [[Ohio]]: [http://www.allaroundcleveland.com AllAroundCleveland.com], [http://www.allaroundclevelandcars.com AllAroundClevelandCars.com] and [http://www.allaroundclevelandjobs.com AllAroundClevelandJobs.com]. + +* ''[[The News-Herald (Ohio)|The News-Herald]]'' {{WS|news-herald.com}} of [[Willoughby, Ohio|Willoughby]] +* ''[[The Morning Journal]]'' {{WS|morningjournal.com}} of [[Lorain, Ohio|Lorain]] +* ''El Latino Expreso'' {{WS|lorainlatino.com}} of [[Lorain, Ohio|Lorain]] + +== Philadelphia area == +Seven dailies and associated weeklies and magazines in [[Pennsylvania]] and [[New Jersey]], and associated Websites: [http://www.allaroundphilly.com AllAroundPhilly.com], [http://www.jobsinnj.com JobsInNJ.com], [http://www.jobsinpa.com JobsInPA.com], and [http://www.phillycarsearch.com PhillyCarSearch.com]. + +* ''[[The Daily Local News]]'' {{WS|dailylocal.com}} of [[West Chester, Pennsylvania|West Chester]] +* ''[[Delaware County Daily and Sunday Times]] {{WS|delcotimes.com}} of Primos [[Upper Darby Township, Pennsylvania]] +* ''[[The Mercury (Pennsylvania)|The Mercury]]'' {{WS|pottstownmercury.com}} of [[Pottstown, Pennsylvania|Pottstown]] +* ''[[The Reporter (Lansdale)|The Reporter]]'' {{WS|thereporteronline.com}} of [[Lansdale, Pennsylvania|Lansdale]] +* ''The Times Herald'' {{WS|timesherald.com}} of [[Norristown, Pennsylvania|Norristown]] +* ''[[The Trentonian]]'' {{WS|trentonian.com}} of [[Trenton, New Jersey]] + +* Weeklies +* ''The Phoenix'' {{WS|phoenixvillenews.com}} of [[Phoenixville, Pennsylvania]] +** ''El Latino Expreso'' {{WS|njexpreso.com}} of [[Trenton, New Jersey]] +** ''La Voz'' {{WS|lavozpa.com}} of [[Norristown, Pennsylvania]] +** ''The Tri County Record'' {{WS|tricountyrecord.com}} of [[Morgantown, Pennsylvania]] +** ''Penny Pincher'' {{WS|pennypincherpa.com}}of [[Pottstown, Pennsylvania]] + +* Chesapeake Publishing {{WS|southernchestercountyweeklies.com}} +** ''The Kennett Paper'' {{WS|kennettpaper.com}} of [[Kennett Square, Pennsylvania]] +** ''Avon Grove Sun'' {{WS|avongrovesun.com}} of [[West Grove, Pennsylvania]] +** ''The Central Record'' {{WS|medfordcentralrecord.com}} of [[Medford, New Jersey]] +** ''Maple Shade Progress'' {{WS|mapleshadeprogress.com}} of [[Maple Shade, New Jersey]] + +* Intercounty Newspapers {{WS|buckslocalnews.com}} {{WS|southjerseylocalnews.com}} +** ''The Pennington Post'' {{WS|penningtonpost.com}} of [[Pennington, New Jersey]] +** ''The Bristol Pilot'' {{WS|bristolpilot.com}} of [[Bristol, Pennsylvania]] +** ''Yardley News'' {{WS|yardleynews.com}} of [[Yardley, Pennsylvania]] +** ''Advance of Bucks County'' {{WS|advanceofbucks.com}} of [[Newtown, Pennsylvania]] +** ''Record Breeze'' {{WS|recordbreeze.com}} of [[Berlin, New Jersey]] +** ''Community News'' {{WS|sjcommunitynews.com}} of [[Pemberton, New Jersey]] + +* Montgomery Newspapers {{WS|montgomerynews.com}} +** ''Ambler Gazette'' {{WS|amblergazette.com}} of [[Ambler, Pennsylvania]] +** ''The Colonial'' {{WS|colonialnews.com}} of [[Plymouth Meeting, Pennsylvania]] +** ''Glenside News'' {{WS|glensidenews.com}} of [[Glenside, Pennsylvania]] +** ''The Globe'' {{WS|globenewspaper.com}} of [[Lower Moreland Township, Pennsylvania]] +** ''Montgomery Life'' {{WS|montgomerylife.com}} of [[Fort Washington, Pennsylvania]] +** ''North Penn Life'' {{WS|northpennlife.com}} of [[Lansdale, Pennsylvania]] +** ''Perkasie News Herald'' {{WS|perkasienewsherald.com}} of [[Perkasie, Pennsylvania]] +** ''Public Spirit'' {{WS|thepublicspirit.com}} of [[Hatboro, Pennsylvania]] +** ''Souderton Independent'' {{WS|soudertonindependent.com}} of [[Souderton, Pennsylvania]] +** ''Springfield Sun'' {{WS|springfieldsun.com}} of [[Springfield, Pennsylvania]] +** ''Spring-Ford Reporter'' {{WS|springfordreporter.com}} of [[Royersford, Pennsylvania]] +** ''Times Chronicle'' {{WS|thetimeschronicle.com}} of [[Jenkintown, Pennsylvania]] +** ''Valley Item'' {{WS|valleyitem.com}} of [[Perkiomenville, Pennsylvania]] +** ''Willow Grove Guide'' {{WS|willowgroveguide.com}} of [[Willow Grove, Pennsylvania]] +** ''The Review'' {{WS|roxreview.com}} of [[Roxborough, Philadelphia, Pennsylvania]] + +* Main Line Media News {{WS|mainlinemedianews.com}} +** ''Main Line Times'' {{WS|mainlinetimes.com}} of [[Ardmore, Pennsylvania]] +** ''Main Line Life'' {{WS|mainlinelife.com}} of [[Ardmore, Pennsylvania]] +** ''The King of Prussia Courier'' {{WS|kingofprussiacourier.com}} of [[King of Prussia, Pennsylvania]] + +* Delaware County News Network {{WS|delconewsnetwork.com}} +** ''News of Delaware County'' {{WS|newsofdelawarecounty.com}} of [[Havertown, Pennsylvania]] +** ''County Press'' {{WS|countypressonline.com}} of [[Newtown Square, Pennsylvania]] +** ''Garnet Valley Press'' {{WS|countypressonline.com}} of [[Glen Mills, Pennsylvania]] +** ''Springfield Press'' {{WS|countypressonline.com}} of [[Springfield, Pennsylvania]] +** ''Town Talk'' {{WS|towntalknews.com}} of [[Ridley, Pennsylvania]] + +* Berks-Mont Newspapers {{WS|berksmontnews.com}} +** ''The Boyertown Area Times'' {{WS|berksmontnews.com/boyertown_area_times}} of [[Boyertown, Pennsylvania]] +** ''The Kutztown Area Patriot'' {{WS|berksmontnews.com/kutztown_area_patriot}} of [[Kutztown, Pennsylvania]] +** ''The Hamburg Area Item'' {{WS|berksmontnews.com/hamburg_area_item}} of [[Hamburg, Pennsylvania]] +** ''The Southern Berks News'' {{WS|berksmontnews.com/southern_berks_news}} of [[Exeter Township, Berks County, Pennsylvania]] +** ''Community Connection'' {{WS|berksmontnews.com/community_connection}} of [[Boyertown, Pennsylvania]] + +* Magazines +** ''Bucks Co. Town & Country Living'' {{WS|buckscountymagazine.com}} +** ''Parents Express'' {{WS|parents-express.com}} +** ''Real Men, Rednecks'' {{WS|realmenredneck.com}} + +{{JRC}} + +==References== + + +[[Category:Journal Register publications|*]] diff --git a/objectivec/Tests/DiffMatchPatchTest-Info.plist b/objectivec/Tests/DiffMatchPatchTest-Info.plist new file mode 100755 index 0000000..c285a47 --- /dev/null +++ b/objectivec/Tests/DiffMatchPatchTest-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.yourcompany.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/objectivec/Tests/DiffMatchPatchTest.h b/objectivec/Tests/DiffMatchPatchTest.h new file mode 100755 index 0000000..1cb3753 --- /dev/null +++ b/objectivec/Tests/DiffMatchPatchTest.h @@ -0,0 +1,31 @@ +/* + * Diff Match and Patch -- Test harness + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#import + +#import "DiffMatchPatch.h" + + +@interface DiffMatchPatchTest : SenTestCase { + +} + +@end diff --git a/objectivec/Tests/DiffMatchPatchTest.m b/objectivec/Tests/DiffMatchPatchTest.m new file mode 100755 index 0000000..85bb9e3 --- /dev/null +++ b/objectivec/Tests/DiffMatchPatchTest.m @@ -0,0 +1,1315 @@ +/* + * Diff Match and Patch -- Test harness + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#import "DiffMatchPatchTest.h" + +#import "DiffMatchPatch.h" +#import "NSMutableDictionary+DMPExtensions.h" + +#define stringForBOOL(A) ([((NSNumber *)A) boolValue] ? @"true" : @"false") + +@interface DiffMatchPatchTest (PrivatMethods) +- (NSArray *)diff_rebuildtexts:(NSMutableArray *)diffs; +@end + +@implementation DiffMatchPatchTest + +- (void)test_diff_commonPrefixTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + // Detect any common suffix. + // Null case. + STAssertEquals((NSUInteger)0, [dmp diff_commonPrefixOfFirstString:@"abc" andSecondString:@"xyz"], @"Common suffix null case failed."); + + // Non-null case. + STAssertEquals((NSUInteger)4, [dmp diff_commonPrefixOfFirstString:@"1234abcdef" andSecondString:@"1234xyz"], @"Common suffix non-null case failed."); + + // Whole case. + STAssertEquals((NSUInteger)4, [dmp diff_commonPrefixOfFirstString:@"1234" andSecondString:@"1234xyz"], @"Common suffix whole case failed."); + + [dmp release]; +} + +- (void)test_diff_commonSuffixTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + // Detect any common suffix. + // Null case. + STAssertEquals((NSUInteger)0, [dmp diff_commonSuffixOfFirstString:@"abc" andSecondString:@"xyz"], @"Detect any common suffix. Null case."); + + // Non-null case. + STAssertEquals((NSUInteger)4, [dmp diff_commonSuffixOfFirstString:@"abcdef1234" andSecondString:@"xyz1234"], @"Detect any common suffix. Non-null case."); + + // Whole case. + STAssertEquals((NSUInteger)4, [dmp diff_commonSuffixOfFirstString:@"1234" andSecondString:@"xyz1234"], @"Detect any common suffix. Whole case."); + + [dmp release]; +} + +- (void)test_diff_commonOverlapTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + // Detect any suffix/prefix overlap. + // Null case. + STAssertEquals((NSUInteger)0, [dmp diff_commonOverlapOfFirstString:@"" andSecondString:@"abcd"], @"Detect any suffix/prefix overlap. Null case."); + + // Whole case. + STAssertEquals((NSUInteger)3, [dmp diff_commonOverlapOfFirstString:@"abc" andSecondString:@"abcd"], @"Detect any suffix/prefix overlap. Whole case."); + + // No overlap. + STAssertEquals((NSUInteger)0, [dmp diff_commonOverlapOfFirstString:@"123456" andSecondString:@"abcd"], @"Detect any suffix/prefix overlap. No overlap."); + + // Overlap. + STAssertEquals((NSUInteger)3, [dmp diff_commonOverlapOfFirstString:@"123456xxx" andSecondString:@"xxxabcd"], @"Detect any suffix/prefix overlap. Overlap."); + + // Unicode. + // Some overly clever languages (C#) may treat ligatures as equal to their + // component letters. E.g. U+FB01 == 'fi' + STAssertEquals((NSUInteger)0, [dmp diff_commonOverlapOfFirstString:@"fi" andSecondString:@"\U0000fb01i"], @"Detect any suffix/prefix overlap. Unicode."); + + [dmp release]; +} + +- (void)test_diff_halfmatchTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + dmp.Diff_Timeout = 1; + NSArray *expectedResult = nil; + + // No match. + STAssertNil([dmp diff_halfMatchOfFirstString:@"1234567890" andSecondString:@"abcdef"], @"No match #1."); + + STAssertNil([dmp diff_halfMatchOfFirstString:@"12345" andSecondString:@"23"], @"No match #2."); + + // Single Match. + expectedResult = [NSArray arrayWithObjects:@"12", @"90", @"a", @"z", @"345678", nil]; + STAssertEqualObjects(expectedResult, [dmp diff_halfMatchOfFirstString:@"1234567890" andSecondString:@"a345678z"], @"Single Match #1."); + + expectedResult = [NSArray arrayWithObjects:@"a", @"z", @"12", @"90", @"345678", nil]; + STAssertEqualObjects(expectedResult, [dmp diff_halfMatchOfFirstString:@"a345678z" andSecondString:@"1234567890"], @"Single Match #2."); + + expectedResult = [NSArray arrayWithObjects:@"abc", @"z", @"1234", @"0", @"56789", nil]; + STAssertEqualObjects(expectedResult, [dmp diff_halfMatchOfFirstString:@"abc56789z" andSecondString:@"1234567890"], @"Single Match #3."); + + expectedResult = [NSArray arrayWithObjects:@"a", @"xyz", @"1", @"7890", @"23456", nil]; + STAssertEqualObjects(expectedResult, [dmp diff_halfMatchOfFirstString:@"a23456xyz" andSecondString:@"1234567890"], @"Single Match #4."); + + // Multiple Matches. + expectedResult = [NSArray arrayWithObjects:@"12123", @"123121", @"a", @"z", @"1234123451234", nil]; + STAssertEqualObjects(expectedResult, [dmp diff_halfMatchOfFirstString:@"121231234123451234123121" andSecondString:@"a1234123451234z"], @"Multiple Matches #1."); + + expectedResult = [NSArray arrayWithObjects:@"", @"-=-=-=-=-=", @"x", @"", @"x-=-=-=-=-=-=-=", nil]; + STAssertEqualObjects(expectedResult, [dmp diff_halfMatchOfFirstString:@"x-=-=-=-=-=-=-=-=-=-=-=-=" andSecondString:@"xx-=-=-=-=-=-=-="], @"Multiple Matches #2."); + + expectedResult = [NSArray arrayWithObjects:@"-=-=-=-=-=", @"", @"", @"y", @"-=-=-=-=-=-=-=y", nil]; + STAssertEqualObjects(expectedResult, [dmp diff_halfMatchOfFirstString:@"-=-=-=-=-=-=-=-=-=-=-=-=y" andSecondString:@"-=-=-=-=-=-=-=yy"], @"Multiple Matches #3."); + + // Non-optimal halfmatch. + // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy + expectedResult = [NSArray arrayWithObjects:@"qHillo", @"w", @"x", @"Hulloy", @"HelloHe", nil]; + STAssertEqualObjects(expectedResult, [dmp diff_halfMatchOfFirstString:@"qHilloHelloHew" andSecondString:@"xHelloHeHulloy"], @"Non-optimal halfmatch."); + + // Optimal no halfmatch. + dmp.Diff_Timeout = 0; + STAssertNil([dmp diff_halfMatchOfFirstString:@"qHilloHelloHew" andSecondString:@"xHelloHeHulloy"], @"Optimal no halfmatch."); + + [dmp release]; +} + +- (void)test_diff_linesToCharsTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + NSArray *result; + + // Convert lines down to characters. + NSMutableArray *tmpVector = [NSMutableArray array]; // Array of NSString objects. + [tmpVector addObject:@""]; + [tmpVector addObject:@"alpha\n"]; + [tmpVector addObject:@"beta\n"]; + result = [dmp diff_linesToCharsForFirstString:@"alpha\nbeta\nalpha\n" andSecondString:@"beta\nalpha\nbeta\n"]; + STAssertEqualObjects(@"\001\002\001", [result objectAtIndex:0], @"Shared lines #1."); + STAssertEqualObjects(@"\002\001\002", [result objectAtIndex:1], @"Shared lines #2."); + STAssertEqualObjects(tmpVector, (NSArray *)[result objectAtIndex:2], @"Shared lines #3."); + + [tmpVector removeAllObjects]; + [tmpVector addObject:@""]; + [tmpVector addObject:@"alpha\r\n"]; + [tmpVector addObject:@"beta\r\n"]; + [tmpVector addObject:@"\r\n"]; + result = [dmp diff_linesToCharsForFirstString:@"" andSecondString:@"alpha\r\nbeta\r\n\r\n\r\n"]; + STAssertEqualObjects(@"", [result objectAtIndex:0], @"Empty string and blank lines #1."); + STAssertEqualObjects(@"\001\002\003\003", [result objectAtIndex:1], @"Empty string and blank lines #2."); + STAssertEqualObjects(tmpVector, (NSArray *)[result objectAtIndex:2], @"Empty string and blank lines #3."); + + [tmpVector removeAllObjects]; + [tmpVector addObject:@""]; + [tmpVector addObject:@"a"]; + [tmpVector addObject:@"b"]; + result = [dmp diff_linesToCharsForFirstString:@"a" andSecondString:@"b"]; + STAssertEqualObjects(@"\001", [result objectAtIndex:0], @"No linebreaks #1."); + STAssertEqualObjects(@"\002", [result objectAtIndex:1], @"No linebreaks #2."); + STAssertEqualObjects(tmpVector, (NSArray *)[result objectAtIndex:2], @"No linebreaks #3."); + + // More than 256 to reveal any 8-bit limitations. + unichar n = 300; + [tmpVector removeAllObjects]; + NSMutableString *lines = [NSMutableString string]; + NSMutableString *chars = [NSMutableString string]; + NSString *currentLine; + for (unichar x = 1; x < n + 1; x++) { + currentLine = [NSString stringWithFormat:@"%d\n", (int)x]; + [tmpVector addObject:currentLine]; + [lines appendString:currentLine]; + [chars appendString:[NSString stringWithFormat:@"%C", x]]; + } + STAssertEquals((NSUInteger)n, tmpVector.count, @"More than 256 #1."); + STAssertEquals((NSUInteger)n, chars.length, @"More than 256 #2."); + [tmpVector insertObject:@"" atIndex:0]; + result = [dmp diff_linesToCharsForFirstString:lines andSecondString:@""]; + STAssertEqualObjects(chars, [result objectAtIndex:0], @"More than 256 #3."); + STAssertEqualObjects(@"", [result objectAtIndex:1], @"More than 256 #4."); + STAssertEqualObjects(tmpVector, (NSArray *)[result objectAtIndex:2], @"More than 256 #5."); + + [dmp release]; +} + +- (void)test_diff_charsToLinesTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + // Convert chars up to lines. + NSArray *diffs = [NSArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"\001\002\001"], + [Diff diffWithOperation:DIFF_INSERT andText:@"\002\001\002"], nil]; + NSMutableArray *tmpVector = [NSMutableArray array]; // Array of NSString objects. + [tmpVector addObject:@""]; + [tmpVector addObject:@"alpha\n"]; + [tmpVector addObject:@"beta\n"]; + [dmp diff_chars:diffs toLines:tmpVector]; + NSArray *expectedResult = [NSArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"alpha\nbeta\nalpha\n"], + [Diff diffWithOperation:DIFF_INSERT andText:@"beta\nalpha\nbeta\n"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Shared lines."); + + // More than 256 to reveal any 8-bit limitations. + unichar n = 300; + [tmpVector removeAllObjects]; + NSMutableString *lines = [NSMutableString string]; + NSMutableString *chars = [NSMutableString string]; + NSString *currentLine; + for (unichar x = 1; x < n + 1; x++) { + currentLine = [NSString stringWithFormat:@"%d\n", (int)x]; + [tmpVector addObject:currentLine]; + [lines appendString:currentLine]; + [chars appendString:[NSString stringWithFormat:@"%C", x]]; + } + STAssertEquals((NSUInteger)n, tmpVector.count, @"More than 256 #1."); + STAssertEquals((NSUInteger)n, chars.length, @"More than 256 #2."); + [tmpVector insertObject:@"" atIndex:0]; + diffs = [NSArray arrayWithObject:[Diff diffWithOperation:DIFF_DELETE andText:chars]]; + [dmp diff_chars:diffs toLines:tmpVector]; + STAssertEqualObjects([NSArray arrayWithObject:[Diff diffWithOperation:DIFF_DELETE andText:lines]], diffs, @"More than 256 #3."); + + [dmp release]; +} + +- (void)test_diff_cleanupMergeTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + NSMutableArray *expectedResult = nil; + + // Cleanup a messy diff. + // Null case. + NSMutableArray *diffs = [NSMutableArray array]; + [dmp diff_cleanupMerge:diffs]; + STAssertEqualObjects([NSMutableArray array], diffs, @"Null case."); + + // No change case. + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"a"], [Diff diffWithOperation:DIFF_DELETE andText:@"b"], [Diff diffWithOperation:DIFF_INSERT andText:@"c"], nil]; + [dmp diff_cleanupMerge:diffs]; + expectedResult = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"a"], [Diff diffWithOperation:DIFF_DELETE andText:@"b"], [Diff diffWithOperation:DIFF_INSERT andText:@"c"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"No change case."); + + // Merge equalities. + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"a"], [Diff diffWithOperation:DIFF_EQUAL andText:@"b"], [Diff diffWithOperation:DIFF_EQUAL andText:@"c"], nil]; + [dmp diff_cleanupMerge:diffs]; + expectedResult = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"abc"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Merge equalities."); + + // Merge deletions. + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_DELETE andText:@"a"], [Diff diffWithOperation:DIFF_DELETE andText:@"b"], [Diff diffWithOperation:DIFF_DELETE andText:@"c"], nil]; + [dmp diff_cleanupMerge:diffs]; + expectedResult = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_DELETE andText:@"abc"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Merge deletions."); + + // Merge insertions. + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_INSERT andText:@"a"], [Diff diffWithOperation:DIFF_INSERT andText:@"b"], [Diff diffWithOperation:DIFF_INSERT andText:@"c"], nil]; + [dmp diff_cleanupMerge:diffs]; + expectedResult = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_INSERT andText:@"abc"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Merge insertions."); + + // Merge interweave. + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_DELETE andText:@"a"], [Diff diffWithOperation:DIFF_INSERT andText:@"b"], [Diff diffWithOperation:DIFF_DELETE andText:@"c"], [Diff diffWithOperation:DIFF_INSERT andText:@"d"], [Diff diffWithOperation:DIFF_EQUAL andText:@"e"], [Diff diffWithOperation:DIFF_EQUAL andText:@"f"], nil]; + [dmp diff_cleanupMerge:diffs]; + expectedResult = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_DELETE andText:@"ac"], [Diff diffWithOperation:DIFF_INSERT andText:@"bd"], [Diff diffWithOperation:DIFF_EQUAL andText:@"ef"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Merge interweave."); + + // Prefix and suffix detection. + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_DELETE andText:@"a"], [Diff diffWithOperation:DIFF_INSERT andText:@"abc"], [Diff diffWithOperation:DIFF_DELETE andText:@"dc"], nil]; + [dmp diff_cleanupMerge:diffs]; + expectedResult = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"a"], [Diff diffWithOperation:DIFF_DELETE andText:@"d"], [Diff diffWithOperation:DIFF_INSERT andText:@"b"], [Diff diffWithOperation:DIFF_EQUAL andText:@"c"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Prefix and suffix detection."); + + // Prefix and suffix detection with equalities. + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"x"], [Diff diffWithOperation:DIFF_DELETE andText:@"a"], [Diff diffWithOperation:DIFF_INSERT andText:@"abc"], [Diff diffWithOperation:DIFF_DELETE andText:@"dc"], [Diff diffWithOperation:DIFF_EQUAL andText:@"y"], nil]; + [dmp diff_cleanupMerge:diffs]; + expectedResult = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"xa"], [Diff diffWithOperation:DIFF_DELETE andText:@"d"], [Diff diffWithOperation:DIFF_INSERT andText:@"b"], [Diff diffWithOperation:DIFF_EQUAL andText:@"cy"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Prefix and suffix detection with equalities."); + + // Slide edit left. + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"a"], [Diff diffWithOperation:DIFF_INSERT andText:@"ba"], [Diff diffWithOperation:DIFF_EQUAL andText:@"c"], nil]; + [dmp diff_cleanupMerge:diffs]; + expectedResult = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_INSERT andText:@"ab"], [Diff diffWithOperation:DIFF_EQUAL andText:@"ac"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Slide edit left."); + + // Slide edit right. + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"c"], [Diff diffWithOperation:DIFF_INSERT andText:@"ab"], [Diff diffWithOperation:DIFF_EQUAL andText:@"a"], nil]; + [dmp diff_cleanupMerge:diffs]; + expectedResult = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"ca"], [Diff diffWithOperation:DIFF_INSERT andText:@"ba"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Slide edit right."); + + // Slide edit left recursive. + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"a"], [Diff diffWithOperation:DIFF_DELETE andText:@"b"], [Diff diffWithOperation:DIFF_EQUAL andText:@"c"], [Diff diffWithOperation:DIFF_DELETE andText:@"ac"], [Diff diffWithOperation:DIFF_EQUAL andText:@"x"], nil]; + [dmp diff_cleanupMerge:diffs]; + expectedResult = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_DELETE andText:@"abc"], [Diff diffWithOperation:DIFF_EQUAL andText:@"acx"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Slide edit left recursive."); + + // Slide edit right recursive. + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"x"], [Diff diffWithOperation:DIFF_DELETE andText:@"ca"], [Diff diffWithOperation:DIFF_EQUAL andText:@"c"], [Diff diffWithOperation:DIFF_DELETE andText:@"b"], [Diff diffWithOperation:DIFF_EQUAL andText:@"a"], nil]; + [dmp diff_cleanupMerge:diffs]; + expectedResult = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"xca"], [Diff diffWithOperation:DIFF_DELETE andText:@"cba"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Slide edit right recursive."); + + [dmp release]; +} + +- (void)test_diff_cleanupSemanticLosslessTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + NSMutableArray *expectedResult = nil; + + // Slide diffs to match logical boundaries. + // Null case. + NSMutableArray *diffs = [NSMutableArray array]; + [dmp diff_cleanupSemanticLossless:diffs]; + STAssertEqualObjects([NSMutableArray array], diffs, @"Null case."); + + // Blank lines. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"AAA\r\n\r\nBBB"], + [Diff diffWithOperation:DIFF_INSERT andText:@"\r\nDDD\r\n\r\nBBB"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"\r\nEEE"], nil]; + [dmp diff_cleanupSemanticLossless:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"AAA\r\n\r\n"], + [Diff diffWithOperation:DIFF_INSERT andText:@"BBB\r\nDDD\r\n\r\n"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"BBB\r\nEEE"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Blank lines."); + + // Line boundaries. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"AAA\r\nBBB"], + [Diff diffWithOperation:DIFF_INSERT andText:@" DDD\r\nBBB"], + [Diff diffWithOperation:DIFF_EQUAL andText:@" EEE"], nil]; + [dmp diff_cleanupSemanticLossless:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"AAA\r\n"], + [Diff diffWithOperation:DIFF_INSERT andText:@"BBB DDD\r\n"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"BBB EEE"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Line boundaries."); + + // Word boundaries. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"The c"], + [Diff diffWithOperation:DIFF_INSERT andText:@"ow and the c"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"at."], nil]; + [dmp diff_cleanupSemanticLossless:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"The "], + [Diff diffWithOperation:DIFF_INSERT andText:@"cow and the "], + [Diff diffWithOperation:DIFF_EQUAL andText:@"cat."], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Word boundaries."); + + // Alphanumeric boundaries. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"The-c"], + [Diff diffWithOperation:DIFF_INSERT andText:@"ow-and-the-c"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"at."], nil]; + [dmp diff_cleanupSemanticLossless:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"The-"], + [Diff diffWithOperation:DIFF_INSERT andText:@"cow-and-the-"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"cat."], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Alphanumeric boundaries."); + + // Hitting the start. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"a"], + [Diff diffWithOperation:DIFF_DELETE andText:@"a"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"ax"], nil]; + [dmp diff_cleanupSemanticLossless:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"a"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"aax"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Hitting the start."); + + // Hitting the end. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"xa"], + [Diff diffWithOperation:DIFF_DELETE andText:@"a"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"a"], nil]; + [dmp diff_cleanupSemanticLossless:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"xaa"], + [Diff diffWithOperation:DIFF_DELETE andText:@"a"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Hitting the end."); + + // Alphanumeric boundaries. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"The xxx. The "], + [Diff diffWithOperation:DIFF_INSERT andText:@"zzz. The "], + [Diff diffWithOperation:DIFF_EQUAL andText:@"yyy."], nil]; + [dmp diff_cleanupSemanticLossless:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"The xxx."], + [Diff diffWithOperation:DIFF_INSERT andText:@" The zzz."], + [Diff diffWithOperation:DIFF_EQUAL andText:@" The yyy."], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Sentence boundaries."); + + [dmp release]; +} + +- (void)test_diff_cleanupSemanticTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + NSMutableArray *expectedResult = nil; + + // Cleanup semantically trivial equalities. + // Null case. + NSMutableArray *diffs = [NSMutableArray array]; + [dmp diff_cleanupSemantic:diffs]; + STAssertEqualObjects([NSMutableArray array], diffs, @"Null case."); + + // No elimination #1. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"ab"], + [Diff diffWithOperation:DIFF_INSERT andText:@"cd"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"12"], + [Diff diffWithOperation:DIFF_DELETE andText:@"e"], nil]; + [dmp diff_cleanupSemantic:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"ab"], + [Diff diffWithOperation:DIFF_INSERT andText:@"cd"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"12"], + [Diff diffWithOperation:DIFF_DELETE andText:@"e"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"No elimination #1."); + + // No elimination #2. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abc"], + [Diff diffWithOperation:DIFF_INSERT andText:@"ABC"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"1234"], + [Diff diffWithOperation:DIFF_DELETE andText:@"wxyz"], nil]; + [dmp diff_cleanupSemantic:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abc"], + [Diff diffWithOperation:DIFF_INSERT andText:@"ABC"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"1234"], + [Diff diffWithOperation:DIFF_DELETE andText:@"wxyz"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"No elimination #2."); + + // Simple elimination. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"a"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"b"], + [Diff diffWithOperation:DIFF_DELETE andText:@"c"], nil]; + [dmp diff_cleanupSemantic:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abc"], + [Diff diffWithOperation:DIFF_INSERT andText:@"b"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Simple elimination."); + + // Backpass elimination. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"ab"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"cd"], + [Diff diffWithOperation:DIFF_DELETE andText:@"e"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"f"], + [Diff diffWithOperation:DIFF_INSERT andText:@"g"], nil]; + [dmp diff_cleanupSemantic:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abcdef"], + [Diff diffWithOperation:DIFF_INSERT andText:@"cdfg"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Backpass elimination."); + + // Multiple eliminations. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_INSERT andText:@"1"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"A"], + [Diff diffWithOperation:DIFF_DELETE andText:@"B"], + [Diff diffWithOperation:DIFF_INSERT andText:@"2"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"_"], + [Diff diffWithOperation:DIFF_INSERT andText:@"1"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"A"], + [Diff diffWithOperation:DIFF_DELETE andText:@"B"], + [Diff diffWithOperation:DIFF_INSERT andText:@"2"], nil]; + [dmp diff_cleanupSemantic:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"AB_AB"], + [Diff diffWithOperation:DIFF_INSERT andText:@"1A2_1A2"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Multiple eliminations."); + + // Word boundaries. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"The c"], + [Diff diffWithOperation:DIFF_DELETE andText:@"ow and the c"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"at."], nil]; + [dmp diff_cleanupSemantic:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"The "], + [Diff diffWithOperation:DIFF_DELETE andText:@"cow and the "], + [Diff diffWithOperation:DIFF_EQUAL andText:@"cat."], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Word boundaries."); + + // No overlap elimination. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abcxx"], + [Diff diffWithOperation:DIFF_INSERT andText:@"xxdef"], nil]; + [dmp diff_cleanupSemantic:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abcxx"], + [Diff diffWithOperation:DIFF_INSERT andText:@"xxdef"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"No overlap elimination."); + + // Overlap elimination. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abcxxx"], + [Diff diffWithOperation:DIFF_INSERT andText:@"xxxdef"], nil]; + [dmp diff_cleanupSemantic:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abc"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"xxx"], + [Diff diffWithOperation:DIFF_INSERT andText:@"def"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Overlap elimination."); + + // Reverse overlap elimination. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"xxxabc"], + [Diff diffWithOperation:DIFF_INSERT andText:@"defxxx"], nil]; + [dmp diff_cleanupSemantic:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_INSERT andText:@"def"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"xxx"], + [Diff diffWithOperation:DIFF_DELETE andText:@"abc"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Reverse overlap elimination."); + + // Two overlap eliminations. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abcd1212"], + [Diff diffWithOperation:DIFF_INSERT andText:@"1212efghi"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"----"], + [Diff diffWithOperation:DIFF_DELETE andText:@"A3"], + [Diff diffWithOperation:DIFF_INSERT andText:@"3BC"], nil]; + [dmp diff_cleanupSemantic:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abcd"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"1212"], + [Diff diffWithOperation:DIFF_INSERT andText:@"efghi"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"----"], + [Diff diffWithOperation:DIFF_DELETE andText:@"A"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"3"], + [Diff diffWithOperation:DIFF_INSERT andText:@"BC"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Two overlap eliminations."); + + [dmp release]; +} + +- (void)test_diff_cleanupEfficiencyTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + NSMutableArray *expectedResult = nil; + + // Cleanup operationally trivial equalities. + dmp.Diff_EditCost = 4; + // Null case. + NSMutableArray *diffs = [NSMutableArray array]; + [dmp diff_cleanupEfficiency:diffs]; + STAssertEqualObjects([NSMutableArray array], diffs, @"Null case."); + + // No elimination. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"ab"], + [Diff diffWithOperation:DIFF_INSERT andText:@"12"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"wxyz"], + [Diff diffWithOperation:DIFF_DELETE andText:@"cd"], + [Diff diffWithOperation:DIFF_INSERT andText:@"34"], nil]; + [dmp diff_cleanupEfficiency:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"ab"], + [Diff diffWithOperation:DIFF_INSERT andText:@"12"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"wxyz"], + [Diff diffWithOperation:DIFF_DELETE andText:@"cd"], + [Diff diffWithOperation:DIFF_INSERT andText:@"34"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"No elimination."); + + // Four-edit elimination. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"ab"], + [Diff diffWithOperation:DIFF_INSERT andText:@"12"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"xyz"], + [Diff diffWithOperation:DIFF_DELETE andText:@"cd"], + [Diff diffWithOperation:DIFF_INSERT andText:@"34"], nil]; + [dmp diff_cleanupEfficiency:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abxyzcd"], + [Diff diffWithOperation:DIFF_INSERT andText:@"12xyz34"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Four-edit elimination."); + + // Three-edit elimination. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_INSERT andText:@"12"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"x"], + [Diff diffWithOperation:DIFF_DELETE andText:@"cd"], + [Diff diffWithOperation:DIFF_INSERT andText:@"34"], nil]; + [dmp diff_cleanupEfficiency:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"xcd"], + [Diff diffWithOperation:DIFF_INSERT andText:@"12x34"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Three-edit elimination."); + + // Backpass elimination. + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"ab"], + [Diff diffWithOperation:DIFF_INSERT andText:@"12"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"xy"], + [Diff diffWithOperation:DIFF_INSERT andText:@"34"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"z"], + [Diff diffWithOperation:DIFF_DELETE andText:@"cd"], + [Diff diffWithOperation:DIFF_INSERT andText:@"56"], nil]; + [dmp diff_cleanupEfficiency:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abxyzcd"], + [Diff diffWithOperation:DIFF_INSERT andText:@"12xy34z56"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"Backpass elimination."); + + // High cost elimination. + dmp.Diff_EditCost = 5; + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"ab"], + [Diff diffWithOperation:DIFF_INSERT andText:@"12"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"wxyz"], + [Diff diffWithOperation:DIFF_DELETE andText:@"cd"], + [Diff diffWithOperation:DIFF_INSERT andText:@"34"], nil]; + [dmp diff_cleanupEfficiency:diffs]; + expectedResult = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abwxyzcd"], + [Diff diffWithOperation:DIFF_INSERT andText:@"12wxyz34"], nil]; + STAssertEqualObjects(expectedResult, diffs, @"High cost elimination."); + dmp.Diff_EditCost = 4; + + [dmp release]; +} + +- (void)test_diff_prettyHtmlTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + // Pretty print. + NSMutableArray *diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"a\n"], + [Diff diffWithOperation:DIFF_DELETE andText:@"b"], + [Diff diffWithOperation:DIFF_INSERT andText:@"c&d"], nil]; + NSString *expectedResult = @"
    <B>b</B>c&d"; + STAssertEqualObjects(expectedResult, [dmp diff_prettyHtml:diffs], @"Pretty print."); + + [dmp release]; +} + +- (void)test_diff_textTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + // Compute the source and destination texts. + NSMutableArray *diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"jump"], + [Diff diffWithOperation:DIFF_DELETE andText:@"s"], + [Diff diffWithOperation:DIFF_INSERT andText:@"ed"], + [Diff diffWithOperation:DIFF_EQUAL andText:@" over "], + [Diff diffWithOperation:DIFF_DELETE andText:@"the"], + [Diff diffWithOperation:DIFF_INSERT andText:@"a"], + [Diff diffWithOperation:DIFF_EQUAL andText:@" lazy"], nil]; + STAssertEqualObjects(@"jumps over the lazy", [dmp diff_text1:diffs], @"Compute the source and destination texts #1"); + + STAssertEqualObjects(@"jumped over a lazy", [dmp diff_text2:diffs], @"Compute the source and destination texts #2"); + + [dmp release]; +} + +- (void)test_diff_deltaTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + NSMutableArray *expectedResult = nil; + NSError *error = nil; + + // Convert a diff into delta string. + NSMutableArray *diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"jump"], + [Diff diffWithOperation:DIFF_DELETE andText:@"s"], + [Diff diffWithOperation:DIFF_INSERT andText:@"ed"], + [Diff diffWithOperation:DIFF_EQUAL andText:@" over "], + [Diff diffWithOperation:DIFF_DELETE andText:@"the"], + [Diff diffWithOperation:DIFF_INSERT andText:@"a"], + [Diff diffWithOperation:DIFF_EQUAL andText:@" lazy"], + [Diff diffWithOperation:DIFF_INSERT andText:@"old dog"], nil]; + NSString *text1 = [dmp diff_text1:diffs]; + STAssertEqualObjects(@"jumps over the lazy", text1, @"Convert a diff into delta string 1."); + + NSString *delta = [dmp diff_toDelta:diffs]; + STAssertEqualObjects(@"=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog", delta, @"Convert a diff into delta string 2."); + + // Convert delta string into a diff. + STAssertEqualObjects(diffs, [dmp diff_fromDeltaWithText:text1 andDelta:delta error:NULL], @"Convert delta string into a diff."); + + // Generates error (19 < 20). + diffs = [dmp diff_fromDeltaWithText:[text1 stringByAppendingString:@"x"] andDelta:delta error:&error]; + if (diffs != nil || error == nil) { + STFail(@"diff_fromDelta: Too long."); + } + error = nil; + + // Generates error (19 > 18). + diffs = [dmp diff_fromDeltaWithText:[text1 substringFromIndex:1] andDelta:delta error:&error]; + if (diffs != nil || error == nil) { + STFail(@"diff_fromDelta: Too short."); + } + error = nil; + + // Generates error (%c3%xy invalid Unicode). + diffs = [dmp diff_fromDeltaWithText:@"" andDelta:@"+%c3%xy" error:&error]; + if (diffs != nil || error == nil) { + STFail(@"diff_fromDelta: Invalid character."); + } + error = nil; + + // Test deltas with special characters. + unichar zero = (unichar)0; + unichar one = (unichar)1; + unichar two = (unichar)2; + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:[NSString stringWithFormat:@"\U00000680 %C \t %%", zero]], + [Diff diffWithOperation:DIFF_DELETE andText:[NSString stringWithFormat:@"\U00000681 %C \n ^", one]], + [Diff diffWithOperation:DIFF_INSERT andText:[NSString stringWithFormat:@"\U00000682 %C \\ |", two]], nil]; + text1 = [dmp diff_text1:diffs]; + NSString *expectedString = [NSString stringWithFormat:@"\U00000680 %C \t %%\U00000681 %C \n ^", zero, one]; + STAssertEqualObjects(expectedString, text1, @"Test deltas with special characters."); + + delta = [dmp diff_toDelta:diffs]; + // Upper case, because to CFURLCreateStringByAddingPercentEscapes() uses upper. + STAssertEqualObjects(@"=7\t-7\t+%DA%82 %02 %5C %7C", delta, @"diff_toDelta: Unicode 1."); + + STAssertEqualObjects(diffs, [dmp diff_fromDeltaWithText:text1 andDelta:delta error:NULL], @"diff_fromDelta: Unicode 2."); + + // Verify pool of unchanged characters. + diffs = [NSMutableArray arrayWithObject: + [Diff diffWithOperation:DIFF_INSERT andText:@"A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # "]]; + NSString *text2 = [dmp diff_text2:diffs]; + STAssertEqualObjects(@"A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ", text2, @"diff_text2: Unchanged characters 1."); + + delta = [dmp diff_toDelta:diffs]; + STAssertEqualObjects(@"+A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ", delta, @"diff_toDelta: Unchanged characters 2."); + + // Convert delta string into a diff. + expectedResult = [dmp diff_fromDeltaWithText:@"" andDelta:delta error:NULL]; + STAssertEqualObjects(diffs, expectedResult, @"diff_fromDelta: Unchanged characters. Convert delta string into a diff."); + + [dmp release]; +} + +- (void)test_diff_xIndexTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + // Translate a location in text1 to text2. + NSMutableArray *diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"a"], + [Diff diffWithOperation:DIFF_INSERT andText:@"1234"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"xyz"], nil] /* Diff */; + STAssertEquals((NSUInteger)5, [dmp diff_xIndexIn:diffs location:2], @"diff_xIndex: Translation on equality. Translate a location in text1 to text2."); + + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"a"], + [Diff diffWithOperation:DIFF_DELETE andText:@"1234"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"xyz"], nil] /* Diff */; + STAssertEquals((NSUInteger)1, [dmp diff_xIndexIn:diffs location:3], @"diff_xIndex: Translation on deletion."); + + [dmp release]; +} + +- (void)test_diff_levenshteinTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + NSMutableArray *diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abc"], + [Diff diffWithOperation:DIFF_INSERT andText:@"1234"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"xyz"], nil] /* Diff */; + STAssertEquals((NSUInteger)4, [dmp diff_levenshtein:diffs], @"diff_levenshtein: Levenshtein with trailing equality."); + + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"xyz"], + [Diff diffWithOperation:DIFF_DELETE andText:@"abc"], + [Diff diffWithOperation:DIFF_INSERT andText:@"1234"], nil] /* Diff */; + STAssertEquals((NSUInteger)4, [dmp diff_levenshtein:diffs], @"diff_levenshtein: Levenshtein with leading equality."); + + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"abc"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"xyz"], + [Diff diffWithOperation:DIFF_INSERT andText:@"1234"], nil] /* Diff */; + STAssertEquals((NSUInteger)7, [dmp diff_levenshtein:diffs], @"diff_levenshtein: Levenshtein with middle equality."); + + [dmp release]; +} + +- (void)diff_bisectTest; +{ + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + // Normal. + NSString *a = @"cat"; + NSString *b = @"map"; + // Since the resulting diff hasn't been normalized, it would be ok if + // the insertion and deletion pairs are swapped. + // If the order changes, tweak this test as required. + NSMutableArray *diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_DELETE andText:@"c"], [Diff diffWithOperation:DIFF_INSERT andText:@"m"], [Diff diffWithOperation:DIFF_EQUAL andText:@"a"], [Diff diffWithOperation:DIFF_DELETE andText:@"t"], [Diff diffWithOperation:DIFF_INSERT andText:@"p"], nil]; + STAssertEqualObjects(diffs, [dmp diff_bisectOfOldString:a andNewString:b deadline:[[NSDate distantFuture] timeIntervalSinceReferenceDate]], @"Bisect test."); + + // Timeout. + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_DELETE andText:@"cat"], [Diff diffWithOperation:DIFF_INSERT andText:@"map"], nil]; + STAssertEqualObjects(diffs, [dmp diff_bisectOfOldString:a andNewString:b deadline:[[NSDate distantPast] timeIntervalSinceReferenceDate]], @"Bisect timeout."); + + [dmp release]; +} + +- (void)test_diff_mainTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + // Perform a trivial diff. + NSMutableArray *diffs = [NSMutableArray array]; + STAssertEqualObjects(diffs, [dmp diff_mainOfOldString:@"" andNewString:@"" checkLines:NO], @"diff_main: Null case."); + + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"abc"], nil]; + STAssertEqualObjects(diffs, [dmp diff_mainOfOldString:@"abc" andNewString:@"abc" checkLines:NO], @"diff_main: Equality."); + + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"ab"], [Diff diffWithOperation:DIFF_INSERT andText:@"123"], [Diff diffWithOperation:DIFF_EQUAL andText:@"c"], nil]; + STAssertEqualObjects(diffs, [dmp diff_mainOfOldString:@"abc" andNewString:@"ab123c" checkLines:NO], @"diff_main: Simple insertion."); + + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"a"], [Diff diffWithOperation:DIFF_DELETE andText:@"123"], [Diff diffWithOperation:DIFF_EQUAL andText:@"bc"], nil]; + STAssertEqualObjects(diffs, [dmp diff_mainOfOldString:@"a123bc" andNewString:@"abc" checkLines:NO], @"diff_main: Simple deletion."); + + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"a"], [Diff diffWithOperation:DIFF_INSERT andText:@"123"], [Diff diffWithOperation:DIFF_EQUAL andText:@"b"], [Diff diffWithOperation:DIFF_INSERT andText:@"456"], [Diff diffWithOperation:DIFF_EQUAL andText:@"c"], nil]; + STAssertEqualObjects(diffs, [dmp diff_mainOfOldString:@"abc" andNewString:@"a123b456c" checkLines:NO], @"diff_main: Two insertions."); + + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_EQUAL andText:@"a"], [Diff diffWithOperation:DIFF_DELETE andText:@"123"], [Diff diffWithOperation:DIFF_EQUAL andText:@"b"], [Diff diffWithOperation:DIFF_DELETE andText:@"456"], [Diff diffWithOperation:DIFF_EQUAL andText:@"c"], nil]; + STAssertEqualObjects(diffs, [dmp diff_mainOfOldString:@"a123b456c" andNewString:@"abc" checkLines:NO], @"diff_main: Two deletions."); + + // Perform a real diff. + // Switch off the timeout. + dmp.Diff_Timeout = 0; + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_DELETE andText:@"a"], [Diff diffWithOperation:DIFF_INSERT andText:@"b"], nil]; + STAssertEqualObjects(diffs, [dmp diff_mainOfOldString:@"a" andNewString:@"b" checkLines:NO], @"diff_main: Simple case #1."); + + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_DELETE andText:@"Apple"], [Diff diffWithOperation:DIFF_INSERT andText:@"Banana"], [Diff diffWithOperation:DIFF_EQUAL andText:@"s are a"], [Diff diffWithOperation:DIFF_INSERT andText:@"lso"], [Diff diffWithOperation:DIFF_EQUAL andText:@" fruit."], nil]; + STAssertEqualObjects(diffs, [dmp diff_mainOfOldString:@"Apples are a fruit." andNewString:@"Bananas are also fruit." checkLines:NO], @"diff_main: Simple case #2."); + + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_DELETE andText:@"a"], [Diff diffWithOperation:DIFF_INSERT andText:@"\U00000680"], [Diff diffWithOperation:DIFF_EQUAL andText:@"x"], [Diff diffWithOperation:DIFF_DELETE andText:@"\t"], [Diff diffWithOperation:DIFF_INSERT andText:[NSString stringWithFormat:@"%C", 0]], nil]; + NSString *aString = [NSString stringWithFormat:@"\U00000680x%C", 0]; + STAssertEqualObjects(diffs, [dmp diff_mainOfOldString:@"ax\t" andNewString:aString checkLines:NO], @"diff_main: Simple case #3."); + + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_DELETE andText:@"1"], [Diff diffWithOperation:DIFF_EQUAL andText:@"a"], [Diff diffWithOperation:DIFF_DELETE andText:@"y"], [Diff diffWithOperation:DIFF_EQUAL andText:@"b"], [Diff diffWithOperation:DIFF_DELETE andText:@"2"], [Diff diffWithOperation:DIFF_INSERT andText:@"xab"], nil]; + STAssertEqualObjects(diffs, [dmp diff_mainOfOldString:@"1ayb2" andNewString:@"abxab" checkLines:NO], @"diff_main: Overlap #1."); + + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_INSERT andText:@"xaxcx"], [Diff diffWithOperation:DIFF_EQUAL andText:@"abc"], [Diff diffWithOperation:DIFF_DELETE andText:@"y"], nil]; + STAssertEqualObjects(diffs, [dmp diff_mainOfOldString:@"abcy" andNewString:@"xaxcxabc" checkLines:NO], @"diff_main: Overlap #2."); + + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_DELETE andText:@"ABCD"], [Diff diffWithOperation:DIFF_EQUAL andText:@"a"], [Diff diffWithOperation:DIFF_DELETE andText:@"="], [Diff diffWithOperation:DIFF_INSERT andText:@"-"], [Diff diffWithOperation:DIFF_EQUAL andText:@"bcd"], [Diff diffWithOperation:DIFF_DELETE andText:@"="], [Diff diffWithOperation:DIFF_INSERT andText:@"-"], [Diff diffWithOperation:DIFF_EQUAL andText:@"efghijklmnopqrs"], [Diff diffWithOperation:DIFF_DELETE andText:@"EFGHIJKLMNOefg"], nil]; + STAssertEqualObjects(diffs, [dmp diff_mainOfOldString:@"ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg" andNewString:@"a-bcd-efghijklmnopqrs" checkLines:NO], @"diff_main: Overlap #3."); + + diffs = [NSMutableArray arrayWithObjects:[Diff diffWithOperation:DIFF_INSERT andText:@" "], [Diff diffWithOperation:DIFF_EQUAL andText:@"a"], [Diff diffWithOperation:DIFF_INSERT andText:@"nd"], [Diff diffWithOperation:DIFF_EQUAL andText:@" [[Pennsylvania]]"], [Diff diffWithOperation:DIFF_DELETE andText:@" and [[New"], nil]; + STAssertEqualObjects(diffs, [dmp diff_mainOfOldString:@"a [[Pennsylvania]] and [[New" andNewString:@" and [[Pennsylvania]]" checkLines:NO], @"diff_main: Large equality."); + + dmp.Diff_Timeout = 0.1f; // 100ms + NSString *a = @"`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n"; + NSString *b = @"I am the very model of a modern major general,\nI've information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n"; + NSMutableString *aMutable = [NSMutableString stringWithString:a]; + NSMutableString *bMutable = [NSMutableString stringWithString:b]; + // Increase the text lengths by 1024 times to ensure a timeout. + for (int x = 0; x < 10; x++) { + [aMutable appendString:aMutable]; + [bMutable appendString:bMutable]; + } + a = aMutable; + b = bMutable; + NSTimeInterval startTime = [NSDate timeIntervalSinceReferenceDate]; + [dmp diff_mainOfOldString:a andNewString:b]; + NSTimeInterval endTime = [NSDate timeIntervalSinceReferenceDate]; + // Test that we took at least the timeout period. + STAssertTrue((dmp.Diff_Timeout <= (endTime - startTime)), @"Test that we took at least the timeout period."); + // Test that we didn't take forever (be forgiving). + // Theoretically this test could fail very occasionally if the + // OS task swaps or locks up for a second at the wrong moment. + // This will fail when running this as PPC code thru Rosetta on Intel. + STAssertTrue(((dmp.Diff_Timeout * 2) > (endTime - startTime)), @"Test that we didn't take forever (be forgiving)."); + dmp.Diff_Timeout = 0; + + // Test the linemode speedup. + // Must be long to pass the 200 character cutoff. + a = @"1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n"; + b = @"abcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\n"; + STAssertEqualObjects([dmp diff_mainOfOldString:a andNewString:b checkLines:YES], [dmp diff_mainOfOldString:a andNewString:b checkLines:NO], @"diff_main: Simple line-mode."); + + a = @"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; + b = @"abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij"; + STAssertEqualObjects([dmp diff_mainOfOldString:a andNewString:b checkLines:YES], [dmp diff_mainOfOldString:a andNewString:b checkLines:NO], @"diff_main: Single line-mode."); + + a = @"1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n"; + b = @"abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n"; + NSArray *texts_linemode = [self diff_rebuildtexts:[dmp diff_mainOfOldString:a andNewString:b checkLines:YES]]; + NSArray *texts_textmode = [self diff_rebuildtexts:[dmp diff_mainOfOldString:a andNewString:b checkLines:NO]]; + STAssertEqualObjects(texts_textmode, texts_linemode, @"diff_main: Overlap line-mode."); + + // CHANGEME: Test null inputs + + [dmp release]; +} + + +#pragma mark Match Test Functions +// MATCH TEST FUNCTIONS + + +- (void)test_match_alphabetTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + // Initialise the bitmasks for Bitap. + NSMutableDictionary *bitmask = [NSMutableDictionary dictionary]; + + [bitmask diff_setUnsignedIntegerValue:4 forUnicharKey:'a']; + [bitmask diff_setUnsignedIntegerValue:2 forUnicharKey:'b']; + [bitmask diff_setUnsignedIntegerValue:1 forUnicharKey:'c']; + STAssertEqualObjects(bitmask, [dmp match_alphabet:@"abc"], @"match_alphabet: Unique."); + + [bitmask removeAllObjects]; + [bitmask diff_setUnsignedIntegerValue:37 forUnicharKey:'a']; + [bitmask diff_setUnsignedIntegerValue:18 forUnicharKey:'b']; + [bitmask diff_setUnsignedIntegerValue:8 forUnicharKey:'c']; + STAssertEqualObjects(bitmask, [dmp match_alphabet:@"abcaba"], @"match_alphabet: Duplicates."); + + [dmp release]; +} + +- (void)test_match_bitapTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + // Bitap algorithm. + dmp.Match_Distance = 100; + dmp.Match_Threshold = 0.5f; + STAssertEquals((NSUInteger)5, [dmp match_bitapOfText:@"abcdefghijk" andPattern:@"fgh" near:5], @"match_bitap: Exact match #1."); + + STAssertEquals((NSUInteger)5, [dmp match_bitapOfText:@"abcdefghijk" andPattern:@"fgh" near:0], @"match_bitap: Exact match #2."); + + STAssertEquals((NSUInteger)4, [dmp match_bitapOfText:@"abcdefghijk" andPattern:@"efxhi" near:0], @"match_bitap: Fuzzy match #1."); + + STAssertEquals((NSUInteger)2, [dmp match_bitapOfText:@"abcdefghijk" andPattern:@"cdefxyhijk" near:5], @"match_bitap: Fuzzy match #2."); + + STAssertEquals((NSUInteger)NSNotFound, [dmp match_bitapOfText:@"abcdefghijk" andPattern:@"bxy" near:1], @"match_bitap: Fuzzy match #3."); + + STAssertEquals((NSUInteger)2, [dmp match_bitapOfText:@"123456789xx0" andPattern:@"3456789x0" near:2], @"match_bitap: Overflow."); + + STAssertEquals((NSUInteger)0, [dmp match_bitapOfText:@"abcdef" andPattern:@"xxabc" near:4], @"match_bitap: Before start match."); + + STAssertEquals((NSUInteger)3, [dmp match_bitapOfText:@"abcdef" andPattern:@"defyy" near:4], @"match_bitap: Beyond end match."); + + STAssertEquals((NSUInteger)0, [dmp match_bitapOfText:@"abcdef" andPattern:@"xabcdefy" near:0], @"match_bitap: Oversized pattern."); + + dmp.Match_Threshold = 0.4f; + STAssertEquals((NSUInteger)4, [dmp match_bitapOfText:@"abcdefghijk" andPattern:@"efxyhi" near:1], @"match_bitap: Threshold #1."); + + dmp.Match_Threshold = 0.3f; + STAssertEquals((NSUInteger)NSNotFound, [dmp match_bitapOfText:@"abcdefghijk" andPattern:@"efxyhi" near:1], @"match_bitap: Threshold #2."); + + dmp.Match_Threshold = 0.0f; + STAssertEquals((NSUInteger)1, [dmp match_bitapOfText:@"abcdefghijk" andPattern:@"bcdef" near:1], @"match_bitap: Threshold #3."); + + dmp.Match_Threshold = 0.5f; + STAssertEquals((NSUInteger)0, [dmp match_bitapOfText:@"abcdexyzabcde" andPattern:@"abccde" near:3], @"match_bitap: Multiple select #1."); + + STAssertEquals((NSUInteger)8, [dmp match_bitapOfText:@"abcdexyzabcde" andPattern:@"abccde" near:5], @"match_bitap: Multiple select #2."); + + dmp.Match_Distance = 10; // Strict location. + STAssertEquals((NSUInteger)NSNotFound, [dmp match_bitapOfText:@"abcdefghijklmnopqrstuvwxyz" andPattern:@"abcdefg" near:24], @"match_bitap: Distance test #1."); + + STAssertEquals((NSUInteger)0, [dmp match_bitapOfText:@"abcdefghijklmnopqrstuvwxyz" andPattern:@"abcdxxefg" near:1], @"match_bitap: Distance test #2."); + + dmp.Match_Distance = 1000; // Loose location. + STAssertEquals((NSUInteger)0, [dmp match_bitapOfText:@"abcdefghijklmnopqrstuvwxyz" andPattern:@"abcdefg" near:24], @"match_bitap: Distance test #3."); + + [dmp release]; +} + +- (void)test_match_mainTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + // Full match. + STAssertEquals((NSUInteger)0, [dmp match_mainForText:@"abcdef" pattern:@"abcdef" near:1000], @"match_main: Equality."); + + STAssertEquals((NSUInteger)NSNotFound, [dmp match_mainForText:@"" pattern:@"abcdef" near:1], @"match_main: Null text."); + + STAssertEquals((NSUInteger)3, [dmp match_mainForText:@"abcdef" pattern:@"" near:3], @"match_main: Null pattern."); + + STAssertEquals((NSUInteger)3, [dmp match_mainForText:@"abcdef" pattern:@"de" near:3], @"match_main: Exact match."); + + STAssertEquals((NSUInteger)3, [dmp match_mainForText:@"abcdef" pattern:@"defy" near:4], @"match_main: Beyond end match."); + + STAssertEquals((NSUInteger)0, [dmp match_mainForText:@"abcdef" pattern:@"abcdefy" near:0], @"match_main: Oversized pattern."); + + dmp.Match_Threshold = 0.7f; + STAssertEquals((NSUInteger)4, [dmp match_mainForText:@"I am the very model of a modern major general." pattern:@" that berry " near:5], @"match_main: Complex match."); + dmp.Match_Threshold = 0.5f; + + // CHANGEME: Test null inputs + + [dmp release]; +} + + +#pragma mark Patch Test Functions +// PATCH TEST FUNCTIONS + + +- (void)test_patch_patchObjTest { + // Patch Object. + Patch *p = [[Patch new] autorelease]; + p.start1 = 20; + p.start2 = 21; + p.length1 = 18; + p.length2 = 17; + p.diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_EQUAL andText:@"jump"], + [Diff diffWithOperation:DIFF_DELETE andText:@"s"], + [Diff diffWithOperation:DIFF_INSERT andText:@"ed"], + [Diff diffWithOperation:DIFF_EQUAL andText:@" over "], + [Diff diffWithOperation:DIFF_DELETE andText:@"the"], + [Diff diffWithOperation:DIFF_INSERT andText:@"a"], + [Diff diffWithOperation:DIFF_EQUAL andText:@"\nlaz"], nil]; + NSString *strp = @"@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n"; + STAssertEqualObjects(strp, [p description], @"Patch: description."); +} + +- (void)test_patch_fromTextTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + STAssertTrue(((NSMutableArray *)[dmp patch_fromText:@"" error:NULL]).count == 0, @"patch_fromText: #0."); + + NSString *strp = @"@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n"; + STAssertEqualObjects(strp, [[[dmp patch_fromText:strp error:NULL] objectAtIndex:0] description], @"patch_fromText: #1."); + + STAssertEqualObjects(@"@@ -1 +1 @@\n-a\n+b\n", [[[dmp patch_fromText:@"@@ -1 +1 @@\n-a\n+b\n" error:NULL] objectAtIndex:0] description], @"patch_fromText: #2."); + + STAssertEqualObjects(@"@@ -1,3 +0,0 @@\n-abc\n", [[[dmp patch_fromText:@"@@ -1,3 +0,0 @@\n-abc\n" error:NULL] objectAtIndex:0] description], @"patch_fromText: #3."); + + STAssertEqualObjects(@"@@ -0,0 +1,3 @@\n+abc\n", [[[dmp patch_fromText:@"@@ -0,0 +1,3 @@\n+abc\n" error:NULL] objectAtIndex:0] description], @"patch_fromText: #4."); + + // Generates error. + NSError *error = nil; + NSMutableArray *patches = [dmp patch_fromText:@"Bad\nPatch\n" error:&error]; + if (patches != nil || error == nil) { + // Error expected. + STFail(@"patch_fromText: #5."); + } + error = nil; + + [dmp release]; +} + +- (void)test_patch_toTextTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + NSString *strp = @"@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n"; + NSMutableArray *patches; + patches = [dmp patch_fromText:strp error:NULL]; + STAssertEqualObjects(strp, [dmp patch_toText:patches], @"toText Test #1"); + + strp = @"@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n"; + patches = [dmp patch_fromText:strp error:NULL]; + STAssertEqualObjects(strp, [dmp patch_toText:patches], @"toText Test #2"); + + [dmp release]; +} + +- (void)test_patch_addContextTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + dmp.Patch_Margin = 4; + Patch *p; + p = [[dmp patch_fromText:@"@@ -21,4 +21,10 @@\n-jump\n+somersault\n" error:NULL] objectAtIndex:0]; + [dmp patch_addContextToPatch:p sourceText:@"The quick brown fox jumps over the lazy dog."]; + STAssertEqualObjects(@"@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n", [p description], @"patch_addContext: Simple case."); + + p = [[dmp patch_fromText:@"@@ -21,4 +21,10 @@\n-jump\n+somersault\n" error:NULL] objectAtIndex:0]; + [dmp patch_addContextToPatch:p sourceText:@"The quick brown fox jumps."]; + STAssertEqualObjects(@"@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n", [p description], @"patch_addContext: Not enough trailing context."); + + p = [[dmp patch_fromText:@"@@ -3 +3,2 @@\n-e\n+at\n" error:NULL] objectAtIndex:0]; + [dmp patch_addContextToPatch:p sourceText:@"The quick brown fox jumps."]; + STAssertEqualObjects(@"@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n", [p description], @"patch_addContext: Not enough leading context."); + + p = [[dmp patch_fromText:@"@@ -3 +3,2 @@\n-e\n+at\n" error:NULL] objectAtIndex:0]; + [dmp patch_addContextToPatch:p sourceText:@"The quick brown fox jumps. The quick brown fox crashes."]; + STAssertEqualObjects(@"@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n", [p description], @"patch_addContext: Ambiguity."); + + [dmp release]; +} + +- (void)test_patch_makeTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + NSMutableArray *patches; + patches = [dmp patch_makeFromOldString:@"" andNewString:@""]; + STAssertEqualObjects(@"", [dmp patch_toText:patches], @"patch_make: Null case."); + + NSString *text1 = @"The quick brown fox jumps over the lazy dog."; + NSString *text2 = @"That quick brown fox jumped over a lazy dog."; + NSString *expectedPatch = @"@@ -1,8 +1,7 @@\n Th\n-at\n+e\n qui\n@@ -21,17 +21,18 @@\n jump\n-ed\n+s\n over \n-a\n+the\n laz\n"; + // The second patch must be @"-21,17 +21,18", not @"-22,17 +21,18" due to rolling context. + patches = [dmp patch_makeFromOldString:text2 andNewString:text1]; + STAssertEqualObjects(expectedPatch, [dmp patch_toText:patches], @"patch_make: Text2+Text1 inputs."); + + expectedPatch = @"@@ -1,11 +1,12 @@\n Th\n-e\n+at\n quick b\n@@ -22,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n"; + patches = [dmp patch_makeFromOldString:text1 andNewString:text2]; + STAssertEqualObjects(expectedPatch, [dmp patch_toText:patches], @"patch_make: Text1+Text2 inputs."); + + NSMutableArray *diffs = [dmp diff_mainOfOldString:text1 andNewString:text2 checkLines:NO]; + patches = [dmp patch_makeFromDiffs:diffs]; + STAssertEqualObjects(expectedPatch, [dmp patch_toText:patches], @"patch_make: Diff input."); + + patches = [dmp patch_makeFromOldString:text1 andDiffs:diffs]; + STAssertEqualObjects(expectedPatch, [dmp patch_toText:patches], @"patch_make: Text1+Diff inputs."); + + patches = [dmp patch_makeFromOldString:text1 newString:text2 diffs:diffs]; + STAssertEqualObjects(expectedPatch, [dmp patch_toText:patches], @"patch_make: Text1+Text2+Diff inputs (deprecated)."); + + patches = [dmp patch_makeFromOldString:@"`1234567890-=[]\\;',./" andNewString:@"~!@#$%^&*()_+{}|:\"<>?"]; + STAssertEqualObjects(@"@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n", + [dmp patch_toText:patches], + @"patch_toText: Character encoding."); + + diffs = [NSMutableArray arrayWithObjects: + [Diff diffWithOperation:DIFF_DELETE andText:@"`1234567890-=[]\\;',./"], + [Diff diffWithOperation:DIFF_INSERT andText:@"~!@#$%^&*()_+{}|:\"<>?"], nil]; + STAssertEqualObjects(diffs, + ((Patch *)[[dmp patch_fromText:@"@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n" error:NULL] objectAtIndex:0]).diffs, + @"patch_fromText: Character decoding."); + + NSMutableString *text1Mutable = [NSMutableString string]; + for (int x = 0; x < 100; x++) { + [text1Mutable appendString:@"abcdef"]; + } + text1 = text1Mutable; + text2 = [text1 stringByAppendingString:@"123"]; + // CHANGEME: Why does this implementation produce a different, more brief patch? + //expectedPatch = @"@@ -573,28 +573,31 @@\n cdefabcdefabcdefabcdefabcdef\n+123\n"; + expectedPatch = @"@@ -597,4 +597,7 @@\n cdef\n+123\n"; + patches = [dmp patch_makeFromOldString:text1 andNewString:text2]; + STAssertEqualObjects(expectedPatch, [dmp patch_toText:patches], @"patch_make: Long string with repeats."); + + // CHANGEME: Test null inputs + + [dmp release]; +} + + +- (void)test_patch_splitMaxTest { + // Assumes that Match_MaxBits is 32. + DiffMatchPatch *dmp = [DiffMatchPatch new]; + NSMutableArray *patches; + + patches = [dmp patch_makeFromOldString:@"abcdefghijklmnopqrstuvwxyz01234567890" andNewString:@"XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0"]; + [dmp patch_splitMax:patches]; + STAssertEqualObjects(@"@@ -1,32 +1,46 @@\n+X\n ab\n+X\n cd\n+X\n ef\n+X\n gh\n+X\n ij\n+X\n kl\n+X\n mn\n+X\n op\n+X\n qr\n+X\n st\n+X\n uv\n+X\n wx\n+X\n yz\n+X\n 012345\n@@ -25,13 +39,18 @@\n zX01\n+X\n 23\n+X\n 45\n+X\n 67\n+X\n 89\n+X\n 0\n", [dmp patch_toText:patches], @"Assumes that Match_MaxBits is 32 #1"); + + patches = [dmp patch_makeFromOldString:@"abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz" andNewString:@"abcdefuvwxyz"]; + NSString *oldToText = [dmp patch_toText:patches]; + [dmp patch_splitMax:patches]; + STAssertEqualObjects(oldToText, [dmp patch_toText:patches], @"Assumes that Match_MaxBits is 32 #2"); + + patches = [dmp patch_makeFromOldString:@"1234567890123456789012345678901234567890123456789012345678901234567890" andNewString:@"abc"]; + [dmp patch_splitMax:patches]; + STAssertEqualObjects(@"@@ -1,32 +1,4 @@\n-1234567890123456789012345678\n 9012\n@@ -29,32 +1,4 @@\n-9012345678901234567890123456\n 7890\n@@ -57,14 +1,3 @@\n-78901234567890\n+abc\n", [dmp patch_toText:patches], @"Assumes that Match_MaxBits is 32 #3"); + + patches = [dmp patch_makeFromOldString:@"abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1" andNewString:@"abcdefghij , h : 1 , t : 1 abcdefghij , h : 1 , t : 1 abcdefghij , h : 0 , t : 1"]; + [dmp patch_splitMax:patches]; + STAssertEqualObjects(@"@@ -2,32 +2,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n@@ -29,32 +29,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n", [dmp patch_toText:patches], @"Assumes that Match_MaxBits is 32 #4"); + + [dmp release]; +} + +- (void)test_patch_addPaddingTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + NSMutableArray *patches; + patches = [dmp patch_makeFromOldString:@"" andNewString:@"test"]; + STAssertEqualObjects(@"@@ -0,0 +1,4 @@\n+test\n", + [dmp patch_toText:patches], + @"patch_addPadding: Both edges full."); + [dmp patch_addPadding:patches]; + STAssertEqualObjects(@"@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n", + [dmp patch_toText:patches], + @"patch_addPadding: Both edges full."); + + patches = [dmp patch_makeFromOldString:@"XY" andNewString:@"XtestY"]; + STAssertEqualObjects(@"@@ -1,2 +1,6 @@\n X\n+test\n Y\n", + [dmp patch_toText:patches], + @"patch_addPadding: Both edges partial."); + [dmp patch_addPadding:patches]; + STAssertEqualObjects(@"@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n", + [dmp patch_toText:patches], + @"patch_addPadding: Both edges partial."); + + patches = [dmp patch_makeFromOldString:@"XXXXYYYY" andNewString:@"XXXXtestYYYY"]; + STAssertEqualObjects(@"@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n", + [dmp patch_toText:patches], + @"patch_addPadding: Both edges none."); + [dmp patch_addPadding:patches]; + STAssertEqualObjects(@"@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n", + [dmp patch_toText:patches], + @"patch_addPadding: Both edges none."); + + [dmp release]; +} + +- (void)test_patch_applyTest { + DiffMatchPatch *dmp = [DiffMatchPatch new]; + + dmp.Match_Distance = 1000; + dmp.Match_Threshold = 0.5f; + dmp.Patch_DeleteThreshold = 0.5f; + NSMutableArray *patches; + patches = [dmp patch_makeFromOldString:@"" andNewString:@""]; + NSArray *results = [dmp patch_apply:patches toString:@"Hello world."]; + NSMutableArray *boolArray = [results objectAtIndex:1]; + NSString *resultStr = [NSString stringWithFormat:@"%@\t%lu", [results objectAtIndex:0], (unsigned long)boolArray.count]; + STAssertEqualObjects(@"Hello world.\t0", resultStr, @"patch_apply: Null case."); + + patches = [dmp patch_makeFromOldString:@"The quick brown fox jumps over the lazy dog." andNewString:@"That quick brown fox jumped over a lazy dog."]; + results = [dmp patch_apply:patches toString:@"The quick brown fox jumps over the lazy dog."]; + boolArray = [results objectAtIndex:1]; + resultStr = [NSString stringWithFormat:@"%@\t%@\t%@", [results objectAtIndex:0], stringForBOOL([boolArray objectAtIndex:0]), stringForBOOL([boolArray objectAtIndex:1])]; + STAssertEqualObjects(@"That quick brown fox jumped over a lazy dog.\ttrue\ttrue", resultStr, @"patch_apply: Exact match."); + + results = [dmp patch_apply:patches toString:@"The quick red rabbit jumps over the tired tiger."]; + boolArray = [results objectAtIndex:1]; + resultStr = [NSString stringWithFormat:@"%@\t%@\t%@", [results objectAtIndex:0], stringForBOOL([boolArray objectAtIndex:0]), stringForBOOL([boolArray objectAtIndex:1])]; + STAssertEqualObjects(@"That quick red rabbit jumped over a tired tiger.\ttrue\ttrue", resultStr, @"patch_apply: Partial match."); + + results = [dmp patch_apply:patches toString:@"I am the very model of a modern major general."]; + boolArray = [results objectAtIndex:1]; + resultStr = [NSString stringWithFormat:@"%@\t%@\t%@", [results objectAtIndex:0], stringForBOOL([boolArray objectAtIndex:0]), stringForBOOL([boolArray objectAtIndex:1])]; + STAssertEqualObjects(@"I am the very model of a modern major general.\tfalse\tfalse", resultStr, @"patch_apply: Failed match."); + + patches = [dmp patch_makeFromOldString:@"x1234567890123456789012345678901234567890123456789012345678901234567890y" andNewString:@"xabcy"]; + results = [dmp patch_apply:patches toString:@"x123456789012345678901234567890-----++++++++++-----123456789012345678901234567890y"]; + boolArray = [results objectAtIndex:1]; + resultStr = [NSString stringWithFormat:@"%@\t%@\t%@", [results objectAtIndex:0], stringForBOOL([boolArray objectAtIndex:0]), stringForBOOL([boolArray objectAtIndex:1])]; + STAssertEqualObjects(@"xabcy\ttrue\ttrue", resultStr, @"patch_apply: Big delete, small change."); + + patches = [dmp patch_makeFromOldString:@"x1234567890123456789012345678901234567890123456789012345678901234567890y" andNewString:@"xabcy"]; + results = [dmp patch_apply:patches toString:@"x12345678901234567890---------------++++++++++---------------12345678901234567890y"]; + boolArray = [results objectAtIndex:1]; + resultStr = [NSString stringWithFormat:@"%@\t%@\t%@", [results objectAtIndex:0], stringForBOOL([boolArray objectAtIndex:0]), stringForBOOL([boolArray objectAtIndex:1])]; + STAssertEqualObjects(@"xabc12345678901234567890---------------++++++++++---------------12345678901234567890y\tfalse\ttrue", resultStr, @"patch_apply: Big delete, big change 1."); + + dmp.Patch_DeleteThreshold = 0.6f; + patches = [dmp patch_makeFromOldString:@"x1234567890123456789012345678901234567890123456789012345678901234567890y" andNewString:@"xabcy"]; + results = [dmp patch_apply:patches toString:@"x12345678901234567890---------------++++++++++---------------12345678901234567890y"]; + boolArray = [results objectAtIndex:1]; + resultStr = [NSString stringWithFormat:@"%@\t%@\t%@", [results objectAtIndex:0], stringForBOOL([boolArray objectAtIndex:0]), stringForBOOL([boolArray objectAtIndex:1])]; + STAssertEqualObjects(@"xabcy\ttrue\ttrue", resultStr, @"patch_apply: Big delete, big change 2."); + dmp.Patch_DeleteThreshold = 0.5f; + + dmp.Match_Threshold = 0.0f; + dmp.Match_Distance = 0; + patches = [dmp patch_makeFromOldString:@"abcdefghijklmnopqrstuvwxyz--------------------1234567890" andNewString:@"abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------1234567YYYYYYYYYY890"]; + results = [dmp patch_apply:patches toString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890"]; + boolArray = [results objectAtIndex:1]; + resultStr = [NSString stringWithFormat:@"%@\t%@\t%@", [results objectAtIndex:0], stringForBOOL([boolArray objectAtIndex:0]), stringForBOOL([boolArray objectAtIndex:1])]; + STAssertEqualObjects(@"ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890\tfalse\ttrue", resultStr, @"patch_apply: Compensate for failed patch."); + dmp.Match_Threshold = 0.5f; + dmp.Match_Distance = 1000; + + patches = [dmp patch_makeFromOldString:@"" andNewString:@"test"]; + NSString *patchStr = [dmp patch_toText:patches]; + [dmp patch_apply:patches toString:@""]; + STAssertEqualObjects(patchStr, [dmp patch_toText:patches], @"patch_apply: No side effects."); + + patches = [dmp patch_makeFromOldString:@"The quick brown fox jumps over the lazy dog." andNewString:@"Woof"]; + patchStr = [dmp patch_toText:patches]; + [dmp patch_apply:patches toString:@"The quick brown fox jumps over the lazy dog."]; + STAssertEqualObjects(patchStr, [dmp patch_toText:patches], @"patch_apply: No side effects with major delete."); + + patches = [dmp patch_makeFromOldString:@"" andNewString:@"test"]; + results = [dmp patch_apply:patches toString:@""]; + boolArray = [results objectAtIndex:1]; + resultStr = [NSString stringWithFormat:@"%@\t%@", [results objectAtIndex:0], stringForBOOL([boolArray objectAtIndex:0])]; + STAssertEqualObjects(@"test\ttrue", resultStr, @"patch_apply: Edge exact match."); + + patches = [dmp patch_makeFromOldString:@"XY" andNewString:@"XtestY"]; + results = [dmp patch_apply:patches toString:@"XY"]; + boolArray = [results objectAtIndex:1]; + resultStr = [NSString stringWithFormat:@"%@\t%@", [results objectAtIndex:0], stringForBOOL([boolArray objectAtIndex:0])]; + STAssertEqualObjects(@"XtestY\ttrue", resultStr, @"patch_apply: Near edge exact match."); + + patches = [dmp patch_makeFromOldString:@"y" andNewString:@"y123"]; + results = [dmp patch_apply:patches toString:@"x"]; + boolArray = [results objectAtIndex:1]; + resultStr = [NSString stringWithFormat:@"%@\t%@", [results objectAtIndex:0], stringForBOOL([boolArray objectAtIndex:0])]; + STAssertEqualObjects(@"x123\ttrue", resultStr, @"patch_apply: Edge partial match."); + + [dmp release]; +} + + +#pragma mark Test Utility Functions +// TEST UTILITY FUNCTIONS + + +- (NSArray *)diff_rebuildtexts:(NSMutableArray *)diffs; +{ + NSArray *text = [NSMutableArray arrayWithObjects:[NSMutableString string], [NSMutableString string], nil]; + for (Diff *myDiff in diffs) { + if (myDiff.operation != DIFF_INSERT) { + [[text objectAtIndex:0] appendString:myDiff.text]; + } + if (myDiff.operation != DIFF_DELETE) { + [[text objectAtIndex:1] appendString:myDiff.text]; + } + } + return text; +} + +@end diff --git a/objectivec/Tests/speedtest.m b/objectivec/Tests/speedtest.m new file mode 100755 index 0000000..2112c62 --- /dev/null +++ b/objectivec/Tests/speedtest.m @@ -0,0 +1,50 @@ +/* + * Diff Match and Patch -- Test harness + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: fraser@google.com (Neil Fraser) + * ObjC port: jan@geheimwerk.de (Jan Weiß) + */ + +#import + +#import + +int main (int argc, const char * argv[]) { + NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; + + NSString *text1 = [NSString stringWithContentsOfFile:@"Speedtest1.txt" + encoding:NSUTF8StringEncoding + error:NULL]; + + NSString *text2 = [NSString stringWithContentsOfFile:@"Speedtest2.txt" + encoding:NSUTF8StringEncoding + error:NULL]; + + DiffMatchPatch *dmp = [DiffMatchPatch new]; + dmp.Diff_Timeout = 0; + + NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate]; + [dmp diff_mainOfOldString:text1 andNewString:text2]; + NSTimeInterval duration = [NSDate timeIntervalSinceReferenceDate] - start; + + [dmp release]; + + NSLog(@"Elapsed time: %.4lf", (double)duration); + + [pool drain]; + return 0; +} diff --git a/objectivec/speedtest_Prefix.pch b/objectivec/speedtest_Prefix.pch new file mode 100755 index 0000000..c0aa561 --- /dev/null +++ b/objectivec/speedtest_Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'speedtest' target in the 'DiffMatchPatch' project. +// + +#ifdef __OBJC__ + #import +#endif diff --git a/python2/__init__.py b/python2/__init__.py new file mode 100644 index 0000000..bd6e8b6 --- /dev/null +++ b/python2/__init__.py @@ -0,0 +1,2 @@ +from .diff_match_patch import diff_match_patch, patch_obj + diff --git a/python2/diff_match_patch.py b/python2/diff_match_patch.py new file mode 100644 index 0000000..3ae6252 --- /dev/null +++ b/python2/diff_match_patch.py @@ -0,0 +1,1918 @@ +#!/usr/bin/python2.4 + +from __future__ import division + +"""Diff Match and Patch +Copyright 2018 The diff-match-patch Authors. +https://github.com/google/diff-match-patch + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Functions for diff, match and patch. + +Computes the difference between two texts to create a patch. +Applies the patch onto another text, allowing for errors. +""" + +__author__ = 'fraser@google.com (Neil Fraser)' + +import math +import re +import sys +import time +import urllib + +class diff_match_patch: + """Class containing the diff, match and patch methods. + + Also contains the behaviour settings. + """ + + def __init__(self): + """Inits a diff_match_patch object with default settings. + Redefine these in your program to override the defaults. + """ + + # Number of seconds to map a diff before giving up (0 for infinity). + self.Diff_Timeout = 1.0 + # Cost of an empty edit operation in terms of edit characters. + self.Diff_EditCost = 4 + # At what point is no match declared (0.0 = perfection, 1.0 = very loose). + self.Match_Threshold = 0.5 + # How far to search for a match (0 = exact location, 1000+ = broad match). + # A match this many characters away from the expected location will add + # 1.0 to the score (0.0 is a perfect match). + self.Match_Distance = 1000 + # When deleting a large block of text (over ~64 characters), how close do + # the contents have to be to match the expected contents. (0.0 = perfection, + # 1.0 = very loose). Note that Match_Threshold controls how closely the + # end points of a delete need to match. + self.Patch_DeleteThreshold = 0.5 + # Chunk size for context length. + self.Patch_Margin = 4 + + # The number of bits in an int. + # Python has no maximum, thus to disable patch splitting set to 0. + # However to avoid long patches in certain pathological cases, use 32. + # Multiple short patches (using native ints) are much faster than long ones. + self.Match_MaxBits = 32 + + # DIFF FUNCTIONS + + # The data structure representing a diff is an array of tuples: + # [(DIFF_DELETE, "Hello"), (DIFF_INSERT, "Goodbye"), (DIFF_EQUAL, " world.")] + # which means: delete "Hello", add "Goodbye" and keep " world." + DIFF_DELETE = -1 + DIFF_INSERT = 1 + DIFF_EQUAL = 0 + + def diff_main(self, text1, text2, checklines=True, deadline=None): + """Find the differences between two texts. Simplifies the problem by + stripping any common prefix or suffix off the texts before diffing. + + Args: + text1: Old string to be diffed. + text2: New string to be diffed. + checklines: Optional speedup flag. If present and false, then don't run + a line-level diff first to identify the changed areas. + Defaults to true, which does a faster, slightly less optimal diff. + deadline: Optional time when the diff should be complete by. Used + internally for recursive calls. Users should set DiffTimeout instead. + + Returns: + Array of changes. + """ + # Set a deadline by which time the diff must be complete. + if deadline == None: + # Unlike in most languages, Python counts time in seconds. + if self.Diff_Timeout <= 0: + deadline = sys.maxint + else: + deadline = time.time() + self.Diff_Timeout + + # Check for null inputs. + if text1 == None or text2 == None: + raise ValueError("Null inputs. (diff_main)") + + # Check for equality (speedup). + if text1 == text2: + if text1: + return [(self.DIFF_EQUAL, text1)] + return [] + + # Trim off common prefix (speedup). + commonlength = self.diff_commonPrefix(text1, text2) + commonprefix = text1[:commonlength] + text1 = text1[commonlength:] + text2 = text2[commonlength:] + + # Trim off common suffix (speedup). + commonlength = self.diff_commonSuffix(text1, text2) + if commonlength == 0: + commonsuffix = '' + else: + commonsuffix = text1[-commonlength:] + text1 = text1[:-commonlength] + text2 = text2[:-commonlength] + + # Compute the diff on the middle block. + diffs = self.diff_compute(text1, text2, checklines, deadline) + + # Restore the prefix and suffix. + if commonprefix: + diffs[:0] = [(self.DIFF_EQUAL, commonprefix)] + if commonsuffix: + diffs.append((self.DIFF_EQUAL, commonsuffix)) + self.diff_cleanupMerge(diffs) + return diffs + + def diff_compute(self, text1, text2, checklines, deadline): + """Find the differences between two texts. Assumes that the texts do not + have any common prefix or suffix. + + Args: + text1: Old string to be diffed. + text2: New string to be diffed. + checklines: Speedup flag. If false, then don't run a line-level diff + first to identify the changed areas. + If true, then run a faster, slightly less optimal diff. + deadline: Time when the diff should be complete by. + + Returns: + Array of changes. + """ + if not text1: + # Just add some text (speedup). + return [(self.DIFF_INSERT, text2)] + + if not text2: + # Just delete some text (speedup). + return [(self.DIFF_DELETE, text1)] + + if len(text1) > len(text2): + (longtext, shorttext) = (text1, text2) + else: + (shorttext, longtext) = (text1, text2) + i = longtext.find(shorttext) + if i != -1: + # Shorter text is inside the longer text (speedup). + diffs = [(self.DIFF_INSERT, longtext[:i]), (self.DIFF_EQUAL, shorttext), + (self.DIFF_INSERT, longtext[i + len(shorttext):])] + # Swap insertions for deletions if diff is reversed. + if len(text1) > len(text2): + diffs[0] = (self.DIFF_DELETE, diffs[0][1]) + diffs[2] = (self.DIFF_DELETE, diffs[2][1]) + return diffs + + if len(shorttext) == 1: + # Single character string. + # After the previous speedup, the character can't be an equality. + return [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)] + + # Check to see if the problem can be split in two. + hm = self.diff_halfMatch(text1, text2) + if hm: + # A half-match was found, sort out the return data. + (text1_a, text1_b, text2_a, text2_b, mid_common) = hm + # Send both pairs off for separate processing. + diffs_a = self.diff_main(text1_a, text2_a, checklines, deadline) + diffs_b = self.diff_main(text1_b, text2_b, checklines, deadline) + # Merge the results. + return diffs_a + [(self.DIFF_EQUAL, mid_common)] + diffs_b + + if checklines and len(text1) > 100 and len(text2) > 100: + return self.diff_lineMode(text1, text2, deadline) + + return self.diff_bisect(text1, text2, deadline) + + def diff_lineMode(self, text1, text2, deadline): + """Do a quick line-level diff on both strings, then rediff the parts for + greater accuracy. + This speedup can produce non-minimal diffs. + + Args: + text1: Old string to be diffed. + text2: New string to be diffed. + deadline: Time when the diff should be complete by. + + Returns: + Array of changes. + """ + + # Scan the text on a line-by-line basis first. + (text1, text2, linearray) = self.diff_linesToChars(text1, text2) + + diffs = self.diff_main(text1, text2, False, deadline) + + # Convert the diff back to original text. + self.diff_charsToLines(diffs, linearray) + # Eliminate freak matches (e.g. blank lines) + self.diff_cleanupSemantic(diffs) + + # Rediff any replacement blocks, this time character-by-character. + # Add a dummy entry at the end. + diffs.append((self.DIFF_EQUAL, '')) + pointer = 0 + count_delete = 0 + count_insert = 0 + text_delete = '' + text_insert = '' + while pointer < len(diffs): + if diffs[pointer][0] == self.DIFF_INSERT: + count_insert += 1 + text_insert += diffs[pointer][1] + elif diffs[pointer][0] == self.DIFF_DELETE: + count_delete += 1 + text_delete += diffs[pointer][1] + elif diffs[pointer][0] == self.DIFF_EQUAL: + # Upon reaching an equality, check for prior redundancies. + if count_delete >= 1 and count_insert >= 1: + # Delete the offending records and add the merged ones. + a = self.diff_main(text_delete, text_insert, False, deadline) + diffs[pointer - count_delete - count_insert : pointer] = a + pointer = pointer - count_delete - count_insert + len(a) + count_insert = 0 + count_delete = 0 + text_delete = '' + text_insert = '' + + pointer += 1 + + diffs.pop() # Remove the dummy entry at the end. + + return diffs + + def diff_bisect(self, text1, text2, deadline): + """Find the 'middle snake' of a diff, split the problem in two + and return the recursively constructed diff. + See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + + Args: + text1: Old string to be diffed. + text2: New string to be diffed. + deadline: Time at which to bail if not yet complete. + + Returns: + Array of diff tuples. + """ + + # Cache the text lengths to prevent multiple calls. + text1_length = len(text1) + text2_length = len(text2) + max_d = (text1_length + text2_length + 1) // 2 + v_offset = max_d + v_length = 2 * max_d + v1 = [-1] * v_length + v1[v_offset + 1] = 0 + v2 = v1[:] + delta = text1_length - text2_length + # If the total number of characters is odd, then the front path will + # collide with the reverse path. + front = (delta % 2 != 0) + # Offsets for start and end of k loop. + # Prevents mapping of space beyond the grid. + k1start = 0 + k1end = 0 + k2start = 0 + k2end = 0 + for d in xrange(max_d): + # Bail out if deadline is reached. + if time.time() > deadline: + break + + # Walk the front path one step. + for k1 in xrange(-d + k1start, d + 1 - k1end, 2): + k1_offset = v_offset + k1 + if k1 == -d or (k1 != d and + v1[k1_offset - 1] < v1[k1_offset + 1]): + x1 = v1[k1_offset + 1] + else: + x1 = v1[k1_offset - 1] + 1 + y1 = x1 - k1 + while (x1 < text1_length and y1 < text2_length and + text1[x1] == text2[y1]): + x1 += 1 + y1 += 1 + v1[k1_offset] = x1 + if x1 > text1_length: + # Ran off the right of the graph. + k1end += 2 + elif y1 > text2_length: + # Ran off the bottom of the graph. + k1start += 2 + elif front: + k2_offset = v_offset + delta - k1 + if k2_offset >= 0 and k2_offset < v_length and v2[k2_offset] != -1: + # Mirror x2 onto top-left coordinate system. + x2 = text1_length - v2[k2_offset] + if x1 >= x2: + # Overlap detected. + return self.diff_bisectSplit(text1, text2, x1, y1, deadline) + + # Walk the reverse path one step. + for k2 in xrange(-d + k2start, d + 1 - k2end, 2): + k2_offset = v_offset + k2 + if k2 == -d or (k2 != d and + v2[k2_offset - 1] < v2[k2_offset + 1]): + x2 = v2[k2_offset + 1] + else: + x2 = v2[k2_offset - 1] + 1 + y2 = x2 - k2 + while (x2 < text1_length and y2 < text2_length and + text1[-x2 - 1] == text2[-y2 - 1]): + x2 += 1 + y2 += 1 + v2[k2_offset] = x2 + if x2 > text1_length: + # Ran off the left of the graph. + k2end += 2 + elif y2 > text2_length: + # Ran off the top of the graph. + k2start += 2 + elif not front: + k1_offset = v_offset + delta - k2 + if k1_offset >= 0 and k1_offset < v_length and v1[k1_offset] != -1: + x1 = v1[k1_offset] + y1 = v_offset + x1 - k1_offset + # Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2 + if x1 >= x2: + # Overlap detected. + return self.diff_bisectSplit(text1, text2, x1, y1, deadline) + + # Diff took too long and hit the deadline or + # number of diffs equals number of characters, no commonality at all. + return [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)] + + def diff_bisectSplit(self, text1, text2, x, y, deadline): + """Given the location of the 'middle snake', split the diff in two parts + and recurse. + + Args: + text1: Old string to be diffed. + text2: New string to be diffed. + x: Index of split point in text1. + y: Index of split point in text2. + deadline: Time at which to bail if not yet complete. + + Returns: + Array of diff tuples. + """ + text1a = text1[:x] + text2a = text2[:y] + text1b = text1[x:] + text2b = text2[y:] + + # Compute both diffs serially. + diffs = self.diff_main(text1a, text2a, False, deadline) + diffsb = self.diff_main(text1b, text2b, False, deadline) + + return diffs + diffsb + + def diff_linesToChars(self, text1, text2): + """Split two texts into an array of strings. Reduce the texts to a string + of hashes where each Unicode character represents one line. + + Args: + text1: First string. + text2: Second string. + + Returns: + Three element tuple, containing the encoded text1, the encoded text2 and + the array of unique strings. The zeroth element of the array of unique + strings is intentionally blank. + """ + lineArray = [] # e.g. lineArray[4] == "Hello\n" + lineHash = {} # e.g. lineHash["Hello\n"] == 4 + + # "\x00" is a valid character, but various debuggers don't like it. + # So we'll insert a junk entry to avoid generating a null character. + lineArray.append('') + + def diff_linesToCharsMunge(text): + """Split a text into an array of strings. Reduce the texts to a string + of hashes where each Unicode character represents one line. + Modifies linearray and linehash through being a closure. + + Args: + text: String to encode. + + Returns: + Encoded string. + """ + chars = [] + # Walk the text, pulling out a substring for each line. + # text.split('\n') would would temporarily double our memory footprint. + # Modifying text would create many large strings to garbage collect. + lineStart = 0 + lineEnd = -1 + while lineEnd < len(text) - 1: + lineEnd = text.find('\n', lineStart) + if lineEnd == -1: + lineEnd = len(text) - 1 + line = text[lineStart:lineEnd + 1] + lineStart = lineEnd + 1 + + if line in lineHash: + chars.append(unichr(lineHash[line])) + else: + lineArray.append(line) + lineHash[line] = len(lineArray) - 1 + chars.append(unichr(len(lineArray) - 1)) + return "".join(chars) + + chars1 = diff_linesToCharsMunge(text1) + chars2 = diff_linesToCharsMunge(text2) + return (chars1, chars2, lineArray) + + def diff_charsToLines(self, diffs, lineArray): + """Rehydrate the text in a diff from a string of line hashes to real lines + of text. + + Args: + diffs: Array of diff tuples. + lineArray: Array of unique strings. + """ + for x in xrange(len(diffs)): + text = [] + for char in diffs[x][1]: + text.append(lineArray[ord(char)]) + diffs[x] = (diffs[x][0], "".join(text)) + + def diff_commonPrefix(self, text1, text2): + """Determine the common prefix of two strings. + + Args: + text1: First string. + text2: Second string. + + Returns: + The number of characters common to the start of each string. + """ + # Quick check for common null cases. + if not text1 or not text2 or text1[0] != text2[0]: + return 0 + # Binary search. + # Performance analysis: http://neil.fraser.name/news/2007/10/09/ + pointermin = 0 + pointermax = min(len(text1), len(text2)) + pointermid = pointermax + pointerstart = 0 + while pointermin < pointermid: + if text1[pointerstart:pointermid] == text2[pointerstart:pointermid]: + pointermin = pointermid + pointerstart = pointermin + else: + pointermax = pointermid + pointermid = (pointermax - pointermin) // 2 + pointermin + return pointermid + + def diff_commonSuffix(self, text1, text2): + """Determine the common suffix of two strings. + + Args: + text1: First string. + text2: Second string. + + Returns: + The number of characters common to the end of each string. + """ + # Quick check for common null cases. + if not text1 or not text2 or text1[-1] != text2[-1]: + return 0 + # Binary search. + # Performance analysis: http://neil.fraser.name/news/2007/10/09/ + pointermin = 0 + pointermax = min(len(text1), len(text2)) + pointermid = pointermax + pointerend = 0 + while pointermin < pointermid: + if (text1[-pointermid:len(text1) - pointerend] == + text2[-pointermid:len(text2) - pointerend]): + pointermin = pointermid + pointerend = pointermin + else: + pointermax = pointermid + pointermid = (pointermax - pointermin) // 2 + pointermin + return pointermid + + def diff_commonOverlap(self, text1, text2): + """Determine if the suffix of one string is the prefix of another. + + Args: + text1 First string. + text2 Second string. + + Returns: + The number of characters common to the end of the first + string and the start of the second string. + """ + # Cache the text lengths to prevent multiple calls. + text1_length = len(text1) + text2_length = len(text2) + # Eliminate the null case. + if text1_length == 0 or text2_length == 0: + return 0 + # Truncate the longer string. + if text1_length > text2_length: + text1 = text1[-text2_length:] + elif text1_length < text2_length: + text2 = text2[:text1_length] + text_length = min(text1_length, text2_length) + # Quick check for the worst case. + if text1 == text2: + return text_length + + # Start by looking for a single character match + # and increase length until no match is found. + # Performance analysis: http://neil.fraser.name/news/2010/11/04/ + best = 0 + length = 1 + while True: + pattern = text1[-length:] + found = text2.find(pattern) + if found == -1: + return best + length += found + if found == 0 or text1[-length:] == text2[:length]: + best = length + length += 1 + + def diff_halfMatch(self, text1, text2): + """Do the two texts share a substring which is at least half the length of + the longer text? + This speedup can produce non-minimal diffs. + + Args: + text1: First string. + text2: Second string. + + Returns: + Five element Array, containing the prefix of text1, the suffix of text1, + the prefix of text2, the suffix of text2 and the common middle. Or None + if there was no match. + """ + if self.Diff_Timeout <= 0: + # Don't risk returning a non-optimal diff if we have unlimited time. + return None + if len(text1) > len(text2): + (longtext, shorttext) = (text1, text2) + else: + (shorttext, longtext) = (text1, text2) + if len(longtext) < 4 or len(shorttext) * 2 < len(longtext): + return None # Pointless. + + def diff_halfMatchI(longtext, shorttext, i): + """Does a substring of shorttext exist within longtext such that the + substring is at least half the length of longtext? + Closure, but does not reference any external variables. + + Args: + longtext: Longer string. + shorttext: Shorter string. + i: Start index of quarter length substring within longtext. + + Returns: + Five element Array, containing the prefix of longtext, the suffix of + longtext, the prefix of shorttext, the suffix of shorttext and the + common middle. Or None if there was no match. + """ + seed = longtext[i:i + len(longtext) // 4] + best_common = '' + j = shorttext.find(seed) + while j != -1: + prefixLength = self.diff_commonPrefix(longtext[i:], shorttext[j:]) + suffixLength = self.diff_commonSuffix(longtext[:i], shorttext[:j]) + if len(best_common) < suffixLength + prefixLength: + best_common = (shorttext[j - suffixLength:j] + + shorttext[j:j + prefixLength]) + best_longtext_a = longtext[:i - suffixLength] + best_longtext_b = longtext[i + prefixLength:] + best_shorttext_a = shorttext[:j - suffixLength] + best_shorttext_b = shorttext[j + prefixLength:] + j = shorttext.find(seed, j + 1) + + if len(best_common) * 2 >= len(longtext): + return (best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common) + else: + return None + + # First check if the second quarter is the seed for a half-match. + hm1 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 3) // 4) + # Check again based on the third quarter. + hm2 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 1) // 2) + if not hm1 and not hm2: + return None + elif not hm2: + hm = hm1 + elif not hm1: + hm = hm2 + else: + # Both matched. Select the longest. + if len(hm1[4]) > len(hm2[4]): + hm = hm1 + else: + hm = hm2 + + # A half-match was found, sort out the return data. + if len(text1) > len(text2): + (text1_a, text1_b, text2_a, text2_b, mid_common) = hm + else: + (text2_a, text2_b, text1_a, text1_b, mid_common) = hm + return (text1_a, text1_b, text2_a, text2_b, mid_common) + + def diff_cleanupSemantic(self, diffs): + """Reduce the number of edits by eliminating semantically trivial + equalities. + + Args: + diffs: Array of diff tuples. + """ + changes = False + equalities = [] # Stack of indices where equalities are found. + lastequality = None # Always equal to diffs[equalities[-1]][1] + pointer = 0 # Index of current position. + # Number of chars that changed prior to the equality. + length_insertions1, length_deletions1 = 0, 0 + # Number of chars that changed after the equality. + length_insertions2, length_deletions2 = 0, 0 + while pointer < len(diffs): + if diffs[pointer][0] == self.DIFF_EQUAL: # Equality found. + equalities.append(pointer) + length_insertions1, length_insertions2 = length_insertions2, 0 + length_deletions1, length_deletions2 = length_deletions2, 0 + lastequality = diffs[pointer][1] + else: # An insertion or deletion. + if diffs[pointer][0] == self.DIFF_INSERT: + length_insertions2 += len(diffs[pointer][1]) + else: + length_deletions2 += len(diffs[pointer][1]) + # Eliminate an equality that is smaller or equal to the edits on both + # sides of it. + if (lastequality and (len(lastequality) <= + max(length_insertions1, length_deletions1)) and + (len(lastequality) <= max(length_insertions2, length_deletions2))): + # Duplicate record. + diffs.insert(equalities[-1], (self.DIFF_DELETE, lastequality)) + # Change second copy to insert. + diffs[equalities[-1] + 1] = (self.DIFF_INSERT, + diffs[equalities[-1] + 1][1]) + # Throw away the equality we just deleted. + equalities.pop() + # Throw away the previous equality (it needs to be reevaluated). + if len(equalities): + equalities.pop() + if len(equalities): + pointer = equalities[-1] + else: + pointer = -1 + # Reset the counters. + length_insertions1, length_deletions1 = 0, 0 + length_insertions2, length_deletions2 = 0, 0 + lastequality = None + changes = True + pointer += 1 + + # Normalize the diff. + if changes: + self.diff_cleanupMerge(diffs) + self.diff_cleanupSemanticLossless(diffs) + + # Find any overlaps between deletions and insertions. + # e.g: abcxxxxxxdef + # -> abcxxxdef + # e.g: xxxabcdefxxx + # -> defxxxabc + # Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 1 + while pointer < len(diffs): + if (diffs[pointer - 1][0] == self.DIFF_DELETE and + diffs[pointer][0] == self.DIFF_INSERT): + deletion = diffs[pointer - 1][1] + insertion = diffs[pointer][1] + overlap_length1 = self.diff_commonOverlap(deletion, insertion) + overlap_length2 = self.diff_commonOverlap(insertion, deletion) + if overlap_length1 >= overlap_length2: + if (overlap_length1 >= len(deletion) / 2.0 or + overlap_length1 >= len(insertion) / 2.0): + # Overlap found. Insert an equality and trim the surrounding edits. + diffs.insert(pointer, (self.DIFF_EQUAL, + insertion[:overlap_length1])) + diffs[pointer - 1] = (self.DIFF_DELETE, + deletion[:len(deletion) - overlap_length1]) + diffs[pointer + 1] = (self.DIFF_INSERT, + insertion[overlap_length1:]) + pointer += 1 + else: + if (overlap_length2 >= len(deletion) / 2.0 or + overlap_length2 >= len(insertion) / 2.0): + # Reverse overlap found. + # Insert an equality and swap and trim the surrounding edits. + diffs.insert(pointer, (self.DIFF_EQUAL, deletion[:overlap_length2])) + diffs[pointer - 1] = (self.DIFF_INSERT, + insertion[:len(insertion) - overlap_length2]) + diffs[pointer + 1] = (self.DIFF_DELETE, deletion[overlap_length2:]) + pointer += 1 + pointer += 1 + pointer += 1 + + def diff_cleanupSemanticLossless(self, diffs): + """Look for single edits surrounded on both sides by equalities + which can be shifted sideways to align the edit to a word boundary. + e.g: The cat came. -> The cat came. + + Args: + diffs: Array of diff tuples. + """ + + def diff_cleanupSemanticScore(one, two): + """Given two strings, compute a score representing whether the + internal boundary falls on logical boundaries. + Scores range from 6 (best) to 0 (worst). + Closure, but does not reference any external variables. + + Args: + one: First string. + two: Second string. + + Returns: + The score. + """ + if not one or not two: + # Edges are the best. + return 6 + + # Each port of this function behaves slightly differently due to + # subtle differences in each language's definition of things like + # 'whitespace'. Since this function's purpose is largely cosmetic, + # the choice has been made to use each language's native features + # rather than force total conformity. + char1 = one[-1] + char2 = two[0] + nonAlphaNumeric1 = not char1.isalnum() + nonAlphaNumeric2 = not char2.isalnum() + whitespace1 = nonAlphaNumeric1 and char1.isspace() + whitespace2 = nonAlphaNumeric2 and char2.isspace() + lineBreak1 = whitespace1 and (char1 == "\r" or char1 == "\n") + lineBreak2 = whitespace2 and (char2 == "\r" or char2 == "\n") + blankLine1 = lineBreak1 and self.BLANKLINEEND.search(one) + blankLine2 = lineBreak2 and self.BLANKLINESTART.match(two) + + if blankLine1 or blankLine2: + # Five points for blank lines. + return 5 + elif lineBreak1 or lineBreak2: + # Four points for line breaks. + return 4 + elif nonAlphaNumeric1 and not whitespace1 and whitespace2: + # Three points for end of sentences. + return 3 + elif whitespace1 or whitespace2: + # Two points for whitespace. + return 2 + elif nonAlphaNumeric1 or nonAlphaNumeric2: + # One point for non-alphanumeric. + return 1 + return 0 + + pointer = 1 + # Intentionally ignore the first and last element (don't need checking). + while pointer < len(diffs) - 1: + if (diffs[pointer - 1][0] == self.DIFF_EQUAL and + diffs[pointer + 1][0] == self.DIFF_EQUAL): + # This is a single edit surrounded by equalities. + equality1 = diffs[pointer - 1][1] + edit = diffs[pointer][1] + equality2 = diffs[pointer + 1][1] + + # First, shift the edit as far left as possible. + commonOffset = self.diff_commonSuffix(equality1, edit) + if commonOffset: + commonString = edit[-commonOffset:] + equality1 = equality1[:-commonOffset] + edit = commonString + edit[:-commonOffset] + equality2 = commonString + equality2 + + # Second, step character by character right, looking for the best fit. + bestEquality1 = equality1 + bestEdit = edit + bestEquality2 = equality2 + bestScore = (diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2)) + while edit and equality2 and edit[0] == equality2[0]: + equality1 += edit[0] + edit = edit[1:] + equality2[0] + equality2 = equality2[1:] + score = (diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2)) + # The >= encourages trailing rather than leading whitespace on edits. + if score >= bestScore: + bestScore = score + bestEquality1 = equality1 + bestEdit = edit + bestEquality2 = equality2 + + if diffs[pointer - 1][1] != bestEquality1: + # We have an improvement, save it back to the diff. + if bestEquality1: + diffs[pointer - 1] = (diffs[pointer - 1][0], bestEquality1) + else: + del diffs[pointer - 1] + pointer -= 1 + diffs[pointer] = (diffs[pointer][0], bestEdit) + if bestEquality2: + diffs[pointer + 1] = (diffs[pointer + 1][0], bestEquality2) + else: + del diffs[pointer + 1] + pointer -= 1 + pointer += 1 + + # Define some regex patterns for matching boundaries. + BLANKLINEEND = re.compile(r"\n\r?\n$"); + BLANKLINESTART = re.compile(r"^\r?\n\r?\n"); + + def diff_cleanupEfficiency(self, diffs): + """Reduce the number of edits by eliminating operationally trivial + equalities. + + Args: + diffs: Array of diff tuples. + """ + changes = False + equalities = [] # Stack of indices where equalities are found. + lastequality = None # Always equal to diffs[equalities[-1]][1] + pointer = 0 # Index of current position. + pre_ins = False # Is there an insertion operation before the last equality. + pre_del = False # Is there a deletion operation before the last equality. + post_ins = False # Is there an insertion operation after the last equality. + post_del = False # Is there a deletion operation after the last equality. + while pointer < len(diffs): + if diffs[pointer][0] == self.DIFF_EQUAL: # Equality found. + if (len(diffs[pointer][1]) < self.Diff_EditCost and + (post_ins or post_del)): + # Candidate found. + equalities.append(pointer) + pre_ins = post_ins + pre_del = post_del + lastequality = diffs[pointer][1] + else: + # Not a candidate, and can never become one. + equalities = [] + lastequality = None + + post_ins = post_del = False + else: # An insertion or deletion. + if diffs[pointer][0] == self.DIFF_DELETE: + post_del = True + else: + post_ins = True + + # Five types to be split: + # ABXYCD + # AXCD + # ABXC + # AXCD + # ABXC + + if lastequality and ((pre_ins and pre_del and post_ins and post_del) or + ((len(lastequality) < self.Diff_EditCost / 2) and + (pre_ins + pre_del + post_ins + post_del) == 3)): + # Duplicate record. + diffs.insert(equalities[-1], (self.DIFF_DELETE, lastequality)) + # Change second copy to insert. + diffs[equalities[-1] + 1] = (self.DIFF_INSERT, + diffs[equalities[-1] + 1][1]) + equalities.pop() # Throw away the equality we just deleted. + lastequality = None + if pre_ins and pre_del: + # No changes made which could affect previous entry, keep going. + post_ins = post_del = True + equalities = [] + else: + if len(equalities): + equalities.pop() # Throw away the previous equality. + if len(equalities): + pointer = equalities[-1] + else: + pointer = -1 + post_ins = post_del = False + changes = True + pointer += 1 + + if changes: + self.diff_cleanupMerge(diffs) + + def diff_cleanupMerge(self, diffs): + """Reorder and merge like edit sections. Merge equalities. + Any edit section can move as long as it doesn't cross an equality. + + Args: + diffs: Array of diff tuples. + """ + diffs.append((self.DIFF_EQUAL, '')) # Add a dummy entry at the end. + pointer = 0 + count_delete = 0 + count_insert = 0 + text_delete = '' + text_insert = '' + while pointer < len(diffs): + if diffs[pointer][0] == self.DIFF_INSERT: + count_insert += 1 + text_insert += diffs[pointer][1] + pointer += 1 + elif diffs[pointer][0] == self.DIFF_DELETE: + count_delete += 1 + text_delete += diffs[pointer][1] + pointer += 1 + elif diffs[pointer][0] == self.DIFF_EQUAL: + # Upon reaching an equality, check for prior redundancies. + if count_delete + count_insert > 1: + if count_delete != 0 and count_insert != 0: + # Factor out any common prefixies. + commonlength = self.diff_commonPrefix(text_insert, text_delete) + if commonlength != 0: + x = pointer - count_delete - count_insert - 1 + if x >= 0 and diffs[x][0] == self.DIFF_EQUAL: + diffs[x] = (diffs[x][0], diffs[x][1] + + text_insert[:commonlength]) + else: + diffs.insert(0, (self.DIFF_EQUAL, text_insert[:commonlength])) + pointer += 1 + text_insert = text_insert[commonlength:] + text_delete = text_delete[commonlength:] + # Factor out any common suffixies. + commonlength = self.diff_commonSuffix(text_insert, text_delete) + if commonlength != 0: + diffs[pointer] = (diffs[pointer][0], text_insert[-commonlength:] + + diffs[pointer][1]) + text_insert = text_insert[:-commonlength] + text_delete = text_delete[:-commonlength] + # Delete the offending records and add the merged ones. + if count_delete == 0: + diffs[pointer - count_insert : pointer] = [ + (self.DIFF_INSERT, text_insert)] + elif count_insert == 0: + diffs[pointer - count_delete : pointer] = [ + (self.DIFF_DELETE, text_delete)] + else: + diffs[pointer - count_delete - count_insert : pointer] = [ + (self.DIFF_DELETE, text_delete), + (self.DIFF_INSERT, text_insert)] + pointer = pointer - count_delete - count_insert + 1 + if count_delete != 0: + pointer += 1 + if count_insert != 0: + pointer += 1 + elif pointer != 0 and diffs[pointer - 1][0] == self.DIFF_EQUAL: + # Merge this equality with the previous one. + diffs[pointer - 1] = (diffs[pointer - 1][0], + diffs[pointer - 1][1] + diffs[pointer][1]) + del diffs[pointer] + else: + pointer += 1 + + count_insert = 0 + count_delete = 0 + text_delete = '' + text_insert = '' + + if diffs[-1][1] == '': + diffs.pop() # Remove the dummy entry at the end. + + # Second pass: look for single edits surrounded on both sides by equalities + # which can be shifted sideways to eliminate an equality. + # e.g: ABAC -> ABAC + changes = False + pointer = 1 + # Intentionally ignore the first and last element (don't need checking). + while pointer < len(diffs) - 1: + if (diffs[pointer - 1][0] == self.DIFF_EQUAL and + diffs[pointer + 1][0] == self.DIFF_EQUAL): + # This is a single edit surrounded by equalities. + if diffs[pointer][1].endswith(diffs[pointer - 1][1]): + # Shift the edit over the previous equality. + diffs[pointer] = (diffs[pointer][0], + diffs[pointer - 1][1] + + diffs[pointer][1][:-len(diffs[pointer - 1][1])]) + diffs[pointer + 1] = (diffs[pointer + 1][0], + diffs[pointer - 1][1] + diffs[pointer + 1][1]) + del diffs[pointer - 1] + changes = True + elif diffs[pointer][1].startswith(diffs[pointer + 1][1]): + # Shift the edit over the next equality. + diffs[pointer - 1] = (diffs[pointer - 1][0], + diffs[pointer - 1][1] + diffs[pointer + 1][1]) + diffs[pointer] = (diffs[pointer][0], + diffs[pointer][1][len(diffs[pointer + 1][1]):] + + diffs[pointer + 1][1]) + del diffs[pointer + 1] + changes = True + pointer += 1 + + # If shifts were made, the diff needs reordering and another shift sweep. + if changes: + self.diff_cleanupMerge(diffs) + + def diff_xIndex(self, diffs, loc): + """loc is a location in text1, compute and return the equivalent location + in text2. e.g. "The cat" vs "The big cat", 1->1, 5->8 + + Args: + diffs: Array of diff tuples. + loc: Location within text1. + + Returns: + Location within text2. + """ + chars1 = 0 + chars2 = 0 + last_chars1 = 0 + last_chars2 = 0 + for x in xrange(len(diffs)): + (op, text) = diffs[x] + if op != self.DIFF_INSERT: # Equality or deletion. + chars1 += len(text) + if op != self.DIFF_DELETE: # Equality or insertion. + chars2 += len(text) + if chars1 > loc: # Overshot the location. + break + last_chars1 = chars1 + last_chars2 = chars2 + + if len(diffs) != x and diffs[x][0] == self.DIFF_DELETE: + # The location was deleted. + return last_chars2 + # Add the remaining len(character). + return last_chars2 + (loc - last_chars1) + + def diff_prettyHtml(self, diffs): + """Convert a diff array into a pretty HTML report. + + Args: + diffs: Array of diff tuples. + + Returns: + HTML representation. + """ + html = [] + for (op, data) in diffs: + text = (data.replace("&", "&").replace("<", "<") + .replace(">", ">").replace("\n", "¶
    ")) + if op == self.DIFF_INSERT: + html.append("%s" % text) + elif op == self.DIFF_DELETE: + html.append("%s" % text) + elif op == self.DIFF_EQUAL: + html.append("%s" % text) + return "".join(html) + + def diff_text1(self, diffs): + """Compute and return the source text (all equalities and deletions). + + Args: + diffs: Array of diff tuples. + + Returns: + Source text. + """ + text = [] + for (op, data) in diffs: + if op != self.DIFF_INSERT: + text.append(data) + return "".join(text) + + def diff_text2(self, diffs): + """Compute and return the destination text (all equalities and insertions). + + Args: + diffs: Array of diff tuples. + + Returns: + Destination text. + """ + text = [] + for (op, data) in diffs: + if op != self.DIFF_DELETE: + text.append(data) + return "".join(text) + + def diff_levenshtein(self, diffs): + """Compute the Levenshtein distance; the number of inserted, deleted or + substituted characters. + + Args: + diffs: Array of diff tuples. + + Returns: + Number of changes. + """ + levenshtein = 0 + insertions = 0 + deletions = 0 + for (op, data) in diffs: + if op == self.DIFF_INSERT: + insertions += len(data) + elif op == self.DIFF_DELETE: + deletions += len(data) + elif op == self.DIFF_EQUAL: + # A deletion and an insertion is one substitution. + levenshtein += max(insertions, deletions) + insertions = 0 + deletions = 0 + levenshtein += max(insertions, deletions) + return levenshtein + + def diff_toDelta(self, diffs): + """Crush the diff into an encoded string which describes the operations + required to transform text1 into text2. + E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + Operations are tab-separated. Inserted text is escaped using %xx notation. + + Args: + diffs: Array of diff tuples. + + Returns: + Delta text. + """ + text = [] + for (op, data) in diffs: + if op == self.DIFF_INSERT: + # High ascii will raise UnicodeDecodeError. Use Unicode instead. + data = data.encode("utf-8") + text.append("+" + urllib.quote(data, "!~*'();/?:@&=+$,# ")) + elif op == self.DIFF_DELETE: + text.append("-%d" % len(data)) + elif op == self.DIFF_EQUAL: + text.append("=%d" % len(data)) + return "\t".join(text) + + def diff_fromDelta(self, text1, delta): + """Given the original text1, and an encoded string which describes the + operations required to transform text1 into text2, compute the full diff. + + Args: + text1: Source string for the diff. + delta: Delta text. + + Returns: + Array of diff tuples. + + Raises: + ValueError: If invalid input. + """ + if type(delta) == unicode: + # Deltas should be composed of a subset of ascii chars, Unicode not + # required. If this encode raises UnicodeEncodeError, delta is invalid. + delta = delta.encode("ascii") + diffs = [] + pointer = 0 # Cursor in text1 + tokens = delta.split("\t") + for token in tokens: + if token == "": + # Blank tokens are ok (from a trailing \t). + continue + # Each token begins with a one character parameter which specifies the + # operation of this token (delete, insert, equality). + param = token[1:] + if token[0] == "+": + param = urllib.unquote(param).decode("utf-8") + diffs.append((self.DIFF_INSERT, param)) + elif token[0] == "-" or token[0] == "=": + try: + n = int(param) + except ValueError: + raise ValueError("Invalid number in diff_fromDelta: " + param) + if n < 0: + raise ValueError("Negative number in diff_fromDelta: " + param) + text = text1[pointer : pointer + n] + pointer += n + if token[0] == "=": + diffs.append((self.DIFF_EQUAL, text)) + else: + diffs.append((self.DIFF_DELETE, text)) + else: + # Anything else is an error. + raise ValueError("Invalid diff operation in diff_fromDelta: " + + token[0]) + if pointer != len(text1): + raise ValueError( + "Delta length (%d) does not equal source text length (%d)." % + (pointer, len(text1))) + return diffs + + # MATCH FUNCTIONS + + def match_main(self, text, pattern, loc): + """Locate the best instance of 'pattern' in 'text' near 'loc'. + + Args: + text: The text to search. + pattern: The pattern to search for. + loc: The location to search around. + + Returns: + Best match index or -1. + """ + # Check for null inputs. + if text == None or pattern == None: + raise ValueError("Null inputs. (match_main)") + + loc = max(0, min(loc, len(text))) + if text == pattern: + # Shortcut (potentially not guaranteed by the algorithm) + return 0 + elif not text: + # Nothing to match. + return -1 + elif text[loc:loc + len(pattern)] == pattern: + # Perfect match at the perfect spot! (Includes case of null pattern) + return loc + else: + # Do a fuzzy compare. + match = self.match_bitap(text, pattern, loc) + return match + + def match_bitap(self, text, pattern, loc): + """Locate the best instance of 'pattern' in 'text' near 'loc' using the + Bitap algorithm. + + Args: + text: The text to search. + pattern: The pattern to search for. + loc: The location to search around. + + Returns: + Best match index or -1. + """ + # Python doesn't have a maxint limit, so ignore this check. + #if self.Match_MaxBits != 0 and len(pattern) > self.Match_MaxBits: + # raise ValueError("Pattern too long for this application.") + + # Initialise the alphabet. + s = self.match_alphabet(pattern) + + def match_bitapScore(e, x): + """Compute and return the score for a match with e errors and x location. + Accesses loc and pattern through being a closure. + + Args: + e: Number of errors in match. + x: Location of match. + + Returns: + Overall score for match (0.0 = good, 1.0 = bad). + """ + accuracy = float(e) / len(pattern) + proximity = abs(loc - x) + if not self.Match_Distance: + # Dodge divide by zero error. + return proximity and 1.0 or accuracy + return accuracy + (proximity / float(self.Match_Distance)) + + # Highest score beyond which we give up. + score_threshold = self.Match_Threshold + # Is there a nearby exact match? (speedup) + best_loc = text.find(pattern, loc) + if best_loc != -1: + score_threshold = min(match_bitapScore(0, best_loc), score_threshold) + # What about in the other direction? (speedup) + best_loc = text.rfind(pattern, loc + len(pattern)) + if best_loc != -1: + score_threshold = min(match_bitapScore(0, best_loc), score_threshold) + + # Initialise the bit arrays. + matchmask = 1 << (len(pattern) - 1) + best_loc = -1 + + bin_max = len(pattern) + len(text) + # Empty initialization added to appease pychecker. + last_rd = None + for d in xrange(len(pattern)): + # Scan for the best match each iteration allows for one more error. + # Run a binary search to determine how far from 'loc' we can stray at + # this error level. + bin_min = 0 + bin_mid = bin_max + while bin_min < bin_mid: + if match_bitapScore(d, loc + bin_mid) <= score_threshold: + bin_min = bin_mid + else: + bin_max = bin_mid + bin_mid = (bin_max - bin_min) // 2 + bin_min + + # Use the result from this iteration as the maximum for the next. + bin_max = bin_mid + start = max(1, loc - bin_mid + 1) + finish = min(loc + bin_mid, len(text)) + len(pattern) + + rd = [0] * (finish + 2) + rd[finish + 1] = (1 << d) - 1 + for j in xrange(finish, start - 1, -1): + if len(text) <= j - 1: + # Out of range. + charMatch = 0 + else: + charMatch = s.get(text[j - 1], 0) + if d == 0: # First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch + else: # Subsequent passes: fuzzy match. + rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) | ( + ((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1] + if rd[j] & matchmask: + score = match_bitapScore(d, j - 1) + # This match will almost certainly be better than any existing match. + # But check anyway. + if score <= score_threshold: + # Told you so. + score_threshold = score + best_loc = j - 1 + if best_loc > loc: + # When passing loc, don't exceed our current distance from loc. + start = max(1, 2 * loc - best_loc) + else: + # Already passed loc, downhill from here on in. + break + # No hope for a (better) match at greater error levels. + if match_bitapScore(d + 1, loc) > score_threshold: + break + last_rd = rd + return best_loc + + def match_alphabet(self, pattern): + """Initialise the alphabet for the Bitap algorithm. + + Args: + pattern: The text to encode. + + Returns: + Hash of character locations. + """ + s = {} + for char in pattern: + s[char] = 0 + for i in xrange(len(pattern)): + s[pattern[i]] |= 1 << (len(pattern) - i - 1) + return s + + # PATCH FUNCTIONS + + def patch_addContext(self, patch, text): + """Increase the context until it is unique, + but don't let the pattern expand beyond Match_MaxBits. + + Args: + patch: The patch to grow. + text: Source text. + """ + if len(text) == 0: + return + pattern = text[patch.start2 : patch.start2 + patch.length1] + padding = 0 + + # Look for the first and last matches of pattern in text. If two different + # matches are found, increase the pattern length. + while (text.find(pattern) != text.rfind(pattern) and (self.Match_MaxBits == + 0 or len(pattern) < self.Match_MaxBits - self.Patch_Margin - + self.Patch_Margin)): + padding += self.Patch_Margin + pattern = text[max(0, patch.start2 - padding) : + patch.start2 + patch.length1 + padding] + # Add one chunk for good luck. + padding += self.Patch_Margin + + # Add the prefix. + prefix = text[max(0, patch.start2 - padding) : patch.start2] + if prefix: + patch.diffs[:0] = [(self.DIFF_EQUAL, prefix)] + # Add the suffix. + suffix = text[patch.start2 + patch.length1 : + patch.start2 + patch.length1 + padding] + if suffix: + patch.diffs.append((self.DIFF_EQUAL, suffix)) + + # Roll back the start points. + patch.start1 -= len(prefix) + patch.start2 -= len(prefix) + # Extend lengths. + patch.length1 += len(prefix) + len(suffix) + patch.length2 += len(prefix) + len(suffix) + + def patch_make(self, a, b=None, c=None): + """Compute a list of patches to turn text1 into text2. + Use diffs if provided, otherwise compute it ourselves. + There are four ways to call this function, depending on what data is + available to the caller: + Method 1: + a = text1, b = text2 + Method 2: + a = diffs + Method 3 (optimal): + a = text1, b = diffs + Method 4 (deprecated, use method 3): + a = text1, b = text2, c = diffs + + Args: + a: text1 (methods 1,3,4) or Array of diff tuples for text1 to + text2 (method 2). + b: text2 (methods 1,4) or Array of diff tuples for text1 to + text2 (method 3) or undefined (method 2). + c: Array of diff tuples for text1 to text2 (method 4) or + undefined (methods 1,2,3). + + Returns: + Array of Patch objects. + """ + text1 = None + diffs = None + # Note that texts may arrive as 'str' or 'unicode'. + if isinstance(a, basestring) and isinstance(b, basestring) and c is None: + # Method 1: text1, text2 + # Compute diffs from text1 and text2. + text1 = a + diffs = self.diff_main(text1, b, True) + if len(diffs) > 2: + self.diff_cleanupSemantic(diffs) + self.diff_cleanupEfficiency(diffs) + elif isinstance(a, list) and b is None and c is None: + # Method 2: diffs + # Compute text1 from diffs. + diffs = a + text1 = self.diff_text1(diffs) + elif isinstance(a, basestring) and isinstance(b, list) and c is None: + # Method 3: text1, diffs + text1 = a + diffs = b + elif (isinstance(a, basestring) and isinstance(b, basestring) and + isinstance(c, list)): + # Method 4: text1, text2, diffs + # text2 is not used. + text1 = a + diffs = c + else: + raise ValueError("Unknown call format to patch_make.") + + if not diffs: + return [] # Get rid of the None case. + patches = [] + patch = patch_obj() + char_count1 = 0 # Number of characters into the text1 string. + char_count2 = 0 # Number of characters into the text2 string. + prepatch_text = text1 # Recreate the patches to determine context info. + postpatch_text = text1 + for x in xrange(len(diffs)): + (diff_type, diff_text) = diffs[x] + if len(patch.diffs) == 0 and diff_type != self.DIFF_EQUAL: + # A new patch starts here. + patch.start1 = char_count1 + patch.start2 = char_count2 + if diff_type == self.DIFF_INSERT: + # Insertion + patch.diffs.append(diffs[x]) + patch.length2 += len(diff_text) + postpatch_text = (postpatch_text[:char_count2] + diff_text + + postpatch_text[char_count2:]) + elif diff_type == self.DIFF_DELETE: + # Deletion. + patch.length1 += len(diff_text) + patch.diffs.append(diffs[x]) + postpatch_text = (postpatch_text[:char_count2] + + postpatch_text[char_count2 + len(diff_text):]) + elif (diff_type == self.DIFF_EQUAL and + len(diff_text) <= 2 * self.Patch_Margin and + len(patch.diffs) != 0 and len(diffs) != x + 1): + # Small equality inside a patch. + patch.diffs.append(diffs[x]) + patch.length1 += len(diff_text) + patch.length2 += len(diff_text) + + if (diff_type == self.DIFF_EQUAL and + len(diff_text) >= 2 * self.Patch_Margin): + # Time for a new patch. + if len(patch.diffs) != 0: + self.patch_addContext(patch, prepatch_text) + patches.append(patch) + patch = patch_obj() + # Unlike Unidiff, our patch lists have a rolling context. + # http://code.google.com/p/google-diff-match-patch/wiki/Unidiff + # Update prepatch text & pos to reflect the application of the + # just completed patch. + prepatch_text = postpatch_text + char_count1 = char_count2 + + # Update the current character count. + if diff_type != self.DIFF_INSERT: + char_count1 += len(diff_text) + if diff_type != self.DIFF_DELETE: + char_count2 += len(diff_text) + + # Pick up the leftover patch if not empty. + if len(patch.diffs) != 0: + self.patch_addContext(patch, prepatch_text) + patches.append(patch) + return patches + + def patch_deepCopy(self, patches): + """Given an array of patches, return another array that is identical. + + Args: + patches: Array of Patch objects. + + Returns: + Array of Patch objects. + """ + patchesCopy = [] + for patch in patches: + patchCopy = patch_obj() + # No need to deep copy the tuples since they are immutable. + patchCopy.diffs = patch.diffs[:] + patchCopy.start1 = patch.start1 + patchCopy.start2 = patch.start2 + patchCopy.length1 = patch.length1 + patchCopy.length2 = patch.length2 + patchesCopy.append(patchCopy) + return patchesCopy + + def patch_apply(self, patches, text): + """Merge a set of patches onto the text. Return a patched text, as well + as a list of true/false values indicating which patches were applied. + + Args: + patches: Array of Patch objects. + text: Old text. + + Returns: + Two element Array, containing the new text and an array of boolean values. + """ + if not patches: + return (text, []) + + # Deep copy the patches so that no changes are made to originals. + patches = self.patch_deepCopy(patches) + + nullPadding = self.patch_addPadding(patches) + text = nullPadding + text + nullPadding + self.patch_splitMax(patches) + + # delta keeps track of the offset between the expected and actual location + # of the previous patch. If there are patches expected at positions 10 and + # 20, but the first patch was found at 12, delta is 2 and the second patch + # has an effective expected position of 22. + delta = 0 + results = [] + for patch in patches: + expected_loc = patch.start2 + delta + text1 = self.diff_text1(patch.diffs) + end_loc = -1 + if len(text1) > self.Match_MaxBits: + # patch_splitMax will only provide an oversized pattern in the case of + # a monster delete. + start_loc = self.match_main(text, text1[:self.Match_MaxBits], + expected_loc) + if start_loc != -1: + end_loc = self.match_main(text, text1[-self.Match_MaxBits:], + expected_loc + len(text1) - self.Match_MaxBits) + if end_loc == -1 or start_loc >= end_loc: + # Can't find valid trailing context. Drop this patch. + start_loc = -1 + else: + start_loc = self.match_main(text, text1, expected_loc) + if start_loc == -1: + # No match found. :( + results.append(False) + # Subtract the delta for this failed patch from subsequent patches. + delta -= patch.length2 - patch.length1 + else: + # Found a match. :) + results.append(True) + delta = start_loc - expected_loc + if end_loc == -1: + text2 = text[start_loc : start_loc + len(text1)] + else: + text2 = text[start_loc : end_loc + self.Match_MaxBits] + if text1 == text2: + # Perfect match, just shove the replacement text in. + text = (text[:start_loc] + self.diff_text2(patch.diffs) + + text[start_loc + len(text1):]) + else: + # Imperfect match. + # Run a diff to get a framework of equivalent indices. + diffs = self.diff_main(text1, text2, False) + if (len(text1) > self.Match_MaxBits and + self.diff_levenshtein(diffs) / float(len(text1)) > + self.Patch_DeleteThreshold): + # The end points match, but the content is unacceptably bad. + results[-1] = False + else: + self.diff_cleanupSemanticLossless(diffs) + index1 = 0 + for (op, data) in patch.diffs: + if op != self.DIFF_EQUAL: + index2 = self.diff_xIndex(diffs, index1) + if op == self.DIFF_INSERT: # Insertion + text = text[:start_loc + index2] + data + text[start_loc + + index2:] + elif op == self.DIFF_DELETE: # Deletion + text = text[:start_loc + index2] + text[start_loc + + self.diff_xIndex(diffs, index1 + len(data)):] + if op != self.DIFF_DELETE: + index1 += len(data) + # Strip the padding off. + text = text[len(nullPadding):-len(nullPadding)] + return (text, results) + + def patch_addPadding(self, patches): + """Add some padding on text start and end so that edges can match + something. Intended to be called only from within patch_apply. + + Args: + patches: Array of Patch objects. + + Returns: + The padding string added to each side. + """ + paddingLength = self.Patch_Margin + nullPadding = "" + for x in xrange(1, paddingLength + 1): + nullPadding += chr(x) + + # Bump all the patches forward. + for patch in patches: + patch.start1 += paddingLength + patch.start2 += paddingLength + + # Add some padding on start of first diff. + patch = patches[0] + diffs = patch.diffs + if not diffs or diffs[0][0] != self.DIFF_EQUAL: + # Add nullPadding equality. + diffs.insert(0, (self.DIFF_EQUAL, nullPadding)) + patch.start1 -= paddingLength # Should be 0. + patch.start2 -= paddingLength # Should be 0. + patch.length1 += paddingLength + patch.length2 += paddingLength + elif paddingLength > len(diffs[0][1]): + # Grow first equality. + extraLength = paddingLength - len(diffs[0][1]) + newText = nullPadding[len(diffs[0][1]):] + diffs[0][1] + diffs[0] = (diffs[0][0], newText) + patch.start1 -= extraLength + patch.start2 -= extraLength + patch.length1 += extraLength + patch.length2 += extraLength + + # Add some padding on end of last diff. + patch = patches[-1] + diffs = patch.diffs + if not diffs or diffs[-1][0] != self.DIFF_EQUAL: + # Add nullPadding equality. + diffs.append((self.DIFF_EQUAL, nullPadding)) + patch.length1 += paddingLength + patch.length2 += paddingLength + elif paddingLength > len(diffs[-1][1]): + # Grow last equality. + extraLength = paddingLength - len(diffs[-1][1]) + newText = diffs[-1][1] + nullPadding[:extraLength] + diffs[-1] = (diffs[-1][0], newText) + patch.length1 += extraLength + patch.length2 += extraLength + + return nullPadding + + def patch_splitMax(self, patches): + """Look through the patches and break up any which are longer than the + maximum limit of the match algorithm. + Intended to be called only from within patch_apply. + + Args: + patches: Array of Patch objects. + """ + patch_size = self.Match_MaxBits + if patch_size == 0: + # Python has the option of not splitting strings due to its ability + # to handle integers of arbitrary precision. + return + for x in xrange(len(patches)): + if patches[x].length1 <= patch_size: + continue + bigpatch = patches[x] + # Remove the big old patch. + del patches[x] + x -= 1 + start1 = bigpatch.start1 + start2 = bigpatch.start2 + precontext = '' + while len(bigpatch.diffs) != 0: + # Create one of several smaller patches. + patch = patch_obj() + empty = True + patch.start1 = start1 - len(precontext) + patch.start2 = start2 - len(precontext) + if precontext: + patch.length1 = patch.length2 = len(precontext) + patch.diffs.append((self.DIFF_EQUAL, precontext)) + + while (len(bigpatch.diffs) != 0 and + patch.length1 < patch_size - self.Patch_Margin): + (diff_type, diff_text) = bigpatch.diffs[0] + if diff_type == self.DIFF_INSERT: + # Insertions are harmless. + patch.length2 += len(diff_text) + start2 += len(diff_text) + patch.diffs.append(bigpatch.diffs.pop(0)) + empty = False + elif (diff_type == self.DIFF_DELETE and len(patch.diffs) == 1 and + patch.diffs[0][0] == self.DIFF_EQUAL and + len(diff_text) > 2 * patch_size): + # This is a large deletion. Let it pass in one chunk. + patch.length1 += len(diff_text) + start1 += len(diff_text) + empty = False + patch.diffs.append((diff_type, diff_text)) + del bigpatch.diffs[0] + else: + # Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text[:patch_size - patch.length1 - + self.Patch_Margin] + patch.length1 += len(diff_text) + start1 += len(diff_text) + if diff_type == self.DIFF_EQUAL: + patch.length2 += len(diff_text) + start2 += len(diff_text) + else: + empty = False + + patch.diffs.append((diff_type, diff_text)) + if diff_text == bigpatch.diffs[0][1]: + del bigpatch.diffs[0] + else: + bigpatch.diffs[0] = (bigpatch.diffs[0][0], + bigpatch.diffs[0][1][len(diff_text):]) + + # Compute the head context for the next patch. + precontext = self.diff_text2(patch.diffs) + precontext = precontext[-self.Patch_Margin:] + # Append the end context for this patch. + postcontext = self.diff_text1(bigpatch.diffs)[:self.Patch_Margin] + if postcontext: + patch.length1 += len(postcontext) + patch.length2 += len(postcontext) + if len(patch.diffs) != 0 and patch.diffs[-1][0] == self.DIFF_EQUAL: + patch.diffs[-1] = (self.DIFF_EQUAL, patch.diffs[-1][1] + + postcontext) + else: + patch.diffs.append((self.DIFF_EQUAL, postcontext)) + + if not empty: + x += 1 + patches.insert(x, patch) + + def patch_toText(self, patches): + """Take a list of patches and return a textual representation. + + Args: + patches: Array of Patch objects. + + Returns: + Text representation of patches. + """ + text = [] + for patch in patches: + text.append(str(patch)) + return "".join(text) + + def patch_fromText(self, textline): + """Parse a textual representation of patches and return a list of patch + objects. + + Args: + textline: Text representation of patches. + + Returns: + Array of Patch objects. + + Raises: + ValueError: If invalid input. + """ + if type(textline) == unicode: + # Patches should be composed of a subset of ascii chars, Unicode not + # required. If this encode raises UnicodeEncodeError, patch is invalid. + textline = textline.encode("ascii") + patches = [] + if not textline: + return patches + text = textline.split('\n') + while len(text) != 0: + m = re.match("^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$", text[0]) + if not m: + raise ValueError("Invalid patch string: " + text[0]) + patch = patch_obj() + patches.append(patch) + patch.start1 = int(m.group(1)) + if m.group(2) == '': + patch.start1 -= 1 + patch.length1 = 1 + elif m.group(2) == '0': + patch.length1 = 0 + else: + patch.start1 -= 1 + patch.length1 = int(m.group(2)) + + patch.start2 = int(m.group(3)) + if m.group(4) == '': + patch.start2 -= 1 + patch.length2 = 1 + elif m.group(4) == '0': + patch.length2 = 0 + else: + patch.start2 -= 1 + patch.length2 = int(m.group(4)) + + del text[0] + + while len(text) != 0: + if text[0]: + sign = text[0][0] + else: + sign = '' + line = urllib.unquote(text[0][1:]) + line = line.decode("utf-8") + if sign == '+': + # Insertion. + patch.diffs.append((self.DIFF_INSERT, line)) + elif sign == '-': + # Deletion. + patch.diffs.append((self.DIFF_DELETE, line)) + elif sign == ' ': + # Minor equality. + patch.diffs.append((self.DIFF_EQUAL, line)) + elif sign == '@': + # Start of next patch. + break + elif sign == '': + # Blank line? Whatever. + pass + else: + # WTF? + raise ValueError("Invalid patch mode: '%s'\n%s" % (sign, line)) + del text[0] + return patches + + +class patch_obj: + """Class representing one patch operation. + """ + + def __init__(self): + """Initializes with an empty list of diffs. + """ + self.diffs = [] + self.start1 = None + self.start2 = None + self.length1 = 0 + self.length2 = 0 + + def __str__(self): + """Emmulate GNU diff's format. + Header: @@ -382,8 +481,9 @@ + Indicies are printed as 1-based, not 0-based. + + Returns: + The GNU diff string. + """ + if self.length1 == 0: + coords1 = str(self.start1) + ",0" + elif self.length1 == 1: + coords1 = str(self.start1 + 1) + else: + coords1 = str(self.start1 + 1) + "," + str(self.length1) + if self.length2 == 0: + coords2 = str(self.start2) + ",0" + elif self.length2 == 1: + coords2 = str(self.start2 + 1) + else: + coords2 = str(self.start2 + 1) + "," + str(self.length2) + text = ["@@ -", coords1, " +", coords2, " @@\n"] + # Escape the body of the patch with %xx notation. + for (op, data) in self.diffs: + if op == diff_match_patch.DIFF_INSERT: + text.append("+") + elif op == diff_match_patch.DIFF_DELETE: + text.append("-") + elif op == diff_match_patch.DIFF_EQUAL: + text.append(" ") + # High ascii will raise UnicodeDecodeError. Use Unicode instead. + data = data.encode("utf-8") + text.append(urllib.quote(data, "!~*'();/?:@&=+$,# ") + "\n") + return "".join(text) diff --git a/python2/diff_match_patch_test.py b/python2/diff_match_patch_test.py new file mode 100644 index 0000000..a034272 --- /dev/null +++ b/python2/diff_match_patch_test.py @@ -0,0 +1,868 @@ +#!/usr/bin/python2.4 + +"""Diff Match and Patch -- Test harness +Copyright 2018 The diff-match-patch Authors. +https://github.com/google/diff-match-patch + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import sys +import time +import unittest +import diff_match_patch as dmp_module +# Force a module reload. Allows one to edit the DMP module and rerun the tests +# without leaving the Python interpreter. +reload(dmp_module) + +class DiffMatchPatchTest(unittest.TestCase): + + def setUp(self): + "Test harness for dmp_module." + self.dmp = dmp_module.diff_match_patch() + + def diff_rebuildtexts(self, diffs): + # Construct the two texts which made up the diff originally. + text1 = "" + text2 = "" + for x in range(0, len(diffs)): + if diffs[x][0] != dmp_module.diff_match_patch.DIFF_INSERT: + text1 += diffs[x][1] + if diffs[x][0] != dmp_module.diff_match_patch.DIFF_DELETE: + text2 += diffs[x][1] + return (text1, text2) + + +class DiffTest(DiffMatchPatchTest): + """DIFF TEST FUNCTIONS""" + + def testDiffCommonPrefix(self): + # Detect any common prefix. + # Null case. + self.assertEquals(0, self.dmp.diff_commonPrefix("abc", "xyz")) + + # Non-null case. + self.assertEquals(4, self.dmp.diff_commonPrefix("1234abcdef", "1234xyz")) + + # Whole case. + self.assertEquals(4, self.dmp.diff_commonPrefix("1234", "1234xyz")) + + def testDiffCommonSuffix(self): + # Detect any common suffix. + # Null case. + self.assertEquals(0, self.dmp.diff_commonSuffix("abc", "xyz")) + + # Non-null case. + self.assertEquals(4, self.dmp.diff_commonSuffix("abcdef1234", "xyz1234")) + + # Whole case. + self.assertEquals(4, self.dmp.diff_commonSuffix("1234", "xyz1234")) + + def testDiffCommonOverlap(self): + # Null case. + self.assertEquals(0, self.dmp.diff_commonOverlap("", "abcd")) + + # Whole case. + self.assertEquals(3, self.dmp.diff_commonOverlap("abc", "abcd")) + + # No overlap. + self.assertEquals(0, self.dmp.diff_commonOverlap("123456", "abcd")) + + # Overlap. + self.assertEquals(3, self.dmp.diff_commonOverlap("123456xxx", "xxxabcd")) + + # Unicode. + # Some overly clever languages (C#) may treat ligatures as equal to their + # component letters. E.g. U+FB01 == 'fi' + self.assertEquals(0, self.dmp.diff_commonOverlap("fi", u"\ufb01i")) + + def testDiffHalfMatch(self): + # Detect a halfmatch. + self.dmp.Diff_Timeout = 1 + # No match. + self.assertEquals(None, self.dmp.diff_halfMatch("1234567890", "abcdef")) + + self.assertEquals(None, self.dmp.diff_halfMatch("12345", "23")) + + # Single Match. + self.assertEquals(("12", "90", "a", "z", "345678"), self.dmp.diff_halfMatch("1234567890", "a345678z")) + + self.assertEquals(("a", "z", "12", "90", "345678"), self.dmp.diff_halfMatch("a345678z", "1234567890")) + + self.assertEquals(("abc", "z", "1234", "0", "56789"), self.dmp.diff_halfMatch("abc56789z", "1234567890")) + + self.assertEquals(("a", "xyz", "1", "7890", "23456"), self.dmp.diff_halfMatch("a23456xyz", "1234567890")) + + # Multiple Matches. + self.assertEquals(("12123", "123121", "a", "z", "1234123451234"), self.dmp.diff_halfMatch("121231234123451234123121", "a1234123451234z")) + + self.assertEquals(("", "-=-=-=-=-=", "x", "", "x-=-=-=-=-=-=-="), self.dmp.diff_halfMatch("x-=-=-=-=-=-=-=-=-=-=-=-=", "xx-=-=-=-=-=-=-=")) + + self.assertEquals(("-=-=-=-=-=", "", "", "y", "-=-=-=-=-=-=-=y"), self.dmp.diff_halfMatch("-=-=-=-=-=-=-=-=-=-=-=-=y", "-=-=-=-=-=-=-=yy")) + + # Non-optimal halfmatch. + # Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy + self.assertEquals(("qHillo", "w", "x", "Hulloy", "HelloHe"), self.dmp.diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy")) + + # Optimal no halfmatch. + self.dmp.Diff_Timeout = 0 + self.assertEquals(None, self.dmp.diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy")) + + def testDiffLinesToChars(self): + # Convert lines down to characters. + self.assertEquals(("\x01\x02\x01", "\x02\x01\x02", ["", "alpha\n", "beta\n"]), self.dmp.diff_linesToChars("alpha\nbeta\nalpha\n", "beta\nalpha\nbeta\n")) + + self.assertEquals(("", "\x01\x02\x03\x03", ["", "alpha\r\n", "beta\r\n", "\r\n"]), self.dmp.diff_linesToChars("", "alpha\r\nbeta\r\n\r\n\r\n")) + + self.assertEquals(("\x01", "\x02", ["", "a", "b"]), self.dmp.diff_linesToChars("a", "b")) + + # More than 256 to reveal any 8-bit limitations. + n = 300 + lineList = [] + charList = [] + for x in range(1, n + 1): + lineList.append(str(x) + "\n") + charList.append(unichr(x)) + self.assertEquals(n, len(lineList)) + lines = "".join(lineList) + chars = "".join(charList) + self.assertEquals(n, len(chars)) + lineList.insert(0, "") + self.assertEquals((chars, "", lineList), self.dmp.diff_linesToChars(lines, "")) + + def testDiffCharsToLines(self): + # Convert chars up to lines. + diffs = [(self.dmp.DIFF_EQUAL, "\x01\x02\x01"), (self.dmp.DIFF_INSERT, "\x02\x01\x02")] + self.dmp.diff_charsToLines(diffs, ["", "alpha\n", "beta\n"]) + self.assertEquals([(self.dmp.DIFF_EQUAL, "alpha\nbeta\nalpha\n"), (self.dmp.DIFF_INSERT, "beta\nalpha\nbeta\n")], diffs) + + # More than 256 to reveal any 8-bit limitations. + n = 300 + lineList = [] + charList = [] + for x in range(1, n + 1): + lineList.append(str(x) + "\n") + charList.append(unichr(x)) + self.assertEquals(n, len(lineList)) + lines = "".join(lineList) + chars = "".join(charList) + self.assertEquals(n, len(chars)) + lineList.insert(0, "") + diffs = [(self.dmp.DIFF_DELETE, chars)] + self.dmp.diff_charsToLines(diffs, lineList) + self.assertEquals([(self.dmp.DIFF_DELETE, lines)], diffs) + + def testDiffCleanupMerge(self): + # Cleanup a messy diff. + # Null case. + diffs = [] + self.dmp.diff_cleanupMerge(diffs) + self.assertEquals([], diffs) + + # No change case. + diffs = [(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "b"), (self.dmp.DIFF_INSERT, "c")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEquals([(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "b"), (self.dmp.DIFF_INSERT, "c")], diffs) + + # Merge equalities. + diffs = [(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_EQUAL, "b"), (self.dmp.DIFF_EQUAL, "c")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEquals([(self.dmp.DIFF_EQUAL, "abc")], diffs) + + # Merge deletions. + diffs = [(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_DELETE, "b"), (self.dmp.DIFF_DELETE, "c")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "abc")], diffs) + + # Merge insertions. + diffs = [(self.dmp.DIFF_INSERT, "a"), (self.dmp.DIFF_INSERT, "b"), (self.dmp.DIFF_INSERT, "c")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEquals([(self.dmp.DIFF_INSERT, "abc")], diffs) + + # Merge interweave. + diffs = [(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_INSERT, "b"), (self.dmp.DIFF_DELETE, "c"), (self.dmp.DIFF_INSERT, "d"), (self.dmp.DIFF_EQUAL, "e"), (self.dmp.DIFF_EQUAL, "f")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "ac"), (self.dmp.DIFF_INSERT, "bd"), (self.dmp.DIFF_EQUAL, "ef")], diffs) + + # Prefix and suffix detection. + diffs = [(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_INSERT, "abc"), (self.dmp.DIFF_DELETE, "dc")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEquals([(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "d"), (self.dmp.DIFF_INSERT, "b"), (self.dmp.DIFF_EQUAL, "c")], diffs) + + # Prefix and suffix detection with equalities. + diffs = [(self.dmp.DIFF_EQUAL, "x"), (self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_INSERT, "abc"), (self.dmp.DIFF_DELETE, "dc"), (self.dmp.DIFF_EQUAL, "y")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEquals([(self.dmp.DIFF_EQUAL, "xa"), (self.dmp.DIFF_DELETE, "d"), (self.dmp.DIFF_INSERT, "b"), (self.dmp.DIFF_EQUAL, "cy")], diffs) + + # Slide edit left. + diffs = [(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_INSERT, "ba"), (self.dmp.DIFF_EQUAL, "c")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEquals([(self.dmp.DIFF_INSERT, "ab"), (self.dmp.DIFF_EQUAL, "ac")], diffs) + + # Slide edit right. + diffs = [(self.dmp.DIFF_EQUAL, "c"), (self.dmp.DIFF_INSERT, "ab"), (self.dmp.DIFF_EQUAL, "a")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEquals([(self.dmp.DIFF_EQUAL, "ca"), (self.dmp.DIFF_INSERT, "ba")], diffs) + + # Slide edit left recursive. + diffs = [(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "b"), (self.dmp.DIFF_EQUAL, "c"), (self.dmp.DIFF_DELETE, "ac"), (self.dmp.DIFF_EQUAL, "x")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_EQUAL, "acx")], diffs) + + # Slide edit right recursive. + diffs = [(self.dmp.DIFF_EQUAL, "x"), (self.dmp.DIFF_DELETE, "ca"), (self.dmp.DIFF_EQUAL, "c"), (self.dmp.DIFF_DELETE, "b"), (self.dmp.DIFF_EQUAL, "a")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEquals([(self.dmp.DIFF_EQUAL, "xca"), (self.dmp.DIFF_DELETE, "cba")], diffs) + + def testDiffCleanupSemanticLossless(self): + # Slide diffs to match logical boundaries. + # Null case. + diffs = [] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEquals([], diffs) + + # Blank lines. + diffs = [(self.dmp.DIFF_EQUAL, "AAA\r\n\r\nBBB"), (self.dmp.DIFF_INSERT, "\r\nDDD\r\n\r\nBBB"), (self.dmp.DIFF_EQUAL, "\r\nEEE")] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEquals([(self.dmp.DIFF_EQUAL, "AAA\r\n\r\n"), (self.dmp.DIFF_INSERT, "BBB\r\nDDD\r\n\r\n"), (self.dmp.DIFF_EQUAL, "BBB\r\nEEE")], diffs) + + # Line boundaries. + diffs = [(self.dmp.DIFF_EQUAL, "AAA\r\nBBB"), (self.dmp.DIFF_INSERT, " DDD\r\nBBB"), (self.dmp.DIFF_EQUAL, " EEE")] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEquals([(self.dmp.DIFF_EQUAL, "AAA\r\n"), (self.dmp.DIFF_INSERT, "BBB DDD\r\n"), (self.dmp.DIFF_EQUAL, "BBB EEE")], diffs) + + # Word boundaries. + diffs = [(self.dmp.DIFF_EQUAL, "The c"), (self.dmp.DIFF_INSERT, "ow and the c"), (self.dmp.DIFF_EQUAL, "at.")] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEquals([(self.dmp.DIFF_EQUAL, "The "), (self.dmp.DIFF_INSERT, "cow and the "), (self.dmp.DIFF_EQUAL, "cat.")], diffs) + + # Alphanumeric boundaries. + diffs = [(self.dmp.DIFF_EQUAL, "The-c"), (self.dmp.DIFF_INSERT, "ow-and-the-c"), (self.dmp.DIFF_EQUAL, "at.")] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEquals([(self.dmp.DIFF_EQUAL, "The-"), (self.dmp.DIFF_INSERT, "cow-and-the-"), (self.dmp.DIFF_EQUAL, "cat.")], diffs) + + # Hitting the start. + diffs = [(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_EQUAL, "ax")] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_EQUAL, "aax")], diffs) + + # Hitting the end. + diffs = [(self.dmp.DIFF_EQUAL, "xa"), (self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_EQUAL, "a")] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEquals([(self.dmp.DIFF_EQUAL, "xaa"), (self.dmp.DIFF_DELETE, "a")], diffs) + + # Sentence boundaries. + diffs = [(self.dmp.DIFF_EQUAL, "The xxx. The "), (self.dmp.DIFF_INSERT, "zzz. The "), (self.dmp.DIFF_EQUAL, "yyy.")] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEquals([(self.dmp.DIFF_EQUAL, "The xxx."), (self.dmp.DIFF_INSERT, " The zzz."), (self.dmp.DIFF_EQUAL, " The yyy.")], diffs) + + def testDiffCleanupSemantic(self): + # Cleanup semantically trivial equalities. + # Null case. + diffs = [] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEquals([], diffs) + + # No elimination #1. + diffs = [(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_INSERT, "cd"), (self.dmp.DIFF_EQUAL, "12"), (self.dmp.DIFF_DELETE, "e")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_INSERT, "cd"), (self.dmp.DIFF_EQUAL, "12"), (self.dmp.DIFF_DELETE, "e")], diffs) + + # No elimination #2. + diffs = [(self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_INSERT, "ABC"), (self.dmp.DIFF_EQUAL, "1234"), (self.dmp.DIFF_DELETE, "wxyz")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_INSERT, "ABC"), (self.dmp.DIFF_EQUAL, "1234"), (self.dmp.DIFF_DELETE, "wxyz")], diffs) + + # Simple elimination. + diffs = [(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_EQUAL, "b"), (self.dmp.DIFF_DELETE, "c")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_INSERT, "b")], diffs) + + # Backpass elimination. + diffs = [(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_EQUAL, "cd"), (self.dmp.DIFF_DELETE, "e"), (self.dmp.DIFF_EQUAL, "f"), (self.dmp.DIFF_INSERT, "g")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "abcdef"), (self.dmp.DIFF_INSERT, "cdfg")], diffs) + + # Multiple eliminations. + diffs = [(self.dmp.DIFF_INSERT, "1"), (self.dmp.DIFF_EQUAL, "A"), (self.dmp.DIFF_DELETE, "B"), (self.dmp.DIFF_INSERT, "2"), (self.dmp.DIFF_EQUAL, "_"), (self.dmp.DIFF_INSERT, "1"), (self.dmp.DIFF_EQUAL, "A"), (self.dmp.DIFF_DELETE, "B"), (self.dmp.DIFF_INSERT, "2")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "AB_AB"), (self.dmp.DIFF_INSERT, "1A2_1A2")], diffs) + + # Word boundaries. + diffs = [(self.dmp.DIFF_EQUAL, "The c"), (self.dmp.DIFF_DELETE, "ow and the c"), (self.dmp.DIFF_EQUAL, "at.")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEquals([(self.dmp.DIFF_EQUAL, "The "), (self.dmp.DIFF_DELETE, "cow and the "), (self.dmp.DIFF_EQUAL, "cat.")], diffs) + + # No overlap elimination. + diffs = [(self.dmp.DIFF_DELETE, "abcxx"), (self.dmp.DIFF_INSERT, "xxdef")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "abcxx"), (self.dmp.DIFF_INSERT, "xxdef")], diffs) + + # Overlap elimination. + diffs = [(self.dmp.DIFF_DELETE, "abcxxx"), (self.dmp.DIFF_INSERT, "xxxdef")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_EQUAL, "xxx"), (self.dmp.DIFF_INSERT, "def")], diffs) + + # Reverse overlap elimination. + diffs = [(self.dmp.DIFF_DELETE, "xxxabc"), (self.dmp.DIFF_INSERT, "defxxx")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEquals([(self.dmp.DIFF_INSERT, "def"), (self.dmp.DIFF_EQUAL, "xxx"), (self.dmp.DIFF_DELETE, "abc")], diffs) + + # Two overlap eliminations. + diffs = [(self.dmp.DIFF_DELETE, "abcd1212"), (self.dmp.DIFF_INSERT, "1212efghi"), (self.dmp.DIFF_EQUAL, "----"), (self.dmp.DIFF_DELETE, "A3"), (self.dmp.DIFF_INSERT, "3BC")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "abcd"), (self.dmp.DIFF_EQUAL, "1212"), (self.dmp.DIFF_INSERT, "efghi"), (self.dmp.DIFF_EQUAL, "----"), (self.dmp.DIFF_DELETE, "A"), (self.dmp.DIFF_EQUAL, "3"), (self.dmp.DIFF_INSERT, "BC")], diffs) + + def testDiffCleanupEfficiency(self): + # Cleanup operationally trivial equalities. + self.dmp.Diff_EditCost = 4 + # Null case. + diffs = [] + self.dmp.diff_cleanupEfficiency(diffs) + self.assertEquals([], diffs) + + # No elimination. + diffs = [(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_INSERT, "12"), (self.dmp.DIFF_EQUAL, "wxyz"), (self.dmp.DIFF_DELETE, "cd"), (self.dmp.DIFF_INSERT, "34")] + self.dmp.diff_cleanupEfficiency(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_INSERT, "12"), (self.dmp.DIFF_EQUAL, "wxyz"), (self.dmp.DIFF_DELETE, "cd"), (self.dmp.DIFF_INSERT, "34")], diffs) + + # Four-edit elimination. + diffs = [(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_INSERT, "12"), (self.dmp.DIFF_EQUAL, "xyz"), (self.dmp.DIFF_DELETE, "cd"), (self.dmp.DIFF_INSERT, "34")] + self.dmp.diff_cleanupEfficiency(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "abxyzcd"), (self.dmp.DIFF_INSERT, "12xyz34")], diffs) + + # Three-edit elimination. + diffs = [(self.dmp.DIFF_INSERT, "12"), (self.dmp.DIFF_EQUAL, "x"), (self.dmp.DIFF_DELETE, "cd"), (self.dmp.DIFF_INSERT, "34")] + self.dmp.diff_cleanupEfficiency(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "xcd"), (self.dmp.DIFF_INSERT, "12x34")], diffs) + + # Backpass elimination. + diffs = [(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_INSERT, "12"), (self.dmp.DIFF_EQUAL, "xy"), (self.dmp.DIFF_INSERT, "34"), (self.dmp.DIFF_EQUAL, "z"), (self.dmp.DIFF_DELETE, "cd"), (self.dmp.DIFF_INSERT, "56")] + self.dmp.diff_cleanupEfficiency(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "abxyzcd"), (self.dmp.DIFF_INSERT, "12xy34z56")], diffs) + + # High cost elimination. + self.dmp.Diff_EditCost = 5 + diffs = [(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_INSERT, "12"), (self.dmp.DIFF_EQUAL, "wxyz"), (self.dmp.DIFF_DELETE, "cd"), (self.dmp.DIFF_INSERT, "34")] + self.dmp.diff_cleanupEfficiency(diffs) + self.assertEquals([(self.dmp.DIFF_DELETE, "abwxyzcd"), (self.dmp.DIFF_INSERT, "12wxyz34")], diffs) + self.dmp.Diff_EditCost = 4 + + def testDiffPrettyHtml(self): + # Pretty print. + diffs = [(self.dmp.DIFF_EQUAL, "a\n"), (self.dmp.DIFF_DELETE, "b"), (self.dmp.DIFF_INSERT, "c&d")] + self.assertEquals("
    <B>b</B>c&d", self.dmp.diff_prettyHtml(diffs)) + + def testDiffText(self): + # Compute the source and destination texts. + diffs = [(self.dmp.DIFF_EQUAL, "jump"), (self.dmp.DIFF_DELETE, "s"), (self.dmp.DIFF_INSERT, "ed"), (self.dmp.DIFF_EQUAL, " over "), (self.dmp.DIFF_DELETE, "the"), (self.dmp.DIFF_INSERT, "a"), (self.dmp.DIFF_EQUAL, " lazy")] + self.assertEquals("jumps over the lazy", self.dmp.diff_text1(diffs)) + + self.assertEquals("jumped over a lazy", self.dmp.diff_text2(diffs)) + + def testDiffDelta(self): + # Convert a diff into delta string. + diffs = [(self.dmp.DIFF_EQUAL, "jump"), (self.dmp.DIFF_DELETE, "s"), (self.dmp.DIFF_INSERT, "ed"), (self.dmp.DIFF_EQUAL, " over "), (self.dmp.DIFF_DELETE, "the"), (self.dmp.DIFF_INSERT, "a"), (self.dmp.DIFF_EQUAL, " lazy"), (self.dmp.DIFF_INSERT, "old dog")] + text1 = self.dmp.diff_text1(diffs) + self.assertEquals("jumps over the lazy", text1) + + delta = self.dmp.diff_toDelta(diffs) + self.assertEquals("=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog", delta) + + # Convert delta string into a diff. + self.assertEquals(diffs, self.dmp.diff_fromDelta(text1, delta)) + + # Generates error (19 != 20). + try: + self.dmp.diff_fromDelta(text1 + "x", delta) + self.assertFalse(True) + except ValueError: + # Exception expected. + pass + + # Generates error (19 != 18). + try: + self.dmp.diff_fromDelta(text1[1:], delta) + self.assertFalse(True) + except ValueError: + # Exception expected. + pass + + # Generates error (%c3%xy invalid Unicode). + try: + self.dmp.diff_fromDelta("", "+%c3xy") + self.assertFalse(True) + except ValueError: + # Exception expected. + pass + + # Test deltas with special characters. + diffs = [(self.dmp.DIFF_EQUAL, u"\u0680 \x00 \t %"), (self.dmp.DIFF_DELETE, u"\u0681 \x01 \n ^"), (self.dmp.DIFF_INSERT, u"\u0682 \x02 \\ |")] + text1 = self.dmp.diff_text1(diffs) + self.assertEquals(u"\u0680 \x00 \t %\u0681 \x01 \n ^", text1) + + delta = self.dmp.diff_toDelta(diffs) + self.assertEquals("=7\t-7\t+%DA%82 %02 %5C %7C", delta) + + # Convert delta string into a diff. + self.assertEquals(diffs, self.dmp.diff_fromDelta(text1, delta)) + + # Verify pool of unchanged characters. + diffs = [(self.dmp.DIFF_INSERT, "A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ")] + text2 = self.dmp.diff_text2(diffs) + self.assertEquals("A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", text2) + + delta = self.dmp.diff_toDelta(diffs) + self.assertEquals("+A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", delta) + + # Convert delta string into a diff. + self.assertEquals(diffs, self.dmp.diff_fromDelta("", delta)) + + def testDiffXIndex(self): + # Translate a location in text1 to text2. + self.assertEquals(5, self.dmp.diff_xIndex([(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_INSERT, "1234"), (self.dmp.DIFF_EQUAL, "xyz")], 2)) + + # Translation on deletion. + self.assertEquals(1, self.dmp.diff_xIndex([(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "1234"), (self.dmp.DIFF_EQUAL, "xyz")], 3)) + + def testDiffLevenshtein(self): + # Levenshtein with trailing equality. + self.assertEquals(4, self.dmp.diff_levenshtein([(self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_INSERT, "1234"), (self.dmp.DIFF_EQUAL, "xyz")])) + # Levenshtein with leading equality. + self.assertEquals(4, self.dmp.diff_levenshtein([(self.dmp.DIFF_EQUAL, "xyz"), (self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_INSERT, "1234")])) + # Levenshtein with middle equality. + self.assertEquals(7, self.dmp.diff_levenshtein([(self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_EQUAL, "xyz"), (self.dmp.DIFF_INSERT, "1234")])) + + def testDiffBisect(self): + # Normal. + a = "cat" + b = "map" + # Since the resulting diff hasn't been normalized, it would be ok if + # the insertion and deletion pairs are swapped. + # If the order changes, tweak this test as required. + self.assertEquals([(self.dmp.DIFF_DELETE, "c"), (self.dmp.DIFF_INSERT, "m"), (self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "t"), (self.dmp.DIFF_INSERT, "p")], self.dmp.diff_bisect(a, b, sys.maxint)) + + # Timeout. + self.assertEquals([(self.dmp.DIFF_DELETE, "cat"), (self.dmp.DIFF_INSERT, "map")], self.dmp.diff_bisect(a, b, 0)) + + def testDiffMain(self): + # Perform a trivial diff. + # Null case. + self.assertEquals([], self.dmp.diff_main("", "", False)) + + # Equality. + self.assertEquals([(self.dmp.DIFF_EQUAL, "abc")], self.dmp.diff_main("abc", "abc", False)) + + # Simple insertion. + self.assertEquals([(self.dmp.DIFF_EQUAL, "ab"), (self.dmp.DIFF_INSERT, "123"), (self.dmp.DIFF_EQUAL, "c")], self.dmp.diff_main("abc", "ab123c", False)) + + # Simple deletion. + self.assertEquals([(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "123"), (self.dmp.DIFF_EQUAL, "bc")], self.dmp.diff_main("a123bc", "abc", False)) + + # Two insertions. + self.assertEquals([(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_INSERT, "123"), (self.dmp.DIFF_EQUAL, "b"), (self.dmp.DIFF_INSERT, "456"), (self.dmp.DIFF_EQUAL, "c")], self.dmp.diff_main("abc", "a123b456c", False)) + + # Two deletions. + self.assertEquals([(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "123"), (self.dmp.DIFF_EQUAL, "b"), (self.dmp.DIFF_DELETE, "456"), (self.dmp.DIFF_EQUAL, "c")], self.dmp.diff_main("a123b456c", "abc", False)) + + # Perform a real diff. + # Switch off the timeout. + self.dmp.Diff_Timeout = 0 + # Simple cases. + self.assertEquals([(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_INSERT, "b")], self.dmp.diff_main("a", "b", False)) + + self.assertEquals([(self.dmp.DIFF_DELETE, "Apple"), (self.dmp.DIFF_INSERT, "Banana"), (self.dmp.DIFF_EQUAL, "s are a"), (self.dmp.DIFF_INSERT, "lso"), (self.dmp.DIFF_EQUAL, " fruit.")], self.dmp.diff_main("Apples are a fruit.", "Bananas are also fruit.", False)) + + self.assertEquals([(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_INSERT, u"\u0680"), (self.dmp.DIFF_EQUAL, "x"), (self.dmp.DIFF_DELETE, "\t"), (self.dmp.DIFF_INSERT, "\x00")], self.dmp.diff_main("ax\t", u"\u0680x\x00", False)) + + # Overlaps. + self.assertEquals([(self.dmp.DIFF_DELETE, "1"), (self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "y"), (self.dmp.DIFF_EQUAL, "b"), (self.dmp.DIFF_DELETE, "2"), (self.dmp.DIFF_INSERT, "xab")], self.dmp.diff_main("1ayb2", "abxab", False)) + + self.assertEquals([(self.dmp.DIFF_INSERT, "xaxcx"), (self.dmp.DIFF_EQUAL, "abc"), (self.dmp.DIFF_DELETE, "y")], self.dmp.diff_main("abcy", "xaxcxabc", False)) + + self.assertEquals([(self.dmp.DIFF_DELETE, "ABCD"), (self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "="), (self.dmp.DIFF_INSERT, "-"), (self.dmp.DIFF_EQUAL, "bcd"), (self.dmp.DIFF_DELETE, "="), (self.dmp.DIFF_INSERT, "-"), (self.dmp.DIFF_EQUAL, "efghijklmnopqrs"), (self.dmp.DIFF_DELETE, "EFGHIJKLMNOefg")], self.dmp.diff_main("ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg", "a-bcd-efghijklmnopqrs", False)) + + # Large equality. + self.assertEquals([(self.dmp.DIFF_INSERT, " "), (self.dmp.DIFF_EQUAL,"a"), (self.dmp.DIFF_INSERT,"nd"), (self.dmp.DIFF_EQUAL," [[Pennsylvania]]"), (self.dmp.DIFF_DELETE," and [[New")], self.dmp.diff_main("a [[Pennsylvania]] and [[New", " and [[Pennsylvania]]", False)) + + # Timeout. + self.dmp.Diff_Timeout = 0.1 # 100ms + a = "`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n" + b = "I am the very model of a modern major general,\nI've information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n" + # Increase the text lengths by 1024 times to ensure a timeout. + for x in range(10): + a = a + a + b = b + b + startTime = time.time() + self.dmp.diff_main(a, b) + endTime = time.time() + # Test that we took at least the timeout period. + self.assertTrue(self.dmp.Diff_Timeout <= endTime - startTime) + # Test that we didn't take forever (be forgiving). + # Theoretically this test could fail very occasionally if the + # OS task swaps or locks up for a second at the wrong moment. + self.assertTrue(self.dmp.Diff_Timeout * 2 > endTime - startTime) + self.dmp.Diff_Timeout = 0 + + # Test the linemode speedup. + # Must be long to pass the 100 char cutoff. + # Simple line-mode. + a = "1234567890\n" * 13 + b = "abcdefghij\n" * 13 + self.assertEquals(self.dmp.diff_main(a, b, False), self.dmp.diff_main(a, b, True)) + + # Single line-mode. + a = "1234567890" * 13 + b = "abcdefghij" * 13 + self.assertEquals(self.dmp.diff_main(a, b, False), self.dmp.diff_main(a, b, True)) + + # Overlap line-mode. + a = "1234567890\n" * 13 + b = "abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n" + texts_linemode = self.diff_rebuildtexts(self.dmp.diff_main(a, b, True)) + texts_textmode = self.diff_rebuildtexts(self.dmp.diff_main(a, b, False)) + self.assertEquals(texts_textmode, texts_linemode) + + # Test null inputs. + try: + self.dmp.diff_main(None, None) + self.assertFalse(True) + except ValueError: + # Exception expected. + pass + + +class MatchTest(DiffMatchPatchTest): + """MATCH TEST FUNCTIONS""" + + def testMatchAlphabet(self): + # Initialise the bitmasks for Bitap. + self.assertEquals({"a":4, "b":2, "c":1}, self.dmp.match_alphabet("abc")) + + self.assertEquals({"a":37, "b":18, "c":8}, self.dmp.match_alphabet("abcaba")) + + def testMatchBitap(self): + self.dmp.Match_Distance = 100 + self.dmp.Match_Threshold = 0.5 + # Exact matches. + self.assertEquals(5, self.dmp.match_bitap("abcdefghijk", "fgh", 5)) + + self.assertEquals(5, self.dmp.match_bitap("abcdefghijk", "fgh", 0)) + + # Fuzzy matches. + self.assertEquals(4, self.dmp.match_bitap("abcdefghijk", "efxhi", 0)) + + self.assertEquals(2, self.dmp.match_bitap("abcdefghijk", "cdefxyhijk", 5)) + + self.assertEquals(-1, self.dmp.match_bitap("abcdefghijk", "bxy", 1)) + + # Overflow. + self.assertEquals(2, self.dmp.match_bitap("123456789xx0", "3456789x0", 2)) + + self.assertEquals(0, self.dmp.match_bitap("abcdef", "xxabc", 4)) + + self.assertEquals(3, self.dmp.match_bitap("abcdef", "defyy", 4)) + + self.assertEquals(0, self.dmp.match_bitap("abcdef", "xabcdefy", 0)) + + # Threshold test. + self.dmp.Match_Threshold = 0.4 + self.assertEquals(4, self.dmp.match_bitap("abcdefghijk", "efxyhi", 1)) + + self.dmp.Match_Threshold = 0.3 + self.assertEquals(-1, self.dmp.match_bitap("abcdefghijk", "efxyhi", 1)) + + self.dmp.Match_Threshold = 0.0 + self.assertEquals(1, self.dmp.match_bitap("abcdefghijk", "bcdef", 1)) + self.dmp.Match_Threshold = 0.5 + + # Multiple select. + self.assertEquals(0, self.dmp.match_bitap("abcdexyzabcde", "abccde", 3)) + + self.assertEquals(8, self.dmp.match_bitap("abcdexyzabcde", "abccde", 5)) + + # Distance test. + self.dmp.Match_Distance = 10 # Strict location. + self.assertEquals(-1, self.dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24)) + + self.assertEquals(0, self.dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdxxefg", 1)) + + self.dmp.Match_Distance = 1000 # Loose location. + self.assertEquals(0, self.dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24)) + + + def testMatchMain(self): + # Full match. + # Shortcut matches. + self.assertEquals(0, self.dmp.match_main("abcdef", "abcdef", 1000)) + + self.assertEquals(-1, self.dmp.match_main("", "abcdef", 1)) + + self.assertEquals(3, self.dmp.match_main("abcdef", "", 3)) + + self.assertEquals(3, self.dmp.match_main("abcdef", "de", 3)) + + self.assertEquals(3, self.dmp.match_main("abcdef", "defy", 4)) + + self.assertEquals(0, self.dmp.match_main("abcdef", "abcdefy", 0)) + + # Complex match. + self.dmp.Match_Threshold = 0.7 + self.assertEquals(4, self.dmp.match_main("I am the very model of a modern major general.", " that berry ", 5)) + self.dmp.Match_Threshold = 0.5 + + # Test null inputs. + try: + self.dmp.match_main(None, None, 0) + self.assertFalse(True) + except ValueError: + # Exception expected. + pass + + +class PatchTest(DiffMatchPatchTest): + """PATCH TEST FUNCTIONS""" + + def testPatchObj(self): + # Patch Object. + p = dmp_module.patch_obj() + p.start1 = 20 + p.start2 = 21 + p.length1 = 18 + p.length2 = 17 + p.diffs = [(self.dmp.DIFF_EQUAL, "jump"), (self.dmp.DIFF_DELETE, "s"), (self.dmp.DIFF_INSERT, "ed"), (self.dmp.DIFF_EQUAL, " over "), (self.dmp.DIFF_DELETE, "the"), (self.dmp.DIFF_INSERT, "a"), (self.dmp.DIFF_EQUAL, "\nlaz")] + strp = str(p) + self.assertEquals("@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n", strp) + + def testPatchFromText(self): + self.assertEquals([], self.dmp.patch_fromText("")) + + strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n" + self.assertEquals(strp, str(self.dmp.patch_fromText(strp)[0])) + + self.assertEquals("@@ -1 +1 @@\n-a\n+b\n", str(self.dmp.patch_fromText("@@ -1 +1 @@\n-a\n+b\n")[0])) + + self.assertEquals("@@ -1,3 +0,0 @@\n-abc\n", str(self.dmp.patch_fromText("@@ -1,3 +0,0 @@\n-abc\n")[0])) + + self.assertEquals("@@ -0,0 +1,3 @@\n+abc\n", str(self.dmp.patch_fromText("@@ -0,0 +1,3 @@\n+abc\n")[0])) + + # Generates error. + try: + self.dmp.patch_fromText("Bad\nPatch\n") + self.assertFalse(True) + except ValueError: + # Exception expected. + pass + + def testPatchToText(self): + strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n" + p = self.dmp.patch_fromText(strp) + self.assertEquals(strp, self.dmp.patch_toText(p)) + + strp = "@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n" + p = self.dmp.patch_fromText(strp) + self.assertEquals(strp, self.dmp.patch_toText(p)) + + def testPatchAddContext(self): + self.dmp.Patch_Margin = 4 + p = self.dmp.patch_fromText("@@ -21,4 +21,10 @@\n-jump\n+somersault\n")[0] + self.dmp.patch_addContext(p, "The quick brown fox jumps over the lazy dog.") + self.assertEquals("@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n", str(p)) + + # Same, but not enough trailing context. + p = self.dmp.patch_fromText("@@ -21,4 +21,10 @@\n-jump\n+somersault\n")[0] + self.dmp.patch_addContext(p, "The quick brown fox jumps.") + self.assertEquals("@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n", str(p)) + + # Same, but not enough leading context. + p = self.dmp.patch_fromText("@@ -3 +3,2 @@\n-e\n+at\n")[0] + self.dmp.patch_addContext(p, "The quick brown fox jumps.") + self.assertEquals("@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n", str(p)) + + # Same, but with ambiguity. + p = self.dmp.patch_fromText("@@ -3 +3,2 @@\n-e\n+at\n")[0] + self.dmp.patch_addContext(p, "The quick brown fox jumps. The quick brown fox crashes.") + self.assertEquals("@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n", str(p)) + + def testPatchMake(self): + # Null case. + patches = self.dmp.patch_make("", "") + self.assertEquals("", self.dmp.patch_toText(patches)) + + text1 = "The quick brown fox jumps over the lazy dog." + text2 = "That quick brown fox jumped over a lazy dog." + # Text2+Text1 inputs. + expectedPatch = "@@ -1,8 +1,7 @@\n Th\n-at\n+e\n qui\n@@ -21,17 +21,18 @@\n jump\n-ed\n+s\n over \n-a\n+the\n laz\n" + # The second patch must be "-21,17 +21,18", not "-22,17 +21,18" due to rolling context. + patches = self.dmp.patch_make(text2, text1) + self.assertEquals(expectedPatch, self.dmp.patch_toText(patches)) + + # Text1+Text2 inputs. + expectedPatch = "@@ -1,11 +1,12 @@\n Th\n-e\n+at\n quick b\n@@ -22,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n" + patches = self.dmp.patch_make(text1, text2) + self.assertEquals(expectedPatch, self.dmp.patch_toText(patches)) + + # Diff input. + diffs = self.dmp.diff_main(text1, text2, False) + patches = self.dmp.patch_make(diffs) + self.assertEquals(expectedPatch, self.dmp.patch_toText(patches)) + + # Text1+Diff inputs. + patches = self.dmp.patch_make(text1, diffs) + self.assertEquals(expectedPatch, self.dmp.patch_toText(patches)) + + # Text1+Text2+Diff inputs (deprecated). + patches = self.dmp.patch_make(text1, text2, diffs) + self.assertEquals(expectedPatch, self.dmp.patch_toText(patches)) + + # Character encoding. + patches = self.dmp.patch_make("`1234567890-=[]\\;',./", "~!@#$%^&*()_+{}|:\"<>?") + self.assertEquals("@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n", self.dmp.patch_toText(patches)) + + # Character decoding. + diffs = [(self.dmp.DIFF_DELETE, "`1234567890-=[]\\;',./"), (self.dmp.DIFF_INSERT, "~!@#$%^&*()_+{}|:\"<>?")] + self.assertEquals(diffs, self.dmp.patch_fromText("@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n")[0].diffs) + + # Long string with repeats. + text1 = "" + for x in range(100): + text1 += "abcdef" + text2 = text1 + "123" + expectedPatch = "@@ -573,28 +573,31 @@\n cdefabcdefabcdefabcdefabcdef\n+123\n" + patches = self.dmp.patch_make(text1, text2) + self.assertEquals(expectedPatch, self.dmp.patch_toText(patches)) + + # Test null inputs. + try: + self.dmp.patch_make(None, None) + self.assertFalse(True) + except ValueError: + # Exception expected. + pass + + def testPatchSplitMax(self): + # Assumes that Match_MaxBits is 32. + patches = self.dmp.patch_make("abcdefghijklmnopqrstuvwxyz01234567890", "XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0") + self.dmp.patch_splitMax(patches) + self.assertEquals("@@ -1,32 +1,46 @@\n+X\n ab\n+X\n cd\n+X\n ef\n+X\n gh\n+X\n ij\n+X\n kl\n+X\n mn\n+X\n op\n+X\n qr\n+X\n st\n+X\n uv\n+X\n wx\n+X\n yz\n+X\n 012345\n@@ -25,13 +39,18 @@\n zX01\n+X\n 23\n+X\n 45\n+X\n 67\n+X\n 89\n+X\n 0\n", self.dmp.patch_toText(patches)) + + patches = self.dmp.patch_make("abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz", "abcdefuvwxyz") + oldToText = self.dmp.patch_toText(patches) + self.dmp.patch_splitMax(patches) + self.assertEquals(oldToText, self.dmp.patch_toText(patches)) + + patches = self.dmp.patch_make("1234567890123456789012345678901234567890123456789012345678901234567890", "abc") + self.dmp.patch_splitMax(patches) + self.assertEquals("@@ -1,32 +1,4 @@\n-1234567890123456789012345678\n 9012\n@@ -29,32 +1,4 @@\n-9012345678901234567890123456\n 7890\n@@ -57,14 +1,3 @@\n-78901234567890\n+abc\n", self.dmp.patch_toText(patches)) + + patches = self.dmp.patch_make("abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1", "abcdefghij , h : 1 , t : 1 abcdefghij , h : 1 , t : 1 abcdefghij , h : 0 , t : 1") + self.dmp.patch_splitMax(patches) + self.assertEquals("@@ -2,32 +2,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n@@ -29,32 +29,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n", self.dmp.patch_toText(patches)) + + def testPatchAddPadding(self): + # Both edges full. + patches = self.dmp.patch_make("", "test") + self.assertEquals("@@ -0,0 +1,4 @@\n+test\n", self.dmp.patch_toText(patches)) + self.dmp.patch_addPadding(patches) + self.assertEquals("@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n", self.dmp.patch_toText(patches)) + + # Both edges partial. + patches = self.dmp.patch_make("XY", "XtestY") + self.assertEquals("@@ -1,2 +1,6 @@\n X\n+test\n Y\n", self.dmp.patch_toText(patches)) + self.dmp.patch_addPadding(patches) + self.assertEquals("@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n", self.dmp.patch_toText(patches)) + + # Both edges none. + patches = self.dmp.patch_make("XXXXYYYY", "XXXXtestYYYY") + self.assertEquals("@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n", self.dmp.patch_toText(patches)) + self.dmp.patch_addPadding(patches) + self.assertEquals("@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n", self.dmp.patch_toText(patches)) + + def testPatchApply(self): + self.dmp.Match_Distance = 1000 + self.dmp.Match_Threshold = 0.5 + self.dmp.Patch_DeleteThreshold = 0.5 + # Null case. + patches = self.dmp.patch_make("", "") + results = self.dmp.patch_apply(patches, "Hello world.") + self.assertEquals(("Hello world.", []), results) + + # Exact match. + patches = self.dmp.patch_make("The quick brown fox jumps over the lazy dog.", "That quick brown fox jumped over a lazy dog.") + results = self.dmp.patch_apply(patches, "The quick brown fox jumps over the lazy dog.") + self.assertEquals(("That quick brown fox jumped over a lazy dog.", [True, True]), results) + + # Partial match. + results = self.dmp.patch_apply(patches, "The quick red rabbit jumps over the tired tiger.") + self.assertEquals(("That quick red rabbit jumped over a tired tiger.", [True, True]), results) + + # Failed match. + results = self.dmp.patch_apply(patches, "I am the very model of a modern major general.") + self.assertEquals(("I am the very model of a modern major general.", [False, False]), results) + + # Big delete, small change. + patches = self.dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy") + results = self.dmp.patch_apply(patches, "x123456789012345678901234567890-----++++++++++-----123456789012345678901234567890y") + self.assertEquals(("xabcy", [True, True]), results) + + # Big delete, big change 1. + patches = self.dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy") + results = self.dmp.patch_apply(patches, "x12345678901234567890---------------++++++++++---------------12345678901234567890y") + self.assertEquals(("xabc12345678901234567890---------------++++++++++---------------12345678901234567890y", [False, True]), results) + + # Big delete, big change 2. + self.dmp.Patch_DeleteThreshold = 0.6 + patches = self.dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy") + results = self.dmp.patch_apply(patches, "x12345678901234567890---------------++++++++++---------------12345678901234567890y") + self.assertEquals(("xabcy", [True, True]), results) + self.dmp.Patch_DeleteThreshold = 0.5 + + # Compensate for failed patch. + self.dmp.Match_Threshold = 0.0 + self.dmp.Match_Distance = 0 + patches = self.dmp.patch_make("abcdefghijklmnopqrstuvwxyz--------------------1234567890", "abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------1234567YYYYYYYYYY890") + results = self.dmp.patch_apply(patches, "ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890") + self.assertEquals(("ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890", [False, True]), results) + self.dmp.Match_Threshold = 0.5 + self.dmp.Match_Distance = 1000 + + # No side effects. + patches = self.dmp.patch_make("", "test") + patchstr = self.dmp.patch_toText(patches) + results = self.dmp.patch_apply(patches, "") + self.assertEquals(patchstr, self.dmp.patch_toText(patches)) + + # No side effects with major delete. + patches = self.dmp.patch_make("The quick brown fox jumps over the lazy dog.", "Woof") + patchstr = self.dmp.patch_toText(patches) + self.dmp.patch_apply(patches, "The quick brown fox jumps over the lazy dog.") + self.assertEquals(patchstr, self.dmp.patch_toText(patches)) + + # Edge exact match. + patches = self.dmp.patch_make("", "test") + self.dmp.patch_apply(patches, "") + self.assertEquals(("test", [True]), results) + + # Near edge exact match. + patches = self.dmp.patch_make("XY", "XtestY") + results = self.dmp.patch_apply(patches, "XY") + self.assertEquals(("XtestY", [True]), results) + + # Edge partial match. + patches = self.dmp.patch_make("y", "y123") + results = self.dmp.patch_apply(patches, "x") + self.assertEquals(("x123", [True]), results) + + +if __name__ == "__main__": + unittest.main() diff --git a/python3/__init__.py b/python3/__init__.py new file mode 100644 index 0000000..bd6e8b6 --- /dev/null +++ b/python3/__init__.py @@ -0,0 +1,2 @@ +from .diff_match_patch import diff_match_patch, patch_obj + diff --git a/python3/diff_match_patch.py b/python3/diff_match_patch.py new file mode 100644 index 0000000..0a9b5e8 --- /dev/null +++ b/python3/diff_match_patch.py @@ -0,0 +1,1906 @@ +#!/usr/bin/python3 + +"""Diff Match and Patch +Copyright 2018 The diff-match-patch Authors. +https://github.com/google/diff-match-patch + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Functions for diff, match and patch. + +Computes the difference between two texts to create a patch. +Applies the patch onto another text, allowing for errors. +""" + +__author__ = 'fraser@google.com (Neil Fraser)' + +import math +import re +import sys +import time +import urllib.parse + +class diff_match_patch: + """Class containing the diff, match and patch methods. + + Also contains the behaviour settings. + """ + + def __init__(self): + """Inits a diff_match_patch object with default settings. + Redefine these in your program to override the defaults. + """ + + # Number of seconds to map a diff before giving up (0 for infinity). + self.Diff_Timeout = 1.0 + # Cost of an empty edit operation in terms of edit characters. + self.Diff_EditCost = 4 + # At what point is no match declared (0.0 = perfection, 1.0 = very loose). + self.Match_Threshold = 0.5 + # How far to search for a match (0 = exact location, 1000+ = broad match). + # A match this many characters away from the expected location will add + # 1.0 to the score (0.0 is a perfect match). + self.Match_Distance = 1000 + # When deleting a large block of text (over ~64 characters), how close do + # the contents have to be to match the expected contents. (0.0 = perfection, + # 1.0 = very loose). Note that Match_Threshold controls how closely the + # end points of a delete need to match. + self.Patch_DeleteThreshold = 0.5 + # Chunk size for context length. + self.Patch_Margin = 4 + + # The number of bits in an int. + # Python has no maximum, thus to disable patch splitting set to 0. + # However to avoid long patches in certain pathological cases, use 32. + # Multiple short patches (using native ints) are much faster than long ones. + self.Match_MaxBits = 32 + + # DIFF FUNCTIONS + + # The data structure representing a diff is an array of tuples: + # [(DIFF_DELETE, "Hello"), (DIFF_INSERT, "Goodbye"), (DIFF_EQUAL, " world.")] + # which means: delete "Hello", add "Goodbye" and keep " world." + DIFF_DELETE = -1 + DIFF_INSERT = 1 + DIFF_EQUAL = 0 + + def diff_main(self, text1, text2, checklines=True, deadline=None): + """Find the differences between two texts. Simplifies the problem by + stripping any common prefix or suffix off the texts before diffing. + + Args: + text1: Old string to be diffed. + text2: New string to be diffed. + checklines: Optional speedup flag. If present and false, then don't run + a line-level diff first to identify the changed areas. + Defaults to true, which does a faster, slightly less optimal diff. + deadline: Optional time when the diff should be complete by. Used + internally for recursive calls. Users should set DiffTimeout instead. + + Returns: + Array of changes. + """ + # Set a deadline by which time the diff must be complete. + if deadline == None: + # Unlike in most languages, Python counts time in seconds. + if self.Diff_Timeout <= 0: + deadline = sys.maxsize + else: + deadline = time.time() + self.Diff_Timeout + + # Check for null inputs. + if text1 == None or text2 == None: + raise ValueError("Null inputs. (diff_main)") + + # Check for equality (speedup). + if text1 == text2: + if text1: + return [(self.DIFF_EQUAL, text1)] + return [] + + # Trim off common prefix (speedup). + commonlength = self.diff_commonPrefix(text1, text2) + commonprefix = text1[:commonlength] + text1 = text1[commonlength:] + text2 = text2[commonlength:] + + # Trim off common suffix (speedup). + commonlength = self.diff_commonSuffix(text1, text2) + if commonlength == 0: + commonsuffix = '' + else: + commonsuffix = text1[-commonlength:] + text1 = text1[:-commonlength] + text2 = text2[:-commonlength] + + # Compute the diff on the middle block. + diffs = self.diff_compute(text1, text2, checklines, deadline) + + # Restore the prefix and suffix. + if commonprefix: + diffs[:0] = [(self.DIFF_EQUAL, commonprefix)] + if commonsuffix: + diffs.append((self.DIFF_EQUAL, commonsuffix)) + self.diff_cleanupMerge(diffs) + return diffs + + def diff_compute(self, text1, text2, checklines, deadline): + """Find the differences between two texts. Assumes that the texts do not + have any common prefix or suffix. + + Args: + text1: Old string to be diffed. + text2: New string to be diffed. + checklines: Speedup flag. If false, then don't run a line-level diff + first to identify the changed areas. + If true, then run a faster, slightly less optimal diff. + deadline: Time when the diff should be complete by. + + Returns: + Array of changes. + """ + if not text1: + # Just add some text (speedup). + return [(self.DIFF_INSERT, text2)] + + if not text2: + # Just delete some text (speedup). + return [(self.DIFF_DELETE, text1)] + + if len(text1) > len(text2): + (longtext, shorttext) = (text1, text2) + else: + (shorttext, longtext) = (text1, text2) + i = longtext.find(shorttext) + if i != -1: + # Shorter text is inside the longer text (speedup). + diffs = [(self.DIFF_INSERT, longtext[:i]), (self.DIFF_EQUAL, shorttext), + (self.DIFF_INSERT, longtext[i + len(shorttext):])] + # Swap insertions for deletions if diff is reversed. + if len(text1) > len(text2): + diffs[0] = (self.DIFF_DELETE, diffs[0][1]) + diffs[2] = (self.DIFF_DELETE, diffs[2][1]) + return diffs + + if len(shorttext) == 1: + # Single character string. + # After the previous speedup, the character can't be an equality. + return [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)] + + # Check to see if the problem can be split in two. + hm = self.diff_halfMatch(text1, text2) + if hm: + # A half-match was found, sort out the return data. + (text1_a, text1_b, text2_a, text2_b, mid_common) = hm + # Send both pairs off for separate processing. + diffs_a = self.diff_main(text1_a, text2_a, checklines, deadline) + diffs_b = self.diff_main(text1_b, text2_b, checklines, deadline) + # Merge the results. + return diffs_a + [(self.DIFF_EQUAL, mid_common)] + diffs_b + + if checklines and len(text1) > 100 and len(text2) > 100: + return self.diff_lineMode(text1, text2, deadline) + + return self.diff_bisect(text1, text2, deadline) + + def diff_lineMode(self, text1, text2, deadline): + """Do a quick line-level diff on both strings, then rediff the parts for + greater accuracy. + This speedup can produce non-minimal diffs. + + Args: + text1: Old string to be diffed. + text2: New string to be diffed. + deadline: Time when the diff should be complete by. + + Returns: + Array of changes. + """ + + # Scan the text on a line-by-line basis first. + (text1, text2, linearray) = self.diff_linesToChars(text1, text2) + + diffs = self.diff_main(text1, text2, False, deadline) + + # Convert the diff back to original text. + self.diff_charsToLines(diffs, linearray) + # Eliminate freak matches (e.g. blank lines) + self.diff_cleanupSemantic(diffs) + + # Rediff any replacement blocks, this time character-by-character. + # Add a dummy entry at the end. + diffs.append((self.DIFF_EQUAL, '')) + pointer = 0 + count_delete = 0 + count_insert = 0 + text_delete = '' + text_insert = '' + while pointer < len(diffs): + if diffs[pointer][0] == self.DIFF_INSERT: + count_insert += 1 + text_insert += diffs[pointer][1] + elif diffs[pointer][0] == self.DIFF_DELETE: + count_delete += 1 + text_delete += diffs[pointer][1] + elif diffs[pointer][0] == self.DIFF_EQUAL: + # Upon reaching an equality, check for prior redundancies. + if count_delete >= 1 and count_insert >= 1: + # Delete the offending records and add the merged ones. + a = self.diff_main(text_delete, text_insert, False, deadline) + diffs[pointer - count_delete - count_insert : pointer] = a + pointer = pointer - count_delete - count_insert + len(a) + count_insert = 0 + count_delete = 0 + text_delete = '' + text_insert = '' + + pointer += 1 + + diffs.pop() # Remove the dummy entry at the end. + + return diffs + + def diff_bisect(self, text1, text2, deadline): + """Find the 'middle snake' of a diff, split the problem in two + and return the recursively constructed diff. + See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + + Args: + text1: Old string to be diffed. + text2: New string to be diffed. + deadline: Time at which to bail if not yet complete. + + Returns: + Array of diff tuples. + """ + + # Cache the text lengths to prevent multiple calls. + text1_length = len(text1) + text2_length = len(text2) + max_d = (text1_length + text2_length + 1) // 2 + v_offset = max_d + v_length = 2 * max_d + v1 = [-1] * v_length + v1[v_offset + 1] = 0 + v2 = v1[:] + delta = text1_length - text2_length + # If the total number of characters is odd, then the front path will + # collide with the reverse path. + front = (delta % 2 != 0) + # Offsets for start and end of k loop. + # Prevents mapping of space beyond the grid. + k1start = 0 + k1end = 0 + k2start = 0 + k2end = 0 + for d in range(max_d): + # Bail out if deadline is reached. + if time.time() > deadline: + break + + # Walk the front path one step. + for k1 in range(-d + k1start, d + 1 - k1end, 2): + k1_offset = v_offset + k1 + if k1 == -d or (k1 != d and + v1[k1_offset - 1] < v1[k1_offset + 1]): + x1 = v1[k1_offset + 1] + else: + x1 = v1[k1_offset - 1] + 1 + y1 = x1 - k1 + while (x1 < text1_length and y1 < text2_length and + text1[x1] == text2[y1]): + x1 += 1 + y1 += 1 + v1[k1_offset] = x1 + if x1 > text1_length: + # Ran off the right of the graph. + k1end += 2 + elif y1 > text2_length: + # Ran off the bottom of the graph. + k1start += 2 + elif front: + k2_offset = v_offset + delta - k1 + if k2_offset >= 0 and k2_offset < v_length and v2[k2_offset] != -1: + # Mirror x2 onto top-left coordinate system. + x2 = text1_length - v2[k2_offset] + if x1 >= x2: + # Overlap detected. + return self.diff_bisectSplit(text1, text2, x1, y1, deadline) + + # Walk the reverse path one step. + for k2 in range(-d + k2start, d + 1 - k2end, 2): + k2_offset = v_offset + k2 + if k2 == -d or (k2 != d and + v2[k2_offset - 1] < v2[k2_offset + 1]): + x2 = v2[k2_offset + 1] + else: + x2 = v2[k2_offset - 1] + 1 + y2 = x2 - k2 + while (x2 < text1_length and y2 < text2_length and + text1[-x2 - 1] == text2[-y2 - 1]): + x2 += 1 + y2 += 1 + v2[k2_offset] = x2 + if x2 > text1_length: + # Ran off the left of the graph. + k2end += 2 + elif y2 > text2_length: + # Ran off the top of the graph. + k2start += 2 + elif not front: + k1_offset = v_offset + delta - k2 + if k1_offset >= 0 and k1_offset < v_length and v1[k1_offset] != -1: + x1 = v1[k1_offset] + y1 = v_offset + x1 - k1_offset + # Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2 + if x1 >= x2: + # Overlap detected. + return self.diff_bisectSplit(text1, text2, x1, y1, deadline) + + # Diff took too long and hit the deadline or + # number of diffs equals number of characters, no commonality at all. + return [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)] + + def diff_bisectSplit(self, text1, text2, x, y, deadline): + """Given the location of the 'middle snake', split the diff in two parts + and recurse. + + Args: + text1: Old string to be diffed. + text2: New string to be diffed. + x: Index of split point in text1. + y: Index of split point in text2. + deadline: Time at which to bail if not yet complete. + + Returns: + Array of diff tuples. + """ + text1a = text1[:x] + text2a = text2[:y] + text1b = text1[x:] + text2b = text2[y:] + + # Compute both diffs serially. + diffs = self.diff_main(text1a, text2a, False, deadline) + diffsb = self.diff_main(text1b, text2b, False, deadline) + + return diffs + diffsb + + def diff_linesToChars(self, text1, text2): + """Split two texts into an array of strings. Reduce the texts to a string + of hashes where each Unicode character represents one line. + + Args: + text1: First string. + text2: Second string. + + Returns: + Three element tuple, containing the encoded text1, the encoded text2 and + the array of unique strings. The zeroth element of the array of unique + strings is intentionally blank. + """ + lineArray = [] # e.g. lineArray[4] == "Hello\n" + lineHash = {} # e.g. lineHash["Hello\n"] == 4 + + # "\x00" is a valid character, but various debuggers don't like it. + # So we'll insert a junk entry to avoid generating a null character. + lineArray.append('') + + def diff_linesToCharsMunge(text): + """Split a text into an array of strings. Reduce the texts to a string + of hashes where each Unicode character represents one line. + Modifies linearray and linehash through being a closure. + + Args: + text: String to encode. + + Returns: + Encoded string. + """ + chars = [] + # Walk the text, pulling out a substring for each line. + # text.split('\n') would would temporarily double our memory footprint. + # Modifying text would create many large strings to garbage collect. + lineStart = 0 + lineEnd = -1 + while lineEnd < len(text) - 1: + lineEnd = text.find('\n', lineStart) + if lineEnd == -1: + lineEnd = len(text) - 1 + line = text[lineStart:lineEnd + 1] + lineStart = lineEnd + 1 + + if line in lineHash: + chars.append(chr(lineHash[line])) + else: + lineArray.append(line) + lineHash[line] = len(lineArray) - 1 + chars.append(chr(len(lineArray) - 1)) + return "".join(chars) + + chars1 = diff_linesToCharsMunge(text1) + chars2 = diff_linesToCharsMunge(text2) + return (chars1, chars2, lineArray) + + def diff_charsToLines(self, diffs, lineArray): + """Rehydrate the text in a diff from a string of line hashes to real lines + of text. + + Args: + diffs: Array of diff tuples. + lineArray: Array of unique strings. + """ + for x in range(len(diffs)): + text = [] + for char in diffs[x][1]: + text.append(lineArray[ord(char)]) + diffs[x] = (diffs[x][0], "".join(text)) + + def diff_commonPrefix(self, text1, text2): + """Determine the common prefix of two strings. + + Args: + text1: First string. + text2: Second string. + + Returns: + The number of characters common to the start of each string. + """ + # Quick check for common null cases. + if not text1 or not text2 or text1[0] != text2[0]: + return 0 + # Binary search. + # Performance analysis: http://neil.fraser.name/news/2007/10/09/ + pointermin = 0 + pointermax = min(len(text1), len(text2)) + pointermid = pointermax + pointerstart = 0 + while pointermin < pointermid: + if text1[pointerstart:pointermid] == text2[pointerstart:pointermid]: + pointermin = pointermid + pointerstart = pointermin + else: + pointermax = pointermid + pointermid = (pointermax - pointermin) // 2 + pointermin + return pointermid + + def diff_commonSuffix(self, text1, text2): + """Determine the common suffix of two strings. + + Args: + text1: First string. + text2: Second string. + + Returns: + The number of characters common to the end of each string. + """ + # Quick check for common null cases. + if not text1 or not text2 or text1[-1] != text2[-1]: + return 0 + # Binary search. + # Performance analysis: http://neil.fraser.name/news/2007/10/09/ + pointermin = 0 + pointermax = min(len(text1), len(text2)) + pointermid = pointermax + pointerend = 0 + while pointermin < pointermid: + if (text1[-pointermid:len(text1) - pointerend] == + text2[-pointermid:len(text2) - pointerend]): + pointermin = pointermid + pointerend = pointermin + else: + pointermax = pointermid + pointermid = (pointermax - pointermin) // 2 + pointermin + return pointermid + + def diff_commonOverlap(self, text1, text2): + """Determine if the suffix of one string is the prefix of another. + + Args: + text1 First string. + text2 Second string. + + Returns: + The number of characters common to the end of the first + string and the start of the second string. + """ + # Cache the text lengths to prevent multiple calls. + text1_length = len(text1) + text2_length = len(text2) + # Eliminate the null case. + if text1_length == 0 or text2_length == 0: + return 0 + # Truncate the longer string. + if text1_length > text2_length: + text1 = text1[-text2_length:] + elif text1_length < text2_length: + text2 = text2[:text1_length] + text_length = min(text1_length, text2_length) + # Quick check for the worst case. + if text1 == text2: + return text_length + + # Start by looking for a single character match + # and increase length until no match is found. + # Performance analysis: http://neil.fraser.name/news/2010/11/04/ + best = 0 + length = 1 + while True: + pattern = text1[-length:] + found = text2.find(pattern) + if found == -1: + return best + length += found + if found == 0 or text1[-length:] == text2[:length]: + best = length + length += 1 + + def diff_halfMatch(self, text1, text2): + """Do the two texts share a substring which is at least half the length of + the longer text? + This speedup can produce non-minimal diffs. + + Args: + text1: First string. + text2: Second string. + + Returns: + Five element Array, containing the prefix of text1, the suffix of text1, + the prefix of text2, the suffix of text2 and the common middle. Or None + if there was no match. + """ + if self.Diff_Timeout <= 0: + # Don't risk returning a non-optimal diff if we have unlimited time. + return None + if len(text1) > len(text2): + (longtext, shorttext) = (text1, text2) + else: + (shorttext, longtext) = (text1, text2) + if len(longtext) < 4 or len(shorttext) * 2 < len(longtext): + return None # Pointless. + + def diff_halfMatchI(longtext, shorttext, i): + """Does a substring of shorttext exist within longtext such that the + substring is at least half the length of longtext? + Closure, but does not reference any external variables. + + Args: + longtext: Longer string. + shorttext: Shorter string. + i: Start index of quarter length substring within longtext. + + Returns: + Five element Array, containing the prefix of longtext, the suffix of + longtext, the prefix of shorttext, the suffix of shorttext and the + common middle. Or None if there was no match. + """ + seed = longtext[i:i + len(longtext) // 4] + best_common = '' + j = shorttext.find(seed) + while j != -1: + prefixLength = self.diff_commonPrefix(longtext[i:], shorttext[j:]) + suffixLength = self.diff_commonSuffix(longtext[:i], shorttext[:j]) + if len(best_common) < suffixLength + prefixLength: + best_common = (shorttext[j - suffixLength:j] + + shorttext[j:j + prefixLength]) + best_longtext_a = longtext[:i - suffixLength] + best_longtext_b = longtext[i + prefixLength:] + best_shorttext_a = shorttext[:j - suffixLength] + best_shorttext_b = shorttext[j + prefixLength:] + j = shorttext.find(seed, j + 1) + + if len(best_common) * 2 >= len(longtext): + return (best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common) + else: + return None + + # First check if the second quarter is the seed for a half-match. + hm1 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 3) // 4) + # Check again based on the third quarter. + hm2 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 1) // 2) + if not hm1 and not hm2: + return None + elif not hm2: + hm = hm1 + elif not hm1: + hm = hm2 + else: + # Both matched. Select the longest. + if len(hm1[4]) > len(hm2[4]): + hm = hm1 + else: + hm = hm2 + + # A half-match was found, sort out the return data. + if len(text1) > len(text2): + (text1_a, text1_b, text2_a, text2_b, mid_common) = hm + else: + (text2_a, text2_b, text1_a, text1_b, mid_common) = hm + return (text1_a, text1_b, text2_a, text2_b, mid_common) + + def diff_cleanupSemantic(self, diffs): + """Reduce the number of edits by eliminating semantically trivial + equalities. + + Args: + diffs: Array of diff tuples. + """ + changes = False + equalities = [] # Stack of indices where equalities are found. + lastequality = None # Always equal to diffs[equalities[-1]][1] + pointer = 0 # Index of current position. + # Number of chars that changed prior to the equality. + length_insertions1, length_deletions1 = 0, 0 + # Number of chars that changed after the equality. + length_insertions2, length_deletions2 = 0, 0 + while pointer < len(diffs): + if diffs[pointer][0] == self.DIFF_EQUAL: # Equality found. + equalities.append(pointer) + length_insertions1, length_insertions2 = length_insertions2, 0 + length_deletions1, length_deletions2 = length_deletions2, 0 + lastequality = diffs[pointer][1] + else: # An insertion or deletion. + if diffs[pointer][0] == self.DIFF_INSERT: + length_insertions2 += len(diffs[pointer][1]) + else: + length_deletions2 += len(diffs[pointer][1]) + # Eliminate an equality that is smaller or equal to the edits on both + # sides of it. + if (lastequality and (len(lastequality) <= + max(length_insertions1, length_deletions1)) and + (len(lastequality) <= max(length_insertions2, length_deletions2))): + # Duplicate record. + diffs.insert(equalities[-1], (self.DIFF_DELETE, lastequality)) + # Change second copy to insert. + diffs[equalities[-1] + 1] = (self.DIFF_INSERT, + diffs[equalities[-1] + 1][1]) + # Throw away the equality we just deleted. + equalities.pop() + # Throw away the previous equality (it needs to be reevaluated). + if len(equalities): + equalities.pop() + if len(equalities): + pointer = equalities[-1] + else: + pointer = -1 + # Reset the counters. + length_insertions1, length_deletions1 = 0, 0 + length_insertions2, length_deletions2 = 0, 0 + lastequality = None + changes = True + pointer += 1 + + # Normalize the diff. + if changes: + self.diff_cleanupMerge(diffs) + self.diff_cleanupSemanticLossless(diffs) + + # Find any overlaps between deletions and insertions. + # e.g: abcxxxxxxdef + # -> abcxxxdef + # e.g: xxxabcdefxxx + # -> defxxxabc + # Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 1 + while pointer < len(diffs): + if (diffs[pointer - 1][0] == self.DIFF_DELETE and + diffs[pointer][0] == self.DIFF_INSERT): + deletion = diffs[pointer - 1][1] + insertion = diffs[pointer][1] + overlap_length1 = self.diff_commonOverlap(deletion, insertion) + overlap_length2 = self.diff_commonOverlap(insertion, deletion) + if overlap_length1 >= overlap_length2: + if (overlap_length1 >= len(deletion) / 2.0 or + overlap_length1 >= len(insertion) / 2.0): + # Overlap found. Insert an equality and trim the surrounding edits. + diffs.insert(pointer, (self.DIFF_EQUAL, + insertion[:overlap_length1])) + diffs[pointer - 1] = (self.DIFF_DELETE, + deletion[:len(deletion) - overlap_length1]) + diffs[pointer + 1] = (self.DIFF_INSERT, + insertion[overlap_length1:]) + pointer += 1 + else: + if (overlap_length2 >= len(deletion) / 2.0 or + overlap_length2 >= len(insertion) / 2.0): + # Reverse overlap found. + # Insert an equality and swap and trim the surrounding edits. + diffs.insert(pointer, (self.DIFF_EQUAL, deletion[:overlap_length2])) + diffs[pointer - 1] = (self.DIFF_INSERT, + insertion[:len(insertion) - overlap_length2]) + diffs[pointer + 1] = (self.DIFF_DELETE, deletion[overlap_length2:]) + pointer += 1 + pointer += 1 + pointer += 1 + + def diff_cleanupSemanticLossless(self, diffs): + """Look for single edits surrounded on both sides by equalities + which can be shifted sideways to align the edit to a word boundary. + e.g: The cat came. -> The cat came. + + Args: + diffs: Array of diff tuples. + """ + + def diff_cleanupSemanticScore(one, two): + """Given two strings, compute a score representing whether the + internal boundary falls on logical boundaries. + Scores range from 6 (best) to 0 (worst). + Closure, but does not reference any external variables. + + Args: + one: First string. + two: Second string. + + Returns: + The score. + """ + if not one or not two: + # Edges are the best. + return 6 + + # Each port of this function behaves slightly differently due to + # subtle differences in each language's definition of things like + # 'whitespace'. Since this function's purpose is largely cosmetic, + # the choice has been made to use each language's native features + # rather than force total conformity. + char1 = one[-1] + char2 = two[0] + nonAlphaNumeric1 = not char1.isalnum() + nonAlphaNumeric2 = not char2.isalnum() + whitespace1 = nonAlphaNumeric1 and char1.isspace() + whitespace2 = nonAlphaNumeric2 and char2.isspace() + lineBreak1 = whitespace1 and (char1 == "\r" or char1 == "\n") + lineBreak2 = whitespace2 and (char2 == "\r" or char2 == "\n") + blankLine1 = lineBreak1 and self.BLANKLINEEND.search(one) + blankLine2 = lineBreak2 and self.BLANKLINESTART.match(two) + + if blankLine1 or blankLine2: + # Five points for blank lines. + return 5 + elif lineBreak1 or lineBreak2: + # Four points for line breaks. + return 4 + elif nonAlphaNumeric1 and not whitespace1 and whitespace2: + # Three points for end of sentences. + return 3 + elif whitespace1 or whitespace2: + # Two points for whitespace. + return 2 + elif nonAlphaNumeric1 or nonAlphaNumeric2: + # One point for non-alphanumeric. + return 1 + return 0 + + pointer = 1 + # Intentionally ignore the first and last element (don't need checking). + while pointer < len(diffs) - 1: + if (diffs[pointer - 1][0] == self.DIFF_EQUAL and + diffs[pointer + 1][0] == self.DIFF_EQUAL): + # This is a single edit surrounded by equalities. + equality1 = diffs[pointer - 1][1] + edit = diffs[pointer][1] + equality2 = diffs[pointer + 1][1] + + # First, shift the edit as far left as possible. + commonOffset = self.diff_commonSuffix(equality1, edit) + if commonOffset: + commonString = edit[-commonOffset:] + equality1 = equality1[:-commonOffset] + edit = commonString + edit[:-commonOffset] + equality2 = commonString + equality2 + + # Second, step character by character right, looking for the best fit. + bestEquality1 = equality1 + bestEdit = edit + bestEquality2 = equality2 + bestScore = (diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2)) + while edit and equality2 and edit[0] == equality2[0]: + equality1 += edit[0] + edit = edit[1:] + equality2[0] + equality2 = equality2[1:] + score = (diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2)) + # The >= encourages trailing rather than leading whitespace on edits. + if score >= bestScore: + bestScore = score + bestEquality1 = equality1 + bestEdit = edit + bestEquality2 = equality2 + + if diffs[pointer - 1][1] != bestEquality1: + # We have an improvement, save it back to the diff. + if bestEquality1: + diffs[pointer - 1] = (diffs[pointer - 1][0], bestEquality1) + else: + del diffs[pointer - 1] + pointer -= 1 + diffs[pointer] = (diffs[pointer][0], bestEdit) + if bestEquality2: + diffs[pointer + 1] = (diffs[pointer + 1][0], bestEquality2) + else: + del diffs[pointer + 1] + pointer -= 1 + pointer += 1 + + # Define some regex patterns for matching boundaries. + BLANKLINEEND = re.compile(r"\n\r?\n$"); + BLANKLINESTART = re.compile(r"^\r?\n\r?\n"); + + def diff_cleanupEfficiency(self, diffs): + """Reduce the number of edits by eliminating operationally trivial + equalities. + + Args: + diffs: Array of diff tuples. + """ + changes = False + equalities = [] # Stack of indices where equalities are found. + lastequality = None # Always equal to diffs[equalities[-1]][1] + pointer = 0 # Index of current position. + pre_ins = False # Is there an insertion operation before the last equality. + pre_del = False # Is there a deletion operation before the last equality. + post_ins = False # Is there an insertion operation after the last equality. + post_del = False # Is there a deletion operation after the last equality. + while pointer < len(diffs): + if diffs[pointer][0] == self.DIFF_EQUAL: # Equality found. + if (len(diffs[pointer][1]) < self.Diff_EditCost and + (post_ins or post_del)): + # Candidate found. + equalities.append(pointer) + pre_ins = post_ins + pre_del = post_del + lastequality = diffs[pointer][1] + else: + # Not a candidate, and can never become one. + equalities = [] + lastequality = None + + post_ins = post_del = False + else: # An insertion or deletion. + if diffs[pointer][0] == self.DIFF_DELETE: + post_del = True + else: + post_ins = True + + # Five types to be split: + # ABXYCD + # AXCD + # ABXC + # AXCD + # ABXC + + if lastequality and ((pre_ins and pre_del and post_ins and post_del) or + ((len(lastequality) < self.Diff_EditCost / 2) and + (pre_ins + pre_del + post_ins + post_del) == 3)): + # Duplicate record. + diffs.insert(equalities[-1], (self.DIFF_DELETE, lastequality)) + # Change second copy to insert. + diffs[equalities[-1] + 1] = (self.DIFF_INSERT, + diffs[equalities[-1] + 1][1]) + equalities.pop() # Throw away the equality we just deleted. + lastequality = None + if pre_ins and pre_del: + # No changes made which could affect previous entry, keep going. + post_ins = post_del = True + equalities = [] + else: + if len(equalities): + equalities.pop() # Throw away the previous equality. + if len(equalities): + pointer = equalities[-1] + else: + pointer = -1 + post_ins = post_del = False + changes = True + pointer += 1 + + if changes: + self.diff_cleanupMerge(diffs) + + def diff_cleanupMerge(self, diffs): + """Reorder and merge like edit sections. Merge equalities. + Any edit section can move as long as it doesn't cross an equality. + + Args: + diffs: Array of diff tuples. + """ + diffs.append((self.DIFF_EQUAL, '')) # Add a dummy entry at the end. + pointer = 0 + count_delete = 0 + count_insert = 0 + text_delete = '' + text_insert = '' + while pointer < len(diffs): + if diffs[pointer][0] == self.DIFF_INSERT: + count_insert += 1 + text_insert += diffs[pointer][1] + pointer += 1 + elif diffs[pointer][0] == self.DIFF_DELETE: + count_delete += 1 + text_delete += diffs[pointer][1] + pointer += 1 + elif diffs[pointer][0] == self.DIFF_EQUAL: + # Upon reaching an equality, check for prior redundancies. + if count_delete + count_insert > 1: + if count_delete != 0 and count_insert != 0: + # Factor out any common prefixies. + commonlength = self.diff_commonPrefix(text_insert, text_delete) + if commonlength != 0: + x = pointer - count_delete - count_insert - 1 + if x >= 0 and diffs[x][0] == self.DIFF_EQUAL: + diffs[x] = (diffs[x][0], diffs[x][1] + + text_insert[:commonlength]) + else: + diffs.insert(0, (self.DIFF_EQUAL, text_insert[:commonlength])) + pointer += 1 + text_insert = text_insert[commonlength:] + text_delete = text_delete[commonlength:] + # Factor out any common suffixies. + commonlength = self.diff_commonSuffix(text_insert, text_delete) + if commonlength != 0: + diffs[pointer] = (diffs[pointer][0], text_insert[-commonlength:] + + diffs[pointer][1]) + text_insert = text_insert[:-commonlength] + text_delete = text_delete[:-commonlength] + # Delete the offending records and add the merged ones. + if count_delete == 0: + diffs[pointer - count_insert : pointer] = [ + (self.DIFF_INSERT, text_insert)] + elif count_insert == 0: + diffs[pointer - count_delete : pointer] = [ + (self.DIFF_DELETE, text_delete)] + else: + diffs[pointer - count_delete - count_insert : pointer] = [ + (self.DIFF_DELETE, text_delete), + (self.DIFF_INSERT, text_insert)] + pointer = pointer - count_delete - count_insert + 1 + if count_delete != 0: + pointer += 1 + if count_insert != 0: + pointer += 1 + elif pointer != 0 and diffs[pointer - 1][0] == self.DIFF_EQUAL: + # Merge this equality with the previous one. + diffs[pointer - 1] = (diffs[pointer - 1][0], + diffs[pointer - 1][1] + diffs[pointer][1]) + del diffs[pointer] + else: + pointer += 1 + + count_insert = 0 + count_delete = 0 + text_delete = '' + text_insert = '' + + if diffs[-1][1] == '': + diffs.pop() # Remove the dummy entry at the end. + + # Second pass: look for single edits surrounded on both sides by equalities + # which can be shifted sideways to eliminate an equality. + # e.g: ABAC -> ABAC + changes = False + pointer = 1 + # Intentionally ignore the first and last element (don't need checking). + while pointer < len(diffs) - 1: + if (diffs[pointer - 1][0] == self.DIFF_EQUAL and + diffs[pointer + 1][0] == self.DIFF_EQUAL): + # This is a single edit surrounded by equalities. + if diffs[pointer][1].endswith(diffs[pointer - 1][1]): + # Shift the edit over the previous equality. + diffs[pointer] = (diffs[pointer][0], + diffs[pointer - 1][1] + + diffs[pointer][1][:-len(diffs[pointer - 1][1])]) + diffs[pointer + 1] = (diffs[pointer + 1][0], + diffs[pointer - 1][1] + diffs[pointer + 1][1]) + del diffs[pointer - 1] + changes = True + elif diffs[pointer][1].startswith(diffs[pointer + 1][1]): + # Shift the edit over the next equality. + diffs[pointer - 1] = (diffs[pointer - 1][0], + diffs[pointer - 1][1] + diffs[pointer + 1][1]) + diffs[pointer] = (diffs[pointer][0], + diffs[pointer][1][len(diffs[pointer + 1][1]):] + + diffs[pointer + 1][1]) + del diffs[pointer + 1] + changes = True + pointer += 1 + + # If shifts were made, the diff needs reordering and another shift sweep. + if changes: + self.diff_cleanupMerge(diffs) + + def diff_xIndex(self, diffs, loc): + """loc is a location in text1, compute and return the equivalent location + in text2. e.g. "The cat" vs "The big cat", 1->1, 5->8 + + Args: + diffs: Array of diff tuples. + loc: Location within text1. + + Returns: + Location within text2. + """ + chars1 = 0 + chars2 = 0 + last_chars1 = 0 + last_chars2 = 0 + for x in range(len(diffs)): + (op, text) = diffs[x] + if op != self.DIFF_INSERT: # Equality or deletion. + chars1 += len(text) + if op != self.DIFF_DELETE: # Equality or insertion. + chars2 += len(text) + if chars1 > loc: # Overshot the location. + break + last_chars1 = chars1 + last_chars2 = chars2 + + if len(diffs) != x and diffs[x][0] == self.DIFF_DELETE: + # The location was deleted. + return last_chars2 + # Add the remaining len(character). + return last_chars2 + (loc - last_chars1) + + def diff_prettyHtml(self, diffs): + """Convert a diff array into a pretty HTML report. + + Args: + diffs: Array of diff tuples. + + Returns: + HTML representation. + """ + html = [] + for (op, data) in diffs: + text = (data.replace("&", "&").replace("<", "<") + .replace(">", ">").replace("\n", "¶
    ")) + if op == self.DIFF_INSERT: + html.append("%s" % text) + elif op == self.DIFF_DELETE: + html.append("%s" % text) + elif op == self.DIFF_EQUAL: + html.append("%s" % text) + return "".join(html) + + def diff_text1(self, diffs): + """Compute and return the source text (all equalities and deletions). + + Args: + diffs: Array of diff tuples. + + Returns: + Source text. + """ + text = [] + for (op, data) in diffs: + if op != self.DIFF_INSERT: + text.append(data) + return "".join(text) + + def diff_text2(self, diffs): + """Compute and return the destination text (all equalities and insertions). + + Args: + diffs: Array of diff tuples. + + Returns: + Destination text. + """ + text = [] + for (op, data) in diffs: + if op != self.DIFF_DELETE: + text.append(data) + return "".join(text) + + def diff_levenshtein(self, diffs): + """Compute the Levenshtein distance; the number of inserted, deleted or + substituted characters. + + Args: + diffs: Array of diff tuples. + + Returns: + Number of changes. + """ + levenshtein = 0 + insertions = 0 + deletions = 0 + for (op, data) in diffs: + if op == self.DIFF_INSERT: + insertions += len(data) + elif op == self.DIFF_DELETE: + deletions += len(data) + elif op == self.DIFF_EQUAL: + # A deletion and an insertion is one substitution. + levenshtein += max(insertions, deletions) + insertions = 0 + deletions = 0 + levenshtein += max(insertions, deletions) + return levenshtein + + def diff_toDelta(self, diffs): + """Crush the diff into an encoded string which describes the operations + required to transform text1 into text2. + E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + Operations are tab-separated. Inserted text is escaped using %xx notation. + + Args: + diffs: Array of diff tuples. + + Returns: + Delta text. + """ + text = [] + for (op, data) in diffs: + if op == self.DIFF_INSERT: + # High ascii will raise UnicodeDecodeError. Use Unicode instead. + data = data.encode("utf-8") + text.append("+" + urllib.parse.quote(data, "!~*'();/?:@&=+$,# ")) + elif op == self.DIFF_DELETE: + text.append("-%d" % len(data)) + elif op == self.DIFF_EQUAL: + text.append("=%d" % len(data)) + return "\t".join(text) + + def diff_fromDelta(self, text1, delta): + """Given the original text1, and an encoded string which describes the + operations required to transform text1 into text2, compute the full diff. + + Args: + text1: Source string for the diff. + delta: Delta text. + + Returns: + Array of diff tuples. + + Raises: + ValueError: If invalid input. + """ + diffs = [] + pointer = 0 # Cursor in text1 + tokens = delta.split("\t") + for token in tokens: + if token == "": + # Blank tokens are ok (from a trailing \t). + continue + # Each token begins with a one character parameter which specifies the + # operation of this token (delete, insert, equality). + param = token[1:] + if token[0] == "+": + param = urllib.parse.unquote(param) + diffs.append((self.DIFF_INSERT, param)) + elif token[0] == "-" or token[0] == "=": + try: + n = int(param) + except ValueError: + raise ValueError("Invalid number in diff_fromDelta: " + param) + if n < 0: + raise ValueError("Negative number in diff_fromDelta: " + param) + text = text1[pointer : pointer + n] + pointer += n + if token[0] == "=": + diffs.append((self.DIFF_EQUAL, text)) + else: + diffs.append((self.DIFF_DELETE, text)) + else: + # Anything else is an error. + raise ValueError("Invalid diff operation in diff_fromDelta: " + + token[0]) + if pointer != len(text1): + raise ValueError( + "Delta length (%d) does not equal source text length (%d)." % + (pointer, len(text1))) + return diffs + + # MATCH FUNCTIONS + + def match_main(self, text, pattern, loc): + """Locate the best instance of 'pattern' in 'text' near 'loc'. + + Args: + text: The text to search. + pattern: The pattern to search for. + loc: The location to search around. + + Returns: + Best match index or -1. + """ + # Check for null inputs. + if text == None or pattern == None: + raise ValueError("Null inputs. (match_main)") + + loc = max(0, min(loc, len(text))) + if text == pattern: + # Shortcut (potentially not guaranteed by the algorithm) + return 0 + elif not text: + # Nothing to match. + return -1 + elif text[loc:loc + len(pattern)] == pattern: + # Perfect match at the perfect spot! (Includes case of null pattern) + return loc + else: + # Do a fuzzy compare. + match = self.match_bitap(text, pattern, loc) + return match + + def match_bitap(self, text, pattern, loc): + """Locate the best instance of 'pattern' in 'text' near 'loc' using the + Bitap algorithm. + + Args: + text: The text to search. + pattern: The pattern to search for. + loc: The location to search around. + + Returns: + Best match index or -1. + """ + # Python doesn't have a maxint limit, so ignore this check. + #if self.Match_MaxBits != 0 and len(pattern) > self.Match_MaxBits: + # raise ValueError("Pattern too long for this application.") + + # Initialise the alphabet. + s = self.match_alphabet(pattern) + + def match_bitapScore(e, x): + """Compute and return the score for a match with e errors and x location. + Accesses loc and pattern through being a closure. + + Args: + e: Number of errors in match. + x: Location of match. + + Returns: + Overall score for match (0.0 = good, 1.0 = bad). + """ + accuracy = float(e) / len(pattern) + proximity = abs(loc - x) + if not self.Match_Distance: + # Dodge divide by zero error. + return proximity and 1.0 or accuracy + return accuracy + (proximity / float(self.Match_Distance)) + + # Highest score beyond which we give up. + score_threshold = self.Match_Threshold + # Is there a nearby exact match? (speedup) + best_loc = text.find(pattern, loc) + if best_loc != -1: + score_threshold = min(match_bitapScore(0, best_loc), score_threshold) + # What about in the other direction? (speedup) + best_loc = text.rfind(pattern, loc + len(pattern)) + if best_loc != -1: + score_threshold = min(match_bitapScore(0, best_loc), score_threshold) + + # Initialise the bit arrays. + matchmask = 1 << (len(pattern) - 1) + best_loc = -1 + + bin_max = len(pattern) + len(text) + # Empty initialization added to appease pychecker. + last_rd = None + for d in range(len(pattern)): + # Scan for the best match each iteration allows for one more error. + # Run a binary search to determine how far from 'loc' we can stray at + # this error level. + bin_min = 0 + bin_mid = bin_max + while bin_min < bin_mid: + if match_bitapScore(d, loc + bin_mid) <= score_threshold: + bin_min = bin_mid + else: + bin_max = bin_mid + bin_mid = (bin_max - bin_min) // 2 + bin_min + + # Use the result from this iteration as the maximum for the next. + bin_max = bin_mid + start = max(1, loc - bin_mid + 1) + finish = min(loc + bin_mid, len(text)) + len(pattern) + + rd = [0] * (finish + 2) + rd[finish + 1] = (1 << d) - 1 + for j in range(finish, start - 1, -1): + if len(text) <= j - 1: + # Out of range. + charMatch = 0 + else: + charMatch = s.get(text[j - 1], 0) + if d == 0: # First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch + else: # Subsequent passes: fuzzy match. + rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) | ( + ((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1] + if rd[j] & matchmask: + score = match_bitapScore(d, j - 1) + # This match will almost certainly be better than any existing match. + # But check anyway. + if score <= score_threshold: + # Told you so. + score_threshold = score + best_loc = j - 1 + if best_loc > loc: + # When passing loc, don't exceed our current distance from loc. + start = max(1, 2 * loc - best_loc) + else: + # Already passed loc, downhill from here on in. + break + # No hope for a (better) match at greater error levels. + if match_bitapScore(d + 1, loc) > score_threshold: + break + last_rd = rd + return best_loc + + def match_alphabet(self, pattern): + """Initialise the alphabet for the Bitap algorithm. + + Args: + pattern: The text to encode. + + Returns: + Hash of character locations. + """ + s = {} + for char in pattern: + s[char] = 0 + for i in range(len(pattern)): + s[pattern[i]] |= 1 << (len(pattern) - i - 1) + return s + + # PATCH FUNCTIONS + + def patch_addContext(self, patch, text): + """Increase the context until it is unique, + but don't let the pattern expand beyond Match_MaxBits. + + Args: + patch: The patch to grow. + text: Source text. + """ + if len(text) == 0: + return + pattern = text[patch.start2 : patch.start2 + patch.length1] + padding = 0 + + # Look for the first and last matches of pattern in text. If two different + # matches are found, increase the pattern length. + while (text.find(pattern) != text.rfind(pattern) and (self.Match_MaxBits == + 0 or len(pattern) < self.Match_MaxBits - self.Patch_Margin - + self.Patch_Margin)): + padding += self.Patch_Margin + pattern = text[max(0, patch.start2 - padding) : + patch.start2 + patch.length1 + padding] + # Add one chunk for good luck. + padding += self.Patch_Margin + + # Add the prefix. + prefix = text[max(0, patch.start2 - padding) : patch.start2] + if prefix: + patch.diffs[:0] = [(self.DIFF_EQUAL, prefix)] + # Add the suffix. + suffix = text[patch.start2 + patch.length1 : + patch.start2 + patch.length1 + padding] + if suffix: + patch.diffs.append((self.DIFF_EQUAL, suffix)) + + # Roll back the start points. + patch.start1 -= len(prefix) + patch.start2 -= len(prefix) + # Extend lengths. + patch.length1 += len(prefix) + len(suffix) + patch.length2 += len(prefix) + len(suffix) + + def patch_make(self, a, b=None, c=None): + """Compute a list of patches to turn text1 into text2. + Use diffs if provided, otherwise compute it ourselves. + There are four ways to call this function, depending on what data is + available to the caller: + Method 1: + a = text1, b = text2 + Method 2: + a = diffs + Method 3 (optimal): + a = text1, b = diffs + Method 4 (deprecated, use method 3): + a = text1, b = text2, c = diffs + + Args: + a: text1 (methods 1,3,4) or Array of diff tuples for text1 to + text2 (method 2). + b: text2 (methods 1,4) or Array of diff tuples for text1 to + text2 (method 3) or undefined (method 2). + c: Array of diff tuples for text1 to text2 (method 4) or + undefined (methods 1,2,3). + + Returns: + Array of Patch objects. + """ + text1 = None + diffs = None + if isinstance(a, str) and isinstance(b, str) and c is None: + # Method 1: text1, text2 + # Compute diffs from text1 and text2. + text1 = a + diffs = self.diff_main(text1, b, True) + if len(diffs) > 2: + self.diff_cleanupSemantic(diffs) + self.diff_cleanupEfficiency(diffs) + elif isinstance(a, list) and b is None and c is None: + # Method 2: diffs + # Compute text1 from diffs. + diffs = a + text1 = self.diff_text1(diffs) + elif isinstance(a, str) and isinstance(b, list) and c is None: + # Method 3: text1, diffs + text1 = a + diffs = b + elif (isinstance(a, str) and isinstance(b, str) and + isinstance(c, list)): + # Method 4: text1, text2, diffs + # text2 is not used. + text1 = a + diffs = c + else: + raise ValueError("Unknown call format to patch_make.") + + if not diffs: + return [] # Get rid of the None case. + patches = [] + patch = patch_obj() + char_count1 = 0 # Number of characters into the text1 string. + char_count2 = 0 # Number of characters into the text2 string. + prepatch_text = text1 # Recreate the patches to determine context info. + postpatch_text = text1 + for x in range(len(diffs)): + (diff_type, diff_text) = diffs[x] + if len(patch.diffs) == 0 and diff_type != self.DIFF_EQUAL: + # A new patch starts here. + patch.start1 = char_count1 + patch.start2 = char_count2 + if diff_type == self.DIFF_INSERT: + # Insertion + patch.diffs.append(diffs[x]) + patch.length2 += len(diff_text) + postpatch_text = (postpatch_text[:char_count2] + diff_text + + postpatch_text[char_count2:]) + elif diff_type == self.DIFF_DELETE: + # Deletion. + patch.length1 += len(diff_text) + patch.diffs.append(diffs[x]) + postpatch_text = (postpatch_text[:char_count2] + + postpatch_text[char_count2 + len(diff_text):]) + elif (diff_type == self.DIFF_EQUAL and + len(diff_text) <= 2 * self.Patch_Margin and + len(patch.diffs) != 0 and len(diffs) != x + 1): + # Small equality inside a patch. + patch.diffs.append(diffs[x]) + patch.length1 += len(diff_text) + patch.length2 += len(diff_text) + + if (diff_type == self.DIFF_EQUAL and + len(diff_text) >= 2 * self.Patch_Margin): + # Time for a new patch. + if len(patch.diffs) != 0: + self.patch_addContext(patch, prepatch_text) + patches.append(patch) + patch = patch_obj() + # Unlike Unidiff, our patch lists have a rolling context. + # http://code.google.com/p/google-diff-match-patch/wiki/Unidiff + # Update prepatch text & pos to reflect the application of the + # just completed patch. + prepatch_text = postpatch_text + char_count1 = char_count2 + + # Update the current character count. + if diff_type != self.DIFF_INSERT: + char_count1 += len(diff_text) + if diff_type != self.DIFF_DELETE: + char_count2 += len(diff_text) + + # Pick up the leftover patch if not empty. + if len(patch.diffs) != 0: + self.patch_addContext(patch, prepatch_text) + patches.append(patch) + return patches + + def patch_deepCopy(self, patches): + """Given an array of patches, return another array that is identical. + + Args: + patches: Array of Patch objects. + + Returns: + Array of Patch objects. + """ + patchesCopy = [] + for patch in patches: + patchCopy = patch_obj() + # No need to deep copy the tuples since they are immutable. + patchCopy.diffs = patch.diffs[:] + patchCopy.start1 = patch.start1 + patchCopy.start2 = patch.start2 + patchCopy.length1 = patch.length1 + patchCopy.length2 = patch.length2 + patchesCopy.append(patchCopy) + return patchesCopy + + def patch_apply(self, patches, text): + """Merge a set of patches onto the text. Return a patched text, as well + as a list of true/false values indicating which patches were applied. + + Args: + patches: Array of Patch objects. + text: Old text. + + Returns: + Two element Array, containing the new text and an array of boolean values. + """ + if not patches: + return (text, []) + + # Deep copy the patches so that no changes are made to originals. + patches = self.patch_deepCopy(patches) + + nullPadding = self.patch_addPadding(patches) + text = nullPadding + text + nullPadding + self.patch_splitMax(patches) + + # delta keeps track of the offset between the expected and actual location + # of the previous patch. If there are patches expected at positions 10 and + # 20, but the first patch was found at 12, delta is 2 and the second patch + # has an effective expected position of 22. + delta = 0 + results = [] + for patch in patches: + expected_loc = patch.start2 + delta + text1 = self.diff_text1(patch.diffs) + end_loc = -1 + if len(text1) > self.Match_MaxBits: + # patch_splitMax will only provide an oversized pattern in the case of + # a monster delete. + start_loc = self.match_main(text, text1[:self.Match_MaxBits], + expected_loc) + if start_loc != -1: + end_loc = self.match_main(text, text1[-self.Match_MaxBits:], + expected_loc + len(text1) - self.Match_MaxBits) + if end_loc == -1 or start_loc >= end_loc: + # Can't find valid trailing context. Drop this patch. + start_loc = -1 + else: + start_loc = self.match_main(text, text1, expected_loc) + if start_loc == -1: + # No match found. :( + results.append(False) + # Subtract the delta for this failed patch from subsequent patches. + delta -= patch.length2 - patch.length1 + else: + # Found a match. :) + results.append(True) + delta = start_loc - expected_loc + if end_loc == -1: + text2 = text[start_loc : start_loc + len(text1)] + else: + text2 = text[start_loc : end_loc + self.Match_MaxBits] + if text1 == text2: + # Perfect match, just shove the replacement text in. + text = (text[:start_loc] + self.diff_text2(patch.diffs) + + text[start_loc + len(text1):]) + else: + # Imperfect match. + # Run a diff to get a framework of equivalent indices. + diffs = self.diff_main(text1, text2, False) + if (len(text1) > self.Match_MaxBits and + self.diff_levenshtein(diffs) / float(len(text1)) > + self.Patch_DeleteThreshold): + # The end points match, but the content is unacceptably bad. + results[-1] = False + else: + self.diff_cleanupSemanticLossless(diffs) + index1 = 0 + for (op, data) in patch.diffs: + if op != self.DIFF_EQUAL: + index2 = self.diff_xIndex(diffs, index1) + if op == self.DIFF_INSERT: # Insertion + text = text[:start_loc + index2] + data + text[start_loc + + index2:] + elif op == self.DIFF_DELETE: # Deletion + text = text[:start_loc + index2] + text[start_loc + + self.diff_xIndex(diffs, index1 + len(data)):] + if op != self.DIFF_DELETE: + index1 += len(data) + # Strip the padding off. + text = text[len(nullPadding):-len(nullPadding)] + return (text, results) + + def patch_addPadding(self, patches): + """Add some padding on text start and end so that edges can match + something. Intended to be called only from within patch_apply. + + Args: + patches: Array of Patch objects. + + Returns: + The padding string added to each side. + """ + paddingLength = self.Patch_Margin + nullPadding = "" + for x in range(1, paddingLength + 1): + nullPadding += chr(x) + + # Bump all the patches forward. + for patch in patches: + patch.start1 += paddingLength + patch.start2 += paddingLength + + # Add some padding on start of first diff. + patch = patches[0] + diffs = patch.diffs + if not diffs or diffs[0][0] != self.DIFF_EQUAL: + # Add nullPadding equality. + diffs.insert(0, (self.DIFF_EQUAL, nullPadding)) + patch.start1 -= paddingLength # Should be 0. + patch.start2 -= paddingLength # Should be 0. + patch.length1 += paddingLength + patch.length2 += paddingLength + elif paddingLength > len(diffs[0][1]): + # Grow first equality. + extraLength = paddingLength - len(diffs[0][1]) + newText = nullPadding[len(diffs[0][1]):] + diffs[0][1] + diffs[0] = (diffs[0][0], newText) + patch.start1 -= extraLength + patch.start2 -= extraLength + patch.length1 += extraLength + patch.length2 += extraLength + + # Add some padding on end of last diff. + patch = patches[-1] + diffs = patch.diffs + if not diffs or diffs[-1][0] != self.DIFF_EQUAL: + # Add nullPadding equality. + diffs.append((self.DIFF_EQUAL, nullPadding)) + patch.length1 += paddingLength + patch.length2 += paddingLength + elif paddingLength > len(diffs[-1][1]): + # Grow last equality. + extraLength = paddingLength - len(diffs[-1][1]) + newText = diffs[-1][1] + nullPadding[:extraLength] + diffs[-1] = (diffs[-1][0], newText) + patch.length1 += extraLength + patch.length2 += extraLength + + return nullPadding + + def patch_splitMax(self, patches): + """Look through the patches and break up any which are longer than the + maximum limit of the match algorithm. + Intended to be called only from within patch_apply. + + Args: + patches: Array of Patch objects. + """ + patch_size = self.Match_MaxBits + if patch_size == 0: + # Python has the option of not splitting strings due to its ability + # to handle integers of arbitrary precision. + return + for x in range(len(patches)): + if patches[x].length1 <= patch_size: + continue + bigpatch = patches[x] + # Remove the big old patch. + del patches[x] + x -= 1 + start1 = bigpatch.start1 + start2 = bigpatch.start2 + precontext = '' + while len(bigpatch.diffs) != 0: + # Create one of several smaller patches. + patch = patch_obj() + empty = True + patch.start1 = start1 - len(precontext) + patch.start2 = start2 - len(precontext) + if precontext: + patch.length1 = patch.length2 = len(precontext) + patch.diffs.append((self.DIFF_EQUAL, precontext)) + + while (len(bigpatch.diffs) != 0 and + patch.length1 < patch_size - self.Patch_Margin): + (diff_type, diff_text) = bigpatch.diffs[0] + if diff_type == self.DIFF_INSERT: + # Insertions are harmless. + patch.length2 += len(diff_text) + start2 += len(diff_text) + patch.diffs.append(bigpatch.diffs.pop(0)) + empty = False + elif (diff_type == self.DIFF_DELETE and len(patch.diffs) == 1 and + patch.diffs[0][0] == self.DIFF_EQUAL and + len(diff_text) > 2 * patch_size): + # This is a large deletion. Let it pass in one chunk. + patch.length1 += len(diff_text) + start1 += len(diff_text) + empty = False + patch.diffs.append((diff_type, diff_text)) + del bigpatch.diffs[0] + else: + # Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text[:patch_size - patch.length1 - + self.Patch_Margin] + patch.length1 += len(diff_text) + start1 += len(diff_text) + if diff_type == self.DIFF_EQUAL: + patch.length2 += len(diff_text) + start2 += len(diff_text) + else: + empty = False + + patch.diffs.append((diff_type, diff_text)) + if diff_text == bigpatch.diffs[0][1]: + del bigpatch.diffs[0] + else: + bigpatch.diffs[0] = (bigpatch.diffs[0][0], + bigpatch.diffs[0][1][len(diff_text):]) + + # Compute the head context for the next patch. + precontext = self.diff_text2(patch.diffs) + precontext = precontext[-self.Patch_Margin:] + # Append the end context for this patch. + postcontext = self.diff_text1(bigpatch.diffs)[:self.Patch_Margin] + if postcontext: + patch.length1 += len(postcontext) + patch.length2 += len(postcontext) + if len(patch.diffs) != 0 and patch.diffs[-1][0] == self.DIFF_EQUAL: + patch.diffs[-1] = (self.DIFF_EQUAL, patch.diffs[-1][1] + + postcontext) + else: + patch.diffs.append((self.DIFF_EQUAL, postcontext)) + + if not empty: + x += 1 + patches.insert(x, patch) + + def patch_toText(self, patches): + """Take a list of patches and return a textual representation. + + Args: + patches: Array of Patch objects. + + Returns: + Text representation of patches. + """ + text = [] + for patch in patches: + text.append(str(patch)) + return "".join(text) + + def patch_fromText(self, textline): + """Parse a textual representation of patches and return a list of patch + objects. + + Args: + textline: Text representation of patches. + + Returns: + Array of Patch objects. + + Raises: + ValueError: If invalid input. + """ + patches = [] + if not textline: + return patches + text = textline.split('\n') + while len(text) != 0: + m = re.match("^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$", text[0]) + if not m: + raise ValueError("Invalid patch string: " + text[0]) + patch = patch_obj() + patches.append(patch) + patch.start1 = int(m.group(1)) + if m.group(2) == '': + patch.start1 -= 1 + patch.length1 = 1 + elif m.group(2) == '0': + patch.length1 = 0 + else: + patch.start1 -= 1 + patch.length1 = int(m.group(2)) + + patch.start2 = int(m.group(3)) + if m.group(4) == '': + patch.start2 -= 1 + patch.length2 = 1 + elif m.group(4) == '0': + patch.length2 = 0 + else: + patch.start2 -= 1 + patch.length2 = int(m.group(4)) + + del text[0] + + while len(text) != 0: + if text[0]: + sign = text[0][0] + else: + sign = '' + line = urllib.parse.unquote(text[0][1:]) + if sign == '+': + # Insertion. + patch.diffs.append((self.DIFF_INSERT, line)) + elif sign == '-': + # Deletion. + patch.diffs.append((self.DIFF_DELETE, line)) + elif sign == ' ': + # Minor equality. + patch.diffs.append((self.DIFF_EQUAL, line)) + elif sign == '@': + # Start of next patch. + break + elif sign == '': + # Blank line? Whatever. + pass + else: + # WTF? + raise ValueError("Invalid patch mode: '%s'\n%s" % (sign, line)) + del text[0] + return patches + + +class patch_obj: + """Class representing one patch operation. + """ + + def __init__(self): + """Initializes with an empty list of diffs. + """ + self.diffs = [] + self.start1 = None + self.start2 = None + self.length1 = 0 + self.length2 = 0 + + def __str__(self): + """Emmulate GNU diff's format. + Header: @@ -382,8 +481,9 @@ + Indicies are printed as 1-based, not 0-based. + + Returns: + The GNU diff string. + """ + if self.length1 == 0: + coords1 = str(self.start1) + ",0" + elif self.length1 == 1: + coords1 = str(self.start1 + 1) + else: + coords1 = str(self.start1 + 1) + "," + str(self.length1) + if self.length2 == 0: + coords2 = str(self.start2) + ",0" + elif self.length2 == 1: + coords2 = str(self.start2 + 1) + else: + coords2 = str(self.start2 + 1) + "," + str(self.length2) + text = ["@@ -", coords1, " +", coords2, " @@\n"] + # Escape the body of the patch with %xx notation. + for (op, data) in self.diffs: + if op == diff_match_patch.DIFF_INSERT: + text.append("+") + elif op == diff_match_patch.DIFF_DELETE: + text.append("-") + elif op == diff_match_patch.DIFF_EQUAL: + text.append(" ") + # High ascii will raise UnicodeDecodeError. Use Unicode instead. + data = data.encode("utf-8") + text.append(urllib.parse.quote(data, "!~*'();/?:@&=+$,# ") + "\n") + return "".join(text) diff --git a/python3/diff_match_patch_test.py b/python3/diff_match_patch_test.py new file mode 100644 index 0000000..bd9cf29 --- /dev/null +++ b/python3/diff_match_patch_test.py @@ -0,0 +1,870 @@ +#!/usr/bin/python3 + +"""Diff Match and Patch -- Test harness +Copyright 2018 The diff-match-patch Authors. +https://github.com/google/diff-match-patch + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import imp +import sys +import time +import unittest +import diff_match_patch as dmp_module +# Force a module reload. Allows one to edit the DMP module and rerun the tests +# without leaving the Python interpreter. +imp.reload(dmp_module) + +class DiffMatchPatchTest(unittest.TestCase): + + def setUp(self): + "Test harness for dmp_module." + self.dmp = dmp_module.diff_match_patch() + + def diff_rebuildtexts(self, diffs): + # Construct the two texts which made up the diff originally. + text1 = "" + text2 = "" + for x in range(0, len(diffs)): + if diffs[x][0] != dmp_module.diff_match_patch.DIFF_INSERT: + text1 += diffs[x][1] + if diffs[x][0] != dmp_module.diff_match_patch.DIFF_DELETE: + text2 += diffs[x][1] + return (text1, text2) + + +class DiffTest(DiffMatchPatchTest): + """DIFF TEST FUNCTIONS""" + + def testDiffCommonPrefix(self): + # Detect any common prefix. + # Null case. + self.assertEqual(0, self.dmp.diff_commonPrefix("abc", "xyz")) + + # Non-null case. + self.assertEqual(4, self.dmp.diff_commonPrefix("1234abcdef", "1234xyz")) + + # Whole case. + self.assertEqual(4, self.dmp.diff_commonPrefix("1234", "1234xyz")) + + def testDiffCommonSuffix(self): + # Detect any common suffix. + # Null case. + self.assertEqual(0, self.dmp.diff_commonSuffix("abc", "xyz")) + + # Non-null case. + self.assertEqual(4, self.dmp.diff_commonSuffix("abcdef1234", "xyz1234")) + + # Whole case. + self.assertEqual(4, self.dmp.diff_commonSuffix("1234", "xyz1234")) + + def testDiffCommonOverlap(self): + # Null case. + self.assertEqual(0, self.dmp.diff_commonOverlap("", "abcd")) + + # Whole case. + self.assertEqual(3, self.dmp.diff_commonOverlap("abc", "abcd")) + + # No overlap. + self.assertEqual(0, self.dmp.diff_commonOverlap("123456", "abcd")) + + # Overlap. + self.assertEqual(3, self.dmp.diff_commonOverlap("123456xxx", "xxxabcd")) + + # Unicode. + # Some overly clever languages (C#) may treat ligatures as equal to their + # component letters. E.g. U+FB01 == 'fi' + self.assertEqual(0, self.dmp.diff_commonOverlap("fi", "\ufb01i")) + + def testDiffHalfMatch(self): + # Detect a halfmatch. + self.dmp.Diff_Timeout = 1 + # No match. + self.assertEqual(None, self.dmp.diff_halfMatch("1234567890", "abcdef")) + + self.assertEqual(None, self.dmp.diff_halfMatch("12345", "23")) + + # Single Match. + self.assertEqual(("12", "90", "a", "z", "345678"), self.dmp.diff_halfMatch("1234567890", "a345678z")) + + self.assertEqual(("a", "z", "12", "90", "345678"), self.dmp.diff_halfMatch("a345678z", "1234567890")) + + self.assertEqual(("abc", "z", "1234", "0", "56789"), self.dmp.diff_halfMatch("abc56789z", "1234567890")) + + self.assertEqual(("a", "xyz", "1", "7890", "23456"), self.dmp.diff_halfMatch("a23456xyz", "1234567890")) + + # Multiple Matches. + self.assertEqual(("12123", "123121", "a", "z", "1234123451234"), self.dmp.diff_halfMatch("121231234123451234123121", "a1234123451234z")) + + self.assertEqual(("", "-=-=-=-=-=", "x", "", "x-=-=-=-=-=-=-="), self.dmp.diff_halfMatch("x-=-=-=-=-=-=-=-=-=-=-=-=", "xx-=-=-=-=-=-=-=")) + + self.assertEqual(("-=-=-=-=-=", "", "", "y", "-=-=-=-=-=-=-=y"), self.dmp.diff_halfMatch("-=-=-=-=-=-=-=-=-=-=-=-=y", "-=-=-=-=-=-=-=yy")) + + # Non-optimal halfmatch. + # Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy + self.assertEqual(("qHillo", "w", "x", "Hulloy", "HelloHe"), self.dmp.diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy")) + + # Optimal no halfmatch. + self.dmp.Diff_Timeout = 0 + self.assertEqual(None, self.dmp.diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy")) + + def testDiffLinesToChars(self): + # Convert lines down to characters. + self.assertEqual(("\x01\x02\x01", "\x02\x01\x02", ["", "alpha\n", "beta\n"]), self.dmp.diff_linesToChars("alpha\nbeta\nalpha\n", "beta\nalpha\nbeta\n")) + + self.assertEqual(("", "\x01\x02\x03\x03", ["", "alpha\r\n", "beta\r\n", "\r\n"]), self.dmp.diff_linesToChars("", "alpha\r\nbeta\r\n\r\n\r\n")) + + self.assertEqual(("\x01", "\x02", ["", "a", "b"]), self.dmp.diff_linesToChars("a", "b")) + + # More than 256 to reveal any 8-bit limitations. + n = 300 + lineList = [] + charList = [] + for x in range(1, n + 1): + lineList.append(str(x) + "\n") + charList.append(chr(x)) + self.assertEqual(n, len(lineList)) + lines = "".join(lineList) + chars = "".join(charList) + self.assertEqual(n, len(chars)) + lineList.insert(0, "") + self.assertEqual((chars, "", lineList), self.dmp.diff_linesToChars(lines, "")) + + def testDiffCharsToLines(self): + # Convert chars up to lines. + diffs = [(self.dmp.DIFF_EQUAL, "\x01\x02\x01"), (self.dmp.DIFF_INSERT, "\x02\x01\x02")] + self.dmp.diff_charsToLines(diffs, ["", "alpha\n", "beta\n"]) + self.assertEqual([(self.dmp.DIFF_EQUAL, "alpha\nbeta\nalpha\n"), (self.dmp.DIFF_INSERT, "beta\nalpha\nbeta\n")], diffs) + + # More than 256 to reveal any 8-bit limitations. + n = 300 + lineList = [] + charList = [] + for x in range(1, n + 1): + lineList.append(str(x) + "\n") + charList.append(chr(x)) + self.assertEqual(n, len(lineList)) + lines = "".join(lineList) + chars = "".join(charList) + self.assertEqual(n, len(chars)) + lineList.insert(0, "") + diffs = [(self.dmp.DIFF_DELETE, chars)] + self.dmp.diff_charsToLines(diffs, lineList) + self.assertEqual([(self.dmp.DIFF_DELETE, lines)], diffs) + + def testDiffCleanupMerge(self): + # Cleanup a messy diff. + # Null case. + diffs = [] + self.dmp.diff_cleanupMerge(diffs) + self.assertEqual([], diffs) + + # No change case. + diffs = [(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "b"), (self.dmp.DIFF_INSERT, "c")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEqual([(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "b"), (self.dmp.DIFF_INSERT, "c")], diffs) + + # Merge equalities. + diffs = [(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_EQUAL, "b"), (self.dmp.DIFF_EQUAL, "c")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEqual([(self.dmp.DIFF_EQUAL, "abc")], diffs) + + # Merge deletions. + diffs = [(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_DELETE, "b"), (self.dmp.DIFF_DELETE, "c")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "abc")], diffs) + + # Merge insertions. + diffs = [(self.dmp.DIFF_INSERT, "a"), (self.dmp.DIFF_INSERT, "b"), (self.dmp.DIFF_INSERT, "c")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEqual([(self.dmp.DIFF_INSERT, "abc")], diffs) + + # Merge interweave. + diffs = [(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_INSERT, "b"), (self.dmp.DIFF_DELETE, "c"), (self.dmp.DIFF_INSERT, "d"), (self.dmp.DIFF_EQUAL, "e"), (self.dmp.DIFF_EQUAL, "f")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "ac"), (self.dmp.DIFF_INSERT, "bd"), (self.dmp.DIFF_EQUAL, "ef")], diffs) + + # Prefix and suffix detection. + diffs = [(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_INSERT, "abc"), (self.dmp.DIFF_DELETE, "dc")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEqual([(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "d"), (self.dmp.DIFF_INSERT, "b"), (self.dmp.DIFF_EQUAL, "c")], diffs) + + # Prefix and suffix detection with equalities. + diffs = [(self.dmp.DIFF_EQUAL, "x"), (self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_INSERT, "abc"), (self.dmp.DIFF_DELETE, "dc"), (self.dmp.DIFF_EQUAL, "y")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEqual([(self.dmp.DIFF_EQUAL, "xa"), (self.dmp.DIFF_DELETE, "d"), (self.dmp.DIFF_INSERT, "b"), (self.dmp.DIFF_EQUAL, "cy")], diffs) + + # Slide edit left. + diffs = [(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_INSERT, "ba"), (self.dmp.DIFF_EQUAL, "c")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEqual([(self.dmp.DIFF_INSERT, "ab"), (self.dmp.DIFF_EQUAL, "ac")], diffs) + + # Slide edit right. + diffs = [(self.dmp.DIFF_EQUAL, "c"), (self.dmp.DIFF_INSERT, "ab"), (self.dmp.DIFF_EQUAL, "a")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEqual([(self.dmp.DIFF_EQUAL, "ca"), (self.dmp.DIFF_INSERT, "ba")], diffs) + + # Slide edit left recursive. + diffs = [(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "b"), (self.dmp.DIFF_EQUAL, "c"), (self.dmp.DIFF_DELETE, "ac"), (self.dmp.DIFF_EQUAL, "x")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_EQUAL, "acx")], diffs) + + # Slide edit right recursive. + diffs = [(self.dmp.DIFF_EQUAL, "x"), (self.dmp.DIFF_DELETE, "ca"), (self.dmp.DIFF_EQUAL, "c"), (self.dmp.DIFF_DELETE, "b"), (self.dmp.DIFF_EQUAL, "a")] + self.dmp.diff_cleanupMerge(diffs) + self.assertEqual([(self.dmp.DIFF_EQUAL, "xca"), (self.dmp.DIFF_DELETE, "cba")], diffs) + + def testDiffCleanupSemanticLossless(self): + # Slide diffs to match logical boundaries. + # Null case. + diffs = [] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEqual([], diffs) + + # Blank lines. + diffs = [(self.dmp.DIFF_EQUAL, "AAA\r\n\r\nBBB"), (self.dmp.DIFF_INSERT, "\r\nDDD\r\n\r\nBBB"), (self.dmp.DIFF_EQUAL, "\r\nEEE")] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEqual([(self.dmp.DIFF_EQUAL, "AAA\r\n\r\n"), (self.dmp.DIFF_INSERT, "BBB\r\nDDD\r\n\r\n"), (self.dmp.DIFF_EQUAL, "BBB\r\nEEE")], diffs) + + # Line boundaries. + diffs = [(self.dmp.DIFF_EQUAL, "AAA\r\nBBB"), (self.dmp.DIFF_INSERT, " DDD\r\nBBB"), (self.dmp.DIFF_EQUAL, " EEE")] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEqual([(self.dmp.DIFF_EQUAL, "AAA\r\n"), (self.dmp.DIFF_INSERT, "BBB DDD\r\n"), (self.dmp.DIFF_EQUAL, "BBB EEE")], diffs) + + # Word boundaries. + diffs = [(self.dmp.DIFF_EQUAL, "The c"), (self.dmp.DIFF_INSERT, "ow and the c"), (self.dmp.DIFF_EQUAL, "at.")] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEqual([(self.dmp.DIFF_EQUAL, "The "), (self.dmp.DIFF_INSERT, "cow and the "), (self.dmp.DIFF_EQUAL, "cat.")], diffs) + + # Alphanumeric boundaries. + diffs = [(self.dmp.DIFF_EQUAL, "The-c"), (self.dmp.DIFF_INSERT, "ow-and-the-c"), (self.dmp.DIFF_EQUAL, "at.")] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEqual([(self.dmp.DIFF_EQUAL, "The-"), (self.dmp.DIFF_INSERT, "cow-and-the-"), (self.dmp.DIFF_EQUAL, "cat.")], diffs) + + # Hitting the start. + diffs = [(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_EQUAL, "ax")] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_EQUAL, "aax")], diffs) + + # Hitting the end. + diffs = [(self.dmp.DIFF_EQUAL, "xa"), (self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_EQUAL, "a")] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEqual([(self.dmp.DIFF_EQUAL, "xaa"), (self.dmp.DIFF_DELETE, "a")], diffs) + + # Sentence boundaries. + diffs = [(self.dmp.DIFF_EQUAL, "The xxx. The "), (self.dmp.DIFF_INSERT, "zzz. The "), (self.dmp.DIFF_EQUAL, "yyy.")] + self.dmp.diff_cleanupSemanticLossless(diffs) + self.assertEqual([(self.dmp.DIFF_EQUAL, "The xxx."), (self.dmp.DIFF_INSERT, " The zzz."), (self.dmp.DIFF_EQUAL, " The yyy.")], diffs) + + def testDiffCleanupSemantic(self): + # Cleanup semantically trivial equalities. + # Null case. + diffs = [] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEqual([], diffs) + + # No elimination #1. + diffs = [(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_INSERT, "cd"), (self.dmp.DIFF_EQUAL, "12"), (self.dmp.DIFF_DELETE, "e")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_INSERT, "cd"), (self.dmp.DIFF_EQUAL, "12"), (self.dmp.DIFF_DELETE, "e")], diffs) + + # No elimination #2. + diffs = [(self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_INSERT, "ABC"), (self.dmp.DIFF_EQUAL, "1234"), (self.dmp.DIFF_DELETE, "wxyz")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_INSERT, "ABC"), (self.dmp.DIFF_EQUAL, "1234"), (self.dmp.DIFF_DELETE, "wxyz")], diffs) + + # Simple elimination. + diffs = [(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_EQUAL, "b"), (self.dmp.DIFF_DELETE, "c")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_INSERT, "b")], diffs) + + # Backpass elimination. + diffs = [(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_EQUAL, "cd"), (self.dmp.DIFF_DELETE, "e"), (self.dmp.DIFF_EQUAL, "f"), (self.dmp.DIFF_INSERT, "g")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "abcdef"), (self.dmp.DIFF_INSERT, "cdfg")], diffs) + + # Multiple eliminations. + diffs = [(self.dmp.DIFF_INSERT, "1"), (self.dmp.DIFF_EQUAL, "A"), (self.dmp.DIFF_DELETE, "B"), (self.dmp.DIFF_INSERT, "2"), (self.dmp.DIFF_EQUAL, "_"), (self.dmp.DIFF_INSERT, "1"), (self.dmp.DIFF_EQUAL, "A"), (self.dmp.DIFF_DELETE, "B"), (self.dmp.DIFF_INSERT, "2")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "AB_AB"), (self.dmp.DIFF_INSERT, "1A2_1A2")], diffs) + + # Word boundaries. + diffs = [(self.dmp.DIFF_EQUAL, "The c"), (self.dmp.DIFF_DELETE, "ow and the c"), (self.dmp.DIFF_EQUAL, "at.")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEqual([(self.dmp.DIFF_EQUAL, "The "), (self.dmp.DIFF_DELETE, "cow and the "), (self.dmp.DIFF_EQUAL, "cat.")], diffs) + + # No overlap elimination. + diffs = [(self.dmp.DIFF_DELETE, "abcxx"), (self.dmp.DIFF_INSERT, "xxdef")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "abcxx"), (self.dmp.DIFF_INSERT, "xxdef")], diffs) + + # Overlap elimination. + diffs = [(self.dmp.DIFF_DELETE, "abcxxx"), (self.dmp.DIFF_INSERT, "xxxdef")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_EQUAL, "xxx"), (self.dmp.DIFF_INSERT, "def")], diffs) + + # Reverse overlap elimination. + diffs = [(self.dmp.DIFF_DELETE, "xxxabc"), (self.dmp.DIFF_INSERT, "defxxx")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEqual([(self.dmp.DIFF_INSERT, "def"), (self.dmp.DIFF_EQUAL, "xxx"), (self.dmp.DIFF_DELETE, "abc")], diffs) + + # Two overlap eliminations. + diffs = [(self.dmp.DIFF_DELETE, "abcd1212"), (self.dmp.DIFF_INSERT, "1212efghi"), (self.dmp.DIFF_EQUAL, "----"), (self.dmp.DIFF_DELETE, "A3"), (self.dmp.DIFF_INSERT, "3BC")] + self.dmp.diff_cleanupSemantic(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "abcd"), (self.dmp.DIFF_EQUAL, "1212"), (self.dmp.DIFF_INSERT, "efghi"), (self.dmp.DIFF_EQUAL, "----"), (self.dmp.DIFF_DELETE, "A"), (self.dmp.DIFF_EQUAL, "3"), (self.dmp.DIFF_INSERT, "BC")], diffs) + + def testDiffCleanupEfficiency(self): + # Cleanup operationally trivial equalities. + self.dmp.Diff_EditCost = 4 + # Null case. + diffs = [] + self.dmp.diff_cleanupEfficiency(diffs) + self.assertEqual([], diffs) + + # No elimination. + diffs = [(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_INSERT, "12"), (self.dmp.DIFF_EQUAL, "wxyz"), (self.dmp.DIFF_DELETE, "cd"), (self.dmp.DIFF_INSERT, "34")] + self.dmp.diff_cleanupEfficiency(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_INSERT, "12"), (self.dmp.DIFF_EQUAL, "wxyz"), (self.dmp.DIFF_DELETE, "cd"), (self.dmp.DIFF_INSERT, "34")], diffs) + + # Four-edit elimination. + diffs = [(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_INSERT, "12"), (self.dmp.DIFF_EQUAL, "xyz"), (self.dmp.DIFF_DELETE, "cd"), (self.dmp.DIFF_INSERT, "34")] + self.dmp.diff_cleanupEfficiency(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "abxyzcd"), (self.dmp.DIFF_INSERT, "12xyz34")], diffs) + + # Three-edit elimination. + diffs = [(self.dmp.DIFF_INSERT, "12"), (self.dmp.DIFF_EQUAL, "x"), (self.dmp.DIFF_DELETE, "cd"), (self.dmp.DIFF_INSERT, "34")] + self.dmp.diff_cleanupEfficiency(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "xcd"), (self.dmp.DIFF_INSERT, "12x34")], diffs) + + # Backpass elimination. + diffs = [(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_INSERT, "12"), (self.dmp.DIFF_EQUAL, "xy"), (self.dmp.DIFF_INSERT, "34"), (self.dmp.DIFF_EQUAL, "z"), (self.dmp.DIFF_DELETE, "cd"), (self.dmp.DIFF_INSERT, "56")] + self.dmp.diff_cleanupEfficiency(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "abxyzcd"), (self.dmp.DIFF_INSERT, "12xy34z56")], diffs) + + # High cost elimination. + self.dmp.Diff_EditCost = 5 + diffs = [(self.dmp.DIFF_DELETE, "ab"), (self.dmp.DIFF_INSERT, "12"), (self.dmp.DIFF_EQUAL, "wxyz"), (self.dmp.DIFF_DELETE, "cd"), (self.dmp.DIFF_INSERT, "34")] + self.dmp.diff_cleanupEfficiency(diffs) + self.assertEqual([(self.dmp.DIFF_DELETE, "abwxyzcd"), (self.dmp.DIFF_INSERT, "12wxyz34")], diffs) + self.dmp.Diff_EditCost = 4 + + def testDiffPrettyHtml(self): + # Pretty print. + diffs = [(self.dmp.DIFF_EQUAL, "a\n"), (self.dmp.DIFF_DELETE, "b"), (self.dmp.DIFF_INSERT, "c&d")] + self.assertEqual("
    <B>b</B>c&d", self.dmp.diff_prettyHtml(diffs)) + + def testDiffText(self): + # Compute the source and destination texts. + diffs = [(self.dmp.DIFF_EQUAL, "jump"), (self.dmp.DIFF_DELETE, "s"), (self.dmp.DIFF_INSERT, "ed"), (self.dmp.DIFF_EQUAL, " over "), (self.dmp.DIFF_DELETE, "the"), (self.dmp.DIFF_INSERT, "a"), (self.dmp.DIFF_EQUAL, " lazy")] + self.assertEqual("jumps over the lazy", self.dmp.diff_text1(diffs)) + + self.assertEqual("jumped over a lazy", self.dmp.diff_text2(diffs)) + + def testDiffDelta(self): + # Convert a diff into delta string. + diffs = [(self.dmp.DIFF_EQUAL, "jump"), (self.dmp.DIFF_DELETE, "s"), (self.dmp.DIFF_INSERT, "ed"), (self.dmp.DIFF_EQUAL, " over "), (self.dmp.DIFF_DELETE, "the"), (self.dmp.DIFF_INSERT, "a"), (self.dmp.DIFF_EQUAL, " lazy"), (self.dmp.DIFF_INSERT, "old dog")] + text1 = self.dmp.diff_text1(diffs) + self.assertEqual("jumps over the lazy", text1) + + delta = self.dmp.diff_toDelta(diffs) + self.assertEqual("=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog", delta) + + # Convert delta string into a diff. + self.assertEqual(diffs, self.dmp.diff_fromDelta(text1, delta)) + + # Generates error (19 != 20). + try: + self.dmp.diff_fromDelta(text1 + "x", delta) + self.assertFalse(True) + except ValueError: + # Exception expected. + pass + + # Generates error (19 != 18). + try: + self.dmp.diff_fromDelta(text1[1:], delta) + self.assertFalse(True) + except ValueError: + # Exception expected. + pass + + # Generates error (%c3%xy invalid Unicode). + # Note: Python 3 can decode this. + #try: + # self.dmp.diff_fromDelta("", "+%c3xy") + # self.assertFalse(True) + #except ValueError: + # # Exception expected. + # pass + + # Test deltas with special characters. + diffs = [(self.dmp.DIFF_EQUAL, "\u0680 \x00 \t %"), (self.dmp.DIFF_DELETE, "\u0681 \x01 \n ^"), (self.dmp.DIFF_INSERT, "\u0682 \x02 \\ |")] + text1 = self.dmp.diff_text1(diffs) + self.assertEqual("\u0680 \x00 \t %\u0681 \x01 \n ^", text1) + + delta = self.dmp.diff_toDelta(diffs) + self.assertEqual("=7\t-7\t+%DA%82 %02 %5C %7C", delta) + + # Convert delta string into a diff. + self.assertEqual(diffs, self.dmp.diff_fromDelta(text1, delta)) + + # Verify pool of unchanged characters. + diffs = [(self.dmp.DIFF_INSERT, "A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ")] + text2 = self.dmp.diff_text2(diffs) + self.assertEqual("A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", text2) + + delta = self.dmp.diff_toDelta(diffs) + self.assertEqual("+A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", delta) + + # Convert delta string into a diff. + self.assertEqual(diffs, self.dmp.diff_fromDelta("", delta)) + + def testDiffXIndex(self): + # Translate a location in text1 to text2. + self.assertEqual(5, self.dmp.diff_xIndex([(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_INSERT, "1234"), (self.dmp.DIFF_EQUAL, "xyz")], 2)) + + # Translation on deletion. + self.assertEqual(1, self.dmp.diff_xIndex([(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "1234"), (self.dmp.DIFF_EQUAL, "xyz")], 3)) + + def testDiffLevenshtein(self): + # Levenshtein with trailing equality. + self.assertEqual(4, self.dmp.diff_levenshtein([(self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_INSERT, "1234"), (self.dmp.DIFF_EQUAL, "xyz")])) + # Levenshtein with leading equality. + self.assertEqual(4, self.dmp.diff_levenshtein([(self.dmp.DIFF_EQUAL, "xyz"), (self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_INSERT, "1234")])) + # Levenshtein with middle equality. + self.assertEqual(7, self.dmp.diff_levenshtein([(self.dmp.DIFF_DELETE, "abc"), (self.dmp.DIFF_EQUAL, "xyz"), (self.dmp.DIFF_INSERT, "1234")])) + + def testDiffBisect(self): + # Normal. + a = "cat" + b = "map" + # Since the resulting diff hasn't been normalized, it would be ok if + # the insertion and deletion pairs are swapped. + # If the order changes, tweak this test as required. + self.assertEqual([(self.dmp.DIFF_DELETE, "c"), (self.dmp.DIFF_INSERT, "m"), (self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "t"), (self.dmp.DIFF_INSERT, "p")], self.dmp.diff_bisect(a, b, sys.maxsize)) + + # Timeout. + self.assertEqual([(self.dmp.DIFF_DELETE, "cat"), (self.dmp.DIFF_INSERT, "map")], self.dmp.diff_bisect(a, b, 0)) + + def testDiffMain(self): + # Perform a trivial diff. + # Null case. + self.assertEqual([], self.dmp.diff_main("", "", False)) + + # Equality. + self.assertEqual([(self.dmp.DIFF_EQUAL, "abc")], self.dmp.diff_main("abc", "abc", False)) + + # Simple insertion. + self.assertEqual([(self.dmp.DIFF_EQUAL, "ab"), (self.dmp.DIFF_INSERT, "123"), (self.dmp.DIFF_EQUAL, "c")], self.dmp.diff_main("abc", "ab123c", False)) + + # Simple deletion. + self.assertEqual([(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "123"), (self.dmp.DIFF_EQUAL, "bc")], self.dmp.diff_main("a123bc", "abc", False)) + + # Two insertions. + self.assertEqual([(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_INSERT, "123"), (self.dmp.DIFF_EQUAL, "b"), (self.dmp.DIFF_INSERT, "456"), (self.dmp.DIFF_EQUAL, "c")], self.dmp.diff_main("abc", "a123b456c", False)) + + # Two deletions. + self.assertEqual([(self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "123"), (self.dmp.DIFF_EQUAL, "b"), (self.dmp.DIFF_DELETE, "456"), (self.dmp.DIFF_EQUAL, "c")], self.dmp.diff_main("a123b456c", "abc", False)) + + # Perform a real diff. + # Switch off the timeout. + self.dmp.Diff_Timeout = 0 + # Simple cases. + self.assertEqual([(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_INSERT, "b")], self.dmp.diff_main("a", "b", False)) + + self.assertEqual([(self.dmp.DIFF_DELETE, "Apple"), (self.dmp.DIFF_INSERT, "Banana"), (self.dmp.DIFF_EQUAL, "s are a"), (self.dmp.DIFF_INSERT, "lso"), (self.dmp.DIFF_EQUAL, " fruit.")], self.dmp.diff_main("Apples are a fruit.", "Bananas are also fruit.", False)) + + self.assertEqual([(self.dmp.DIFF_DELETE, "a"), (self.dmp.DIFF_INSERT, "\u0680"), (self.dmp.DIFF_EQUAL, "x"), (self.dmp.DIFF_DELETE, "\t"), (self.dmp.DIFF_INSERT, "\x00")], self.dmp.diff_main("ax\t", "\u0680x\x00", False)) + + # Overlaps. + self.assertEqual([(self.dmp.DIFF_DELETE, "1"), (self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "y"), (self.dmp.DIFF_EQUAL, "b"), (self.dmp.DIFF_DELETE, "2"), (self.dmp.DIFF_INSERT, "xab")], self.dmp.diff_main("1ayb2", "abxab", False)) + + self.assertEqual([(self.dmp.DIFF_INSERT, "xaxcx"), (self.dmp.DIFF_EQUAL, "abc"), (self.dmp.DIFF_DELETE, "y")], self.dmp.diff_main("abcy", "xaxcxabc", False)) + + self.assertEqual([(self.dmp.DIFF_DELETE, "ABCD"), (self.dmp.DIFF_EQUAL, "a"), (self.dmp.DIFF_DELETE, "="), (self.dmp.DIFF_INSERT, "-"), (self.dmp.DIFF_EQUAL, "bcd"), (self.dmp.DIFF_DELETE, "="), (self.dmp.DIFF_INSERT, "-"), (self.dmp.DIFF_EQUAL, "efghijklmnopqrs"), (self.dmp.DIFF_DELETE, "EFGHIJKLMNOefg")], self.dmp.diff_main("ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg", "a-bcd-efghijklmnopqrs", False)) + + # Large equality. + self.assertEqual([(self.dmp.DIFF_INSERT, " "), (self.dmp.DIFF_EQUAL,"a"), (self.dmp.DIFF_INSERT,"nd"), (self.dmp.DIFF_EQUAL," [[Pennsylvania]]"), (self.dmp.DIFF_DELETE," and [[New")], self.dmp.diff_main("a [[Pennsylvania]] and [[New", " and [[Pennsylvania]]", False)) + + # Timeout. + self.dmp.Diff_Timeout = 0.1 # 100ms + a = "`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n" + b = "I am the very model of a modern major general,\nI've information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n" + # Increase the text lengths by 1024 times to ensure a timeout. + for x in range(10): + a = a + a + b = b + b + startTime = time.time() + self.dmp.diff_main(a, b) + endTime = time.time() + # Test that we took at least the timeout period. + self.assertTrue(self.dmp.Diff_Timeout <= endTime - startTime) + # Test that we didn't take forever (be forgiving). + # Theoretically this test could fail very occasionally if the + # OS task swaps or locks up for a second at the wrong moment. + self.assertTrue(self.dmp.Diff_Timeout * 2 > endTime - startTime) + self.dmp.Diff_Timeout = 0 + + # Test the linemode speedup. + # Must be long to pass the 100 char cutoff. + # Simple line-mode. + a = "1234567890\n" * 13 + b = "abcdefghij\n" * 13 + self.assertEqual(self.dmp.diff_main(a, b, False), self.dmp.diff_main(a, b, True)) + + # Single line-mode. + a = "1234567890" * 13 + b = "abcdefghij" * 13 + self.assertEqual(self.dmp.diff_main(a, b, False), self.dmp.diff_main(a, b, True)) + + # Overlap line-mode. + a = "1234567890\n" * 13 + b = "abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n" + texts_linemode = self.diff_rebuildtexts(self.dmp.diff_main(a, b, True)) + texts_textmode = self.diff_rebuildtexts(self.dmp.diff_main(a, b, False)) + self.assertEqual(texts_textmode, texts_linemode) + + # Test null inputs. + try: + self.dmp.diff_main(None, None) + self.assertFalse(True) + except ValueError: + # Exception expected. + pass + + +class MatchTest(DiffMatchPatchTest): + """MATCH TEST FUNCTIONS""" + + def testMatchAlphabet(self): + # Initialise the bitmasks for Bitap. + self.assertEqual({"a":4, "b":2, "c":1}, self.dmp.match_alphabet("abc")) + + self.assertEqual({"a":37, "b":18, "c":8}, self.dmp.match_alphabet("abcaba")) + + def testMatchBitap(self): + self.dmp.Match_Distance = 100 + self.dmp.Match_Threshold = 0.5 + # Exact matches. + self.assertEqual(5, self.dmp.match_bitap("abcdefghijk", "fgh", 5)) + + self.assertEqual(5, self.dmp.match_bitap("abcdefghijk", "fgh", 0)) + + # Fuzzy matches. + self.assertEqual(4, self.dmp.match_bitap("abcdefghijk", "efxhi", 0)) + + self.assertEqual(2, self.dmp.match_bitap("abcdefghijk", "cdefxyhijk", 5)) + + self.assertEqual(-1, self.dmp.match_bitap("abcdefghijk", "bxy", 1)) + + # Overflow. + self.assertEqual(2, self.dmp.match_bitap("123456789xx0", "3456789x0", 2)) + + self.assertEqual(0, self.dmp.match_bitap("abcdef", "xxabc", 4)) + + self.assertEqual(3, self.dmp.match_bitap("abcdef", "defyy", 4)) + + self.assertEqual(0, self.dmp.match_bitap("abcdef", "xabcdefy", 0)) + + # Threshold test. + self.dmp.Match_Threshold = 0.4 + self.assertEqual(4, self.dmp.match_bitap("abcdefghijk", "efxyhi", 1)) + + self.dmp.Match_Threshold = 0.3 + self.assertEqual(-1, self.dmp.match_bitap("abcdefghijk", "efxyhi", 1)) + + self.dmp.Match_Threshold = 0.0 + self.assertEqual(1, self.dmp.match_bitap("abcdefghijk", "bcdef", 1)) + self.dmp.Match_Threshold = 0.5 + + # Multiple select. + self.assertEqual(0, self.dmp.match_bitap("abcdexyzabcde", "abccde", 3)) + + self.assertEqual(8, self.dmp.match_bitap("abcdexyzabcde", "abccde", 5)) + + # Distance test. + self.dmp.Match_Distance = 10 # Strict location. + self.assertEqual(-1, self.dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24)) + + self.assertEqual(0, self.dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdxxefg", 1)) + + self.dmp.Match_Distance = 1000 # Loose location. + self.assertEqual(0, self.dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24)) + + + def testMatchMain(self): + # Full match. + # Shortcut matches. + self.assertEqual(0, self.dmp.match_main("abcdef", "abcdef", 1000)) + + self.assertEqual(-1, self.dmp.match_main("", "abcdef", 1)) + + self.assertEqual(3, self.dmp.match_main("abcdef", "", 3)) + + self.assertEqual(3, self.dmp.match_main("abcdef", "de", 3)) + + self.assertEqual(3, self.dmp.match_main("abcdef", "defy", 4)) + + self.assertEqual(0, self.dmp.match_main("abcdef", "abcdefy", 0)) + + # Complex match. + self.dmp.Match_Threshold = 0.7 + self.assertEqual(4, self.dmp.match_main("I am the very model of a modern major general.", " that berry ", 5)) + self.dmp.Match_Threshold = 0.5 + + # Test null inputs. + try: + self.dmp.match_main(None, None, 0) + self.assertFalse(True) + except ValueError: + # Exception expected. + pass + + +class PatchTest(DiffMatchPatchTest): + """PATCH TEST FUNCTIONS""" + + def testPatchObj(self): + # Patch Object. + p = dmp_module.patch_obj() + p.start1 = 20 + p.start2 = 21 + p.length1 = 18 + p.length2 = 17 + p.diffs = [(self.dmp.DIFF_EQUAL, "jump"), (self.dmp.DIFF_DELETE, "s"), (self.dmp.DIFF_INSERT, "ed"), (self.dmp.DIFF_EQUAL, " over "), (self.dmp.DIFF_DELETE, "the"), (self.dmp.DIFF_INSERT, "a"), (self.dmp.DIFF_EQUAL, "\nlaz")] + strp = str(p) + self.assertEqual("@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n", strp) + + def testPatchFromText(self): + self.assertEqual([], self.dmp.patch_fromText("")) + + strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n" + self.assertEqual(strp, str(self.dmp.patch_fromText(strp)[0])) + + self.assertEqual("@@ -1 +1 @@\n-a\n+b\n", str(self.dmp.patch_fromText("@@ -1 +1 @@\n-a\n+b\n")[0])) + + self.assertEqual("@@ -1,3 +0,0 @@\n-abc\n", str(self.dmp.patch_fromText("@@ -1,3 +0,0 @@\n-abc\n")[0])) + + self.assertEqual("@@ -0,0 +1,3 @@\n+abc\n", str(self.dmp.patch_fromText("@@ -0,0 +1,3 @@\n+abc\n")[0])) + + # Generates error. + try: + self.dmp.patch_fromText("Bad\nPatch\n") + self.assertFalse(True) + except ValueError: + # Exception expected. + pass + + def testPatchToText(self): + strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n" + p = self.dmp.patch_fromText(strp) + self.assertEqual(strp, self.dmp.patch_toText(p)) + + strp = "@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n" + p = self.dmp.patch_fromText(strp) + self.assertEqual(strp, self.dmp.patch_toText(p)) + + def testPatchAddContext(self): + self.dmp.Patch_Margin = 4 + p = self.dmp.patch_fromText("@@ -21,4 +21,10 @@\n-jump\n+somersault\n")[0] + self.dmp.patch_addContext(p, "The quick brown fox jumps over the lazy dog.") + self.assertEqual("@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n", str(p)) + + # Same, but not enough trailing context. + p = self.dmp.patch_fromText("@@ -21,4 +21,10 @@\n-jump\n+somersault\n")[0] + self.dmp.patch_addContext(p, "The quick brown fox jumps.") + self.assertEqual("@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n", str(p)) + + # Same, but not enough leading context. + p = self.dmp.patch_fromText("@@ -3 +3,2 @@\n-e\n+at\n")[0] + self.dmp.patch_addContext(p, "The quick brown fox jumps.") + self.assertEqual("@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n", str(p)) + + # Same, but with ambiguity. + p = self.dmp.patch_fromText("@@ -3 +3,2 @@\n-e\n+at\n")[0] + self.dmp.patch_addContext(p, "The quick brown fox jumps. The quick brown fox crashes.") + self.assertEqual("@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n", str(p)) + + def testPatchMake(self): + # Null case. + patches = self.dmp.patch_make("", "") + self.assertEqual("", self.dmp.patch_toText(patches)) + + text1 = "The quick brown fox jumps over the lazy dog." + text2 = "That quick brown fox jumped over a lazy dog." + # Text2+Text1 inputs. + expectedPatch = "@@ -1,8 +1,7 @@\n Th\n-at\n+e\n qui\n@@ -21,17 +21,18 @@\n jump\n-ed\n+s\n over \n-a\n+the\n laz\n" + # The second patch must be "-21,17 +21,18", not "-22,17 +21,18" due to rolling context. + patches = self.dmp.patch_make(text2, text1) + self.assertEqual(expectedPatch, self.dmp.patch_toText(patches)) + + # Text1+Text2 inputs. + expectedPatch = "@@ -1,11 +1,12 @@\n Th\n-e\n+at\n quick b\n@@ -22,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n" + patches = self.dmp.patch_make(text1, text2) + self.assertEqual(expectedPatch, self.dmp.patch_toText(patches)) + + # Diff input. + diffs = self.dmp.diff_main(text1, text2, False) + patches = self.dmp.patch_make(diffs) + self.assertEqual(expectedPatch, self.dmp.patch_toText(patches)) + + # Text1+Diff inputs. + patches = self.dmp.patch_make(text1, diffs) + self.assertEqual(expectedPatch, self.dmp.patch_toText(patches)) + + # Text1+Text2+Diff inputs (deprecated). + patches = self.dmp.patch_make(text1, text2, diffs) + self.assertEqual(expectedPatch, self.dmp.patch_toText(patches)) + + # Character encoding. + patches = self.dmp.patch_make("`1234567890-=[]\\;',./", "~!@#$%^&*()_+{}|:\"<>?") + self.assertEqual("@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n", self.dmp.patch_toText(patches)) + + # Character decoding. + diffs = [(self.dmp.DIFF_DELETE, "`1234567890-=[]\\;',./"), (self.dmp.DIFF_INSERT, "~!@#$%^&*()_+{}|:\"<>?")] + self.assertEqual(diffs, self.dmp.patch_fromText("@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n")[0].diffs) + + # Long string with repeats. + text1 = "" + for x in range(100): + text1 += "abcdef" + text2 = text1 + "123" + expectedPatch = "@@ -573,28 +573,31 @@\n cdefabcdefabcdefabcdefabcdef\n+123\n" + patches = self.dmp.patch_make(text1, text2) + self.assertEqual(expectedPatch, self.dmp.patch_toText(patches)) + + # Test null inputs. + try: + self.dmp.patch_make(None, None) + self.assertFalse(True) + except ValueError: + # Exception expected. + pass + + def testPatchSplitMax(self): + # Assumes that Match_MaxBits is 32. + patches = self.dmp.patch_make("abcdefghijklmnopqrstuvwxyz01234567890", "XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0") + self.dmp.patch_splitMax(patches) + self.assertEqual("@@ -1,32 +1,46 @@\n+X\n ab\n+X\n cd\n+X\n ef\n+X\n gh\n+X\n ij\n+X\n kl\n+X\n mn\n+X\n op\n+X\n qr\n+X\n st\n+X\n uv\n+X\n wx\n+X\n yz\n+X\n 012345\n@@ -25,13 +39,18 @@\n zX01\n+X\n 23\n+X\n 45\n+X\n 67\n+X\n 89\n+X\n 0\n", self.dmp.patch_toText(patches)) + + patches = self.dmp.patch_make("abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz", "abcdefuvwxyz") + oldToText = self.dmp.patch_toText(patches) + self.dmp.patch_splitMax(patches) + self.assertEqual(oldToText, self.dmp.patch_toText(patches)) + + patches = self.dmp.patch_make("1234567890123456789012345678901234567890123456789012345678901234567890", "abc") + self.dmp.patch_splitMax(patches) + self.assertEqual("@@ -1,32 +1,4 @@\n-1234567890123456789012345678\n 9012\n@@ -29,32 +1,4 @@\n-9012345678901234567890123456\n 7890\n@@ -57,14 +1,3 @@\n-78901234567890\n+abc\n", self.dmp.patch_toText(patches)) + + patches = self.dmp.patch_make("abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1", "abcdefghij , h : 1 , t : 1 abcdefghij , h : 1 , t : 1 abcdefghij , h : 0 , t : 1") + self.dmp.patch_splitMax(patches) + self.assertEqual("@@ -2,32 +2,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n@@ -29,32 +29,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n", self.dmp.patch_toText(patches)) + + def testPatchAddPadding(self): + # Both edges full. + patches = self.dmp.patch_make("", "test") + self.assertEqual("@@ -0,0 +1,4 @@\n+test\n", self.dmp.patch_toText(patches)) + self.dmp.patch_addPadding(patches) + self.assertEqual("@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n", self.dmp.patch_toText(patches)) + + # Both edges partial. + patches = self.dmp.patch_make("XY", "XtestY") + self.assertEqual("@@ -1,2 +1,6 @@\n X\n+test\n Y\n", self.dmp.patch_toText(patches)) + self.dmp.patch_addPadding(patches) + self.assertEqual("@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n", self.dmp.patch_toText(patches)) + + # Both edges none. + patches = self.dmp.patch_make("XXXXYYYY", "XXXXtestYYYY") + self.assertEqual("@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n", self.dmp.patch_toText(patches)) + self.dmp.patch_addPadding(patches) + self.assertEqual("@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n", self.dmp.patch_toText(patches)) + + def testPatchApply(self): + self.dmp.Match_Distance = 1000 + self.dmp.Match_Threshold = 0.5 + self.dmp.Patch_DeleteThreshold = 0.5 + # Null case. + patches = self.dmp.patch_make("", "") + results = self.dmp.patch_apply(patches, "Hello world.") + self.assertEqual(("Hello world.", []), results) + + # Exact match. + patches = self.dmp.patch_make("The quick brown fox jumps over the lazy dog.", "That quick brown fox jumped over a lazy dog.") + results = self.dmp.patch_apply(patches, "The quick brown fox jumps over the lazy dog.") + self.assertEqual(("That quick brown fox jumped over a lazy dog.", [True, True]), results) + + # Partial match. + results = self.dmp.patch_apply(patches, "The quick red rabbit jumps over the tired tiger.") + self.assertEqual(("That quick red rabbit jumped over a tired tiger.", [True, True]), results) + + # Failed match. + results = self.dmp.patch_apply(patches, "I am the very model of a modern major general.") + self.assertEqual(("I am the very model of a modern major general.", [False, False]), results) + + # Big delete, small change. + patches = self.dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy") + results = self.dmp.patch_apply(patches, "x123456789012345678901234567890-----++++++++++-----123456789012345678901234567890y") + self.assertEqual(("xabcy", [True, True]), results) + + # Big delete, big change 1. + patches = self.dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy") + results = self.dmp.patch_apply(patches, "x12345678901234567890---------------++++++++++---------------12345678901234567890y") + self.assertEqual(("xabc12345678901234567890---------------++++++++++---------------12345678901234567890y", [False, True]), results) + + # Big delete, big change 2. + self.dmp.Patch_DeleteThreshold = 0.6 + patches = self.dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy") + results = self.dmp.patch_apply(patches, "x12345678901234567890---------------++++++++++---------------12345678901234567890y") + self.assertEqual(("xabcy", [True, True]), results) + self.dmp.Patch_DeleteThreshold = 0.5 + + # Compensate for failed patch. + self.dmp.Match_Threshold = 0.0 + self.dmp.Match_Distance = 0 + patches = self.dmp.patch_make("abcdefghijklmnopqrstuvwxyz--------------------1234567890", "abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------1234567YYYYYYYYYY890") + results = self.dmp.patch_apply(patches, "ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890") + self.assertEqual(("ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890", [False, True]), results) + self.dmp.Match_Threshold = 0.5 + self.dmp.Match_Distance = 1000 + + # No side effects. + patches = self.dmp.patch_make("", "test") + patchstr = self.dmp.patch_toText(patches) + results = self.dmp.patch_apply(patches, "") + self.assertEqual(patchstr, self.dmp.patch_toText(patches)) + + # No side effects with major delete. + patches = self.dmp.patch_make("The quick brown fox jumps over the lazy dog.", "Woof") + patchstr = self.dmp.patch_toText(patches) + self.dmp.patch_apply(patches, "The quick brown fox jumps over the lazy dog.") + self.assertEqual(patchstr, self.dmp.patch_toText(patches)) + + # Edge exact match. + patches = self.dmp.patch_make("", "test") + self.dmp.patch_apply(patches, "") + self.assertEqual(("test", [True]), results) + + # Near edge exact match. + patches = self.dmp.patch_make("XY", "XtestY") + results = self.dmp.patch_apply(patches, "XY") + self.assertEqual(("XtestY", [True]), results) + + # Edge partial match. + patches = self.dmp.patch_make("y", "y123") + results = self.dmp.patch_apply(patches, "x") + self.assertEqual(("x123", [True]), results) + + +if __name__ == "__main__": + unittest.main()