├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── lib ├── diff_match_patch.dart └── src │ ├── api.dart │ ├── common.dart │ ├── diff.dart │ ├── diff │ ├── cleanup.dart │ ├── delta.dart │ ├── diff.dart │ ├── half_match.dart │ ├── main.dart │ └── utils.dart │ ├── match.dart │ └── patch.dart ├── pubspec.yaml └── test ├── diff_test.dart ├── match_test.dart └── patch_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # https://www.dartlang.org/tools/private-files.html 2 | 3 | pubspec.lock 4 | 5 | build/ 6 | packages/ 7 | .packages 8 | .dart_tool/ 9 | 10 | .buildlog 11 | *.js_ 12 | *.js.deps 13 | *.js.map 14 | *.dart.js 15 | 16 | # Eclipse 17 | .project 18 | 19 | # IntelliJ 20 | *.iml 21 | *.ipr 22 | *.iws 23 | .idea/ 24 | 25 | # Mac 26 | .DS_Store 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.4.1 (2021-06-03) 2 | - Code cleanup via Pedantic 3 | 4 | # v0.4.0 (2021-06-03) 5 | - Support for null-safety 6 | 7 | # v0.3.0 (2018-10-10) 8 | - Support for Dart 2.0 9 | 10 | # v0.2.2 (2018-10-09) 11 | - Fix implementation of Diff == (and added hashcode) 12 | - Upgrade from unittest to test package 13 | 14 | # v0.2.1 (2014-10-01) 15 | - Update docs to use proper markdown for dartdocs.org 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Diff Match Patch 2 | 3 | This is a port of [google-diff-match-patch](https://code.google.com/p/google-diff-match-patch/) 4 | library to Dart. Initially maintained by localvoid, handed off to jheyne for upgrades. 5 | 6 | ## Algorithms 7 | 8 | This library implements Myer's diff algorithm which is generally considered to 9 | be the best general-purpose diff. A layer of pre-diff speedups and post-diff 10 | cleanups surround the diff algorithm, improving both performance and output 11 | quality. 12 | 13 | This library also implements a Bitap matching algorithm at the heart of a 14 | flexible matching and patching strategy. -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.yaml 2 | 3 | analyzer: 4 | exclude: [.*/**, build/**] # https://github.com/dart-lang/sdk/issues/33112 5 | errors: 6 | uri_has_not_been_generated: ignore 7 | plugins: 8 | - angular 9 | strong-mode: 10 | implicit-casts: false 11 | # implicit-dynamic: false 12 | 13 | # Lint rules and documentation, see http://dart-lang.github.io/linter/lints 14 | linter: 15 | rules: 16 | - cancel_subscriptions 17 | - hash_and_equals 18 | - iterable_contains_unrelated_type 19 | - list_remove_unrelated_type 20 | - test_types_in_equals 21 | - unrelated_type_equality_checks 22 | - valid_regexps 23 | -------------------------------------------------------------------------------- /lib/diff_match_patch.dart: -------------------------------------------------------------------------------- 1 | /// Copyright 2011 Google Inc. 2 | /// Copyright 2014 Boris Kaul 3 | /// http://github.com/localvoid/diff-match-patch 4 | /// 5 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 6 | /// you may not use this file except in compliance with the License. 7 | /// You may obtain a copy of the License at 8 | /// 9 | /// http://www.apache.org/licenses/LICENSE-2.0 10 | /// 11 | /// Unless required by applicable law or agreed to in writing, software 12 | /// distributed under the License is distributed on an 'AS IS' BASIS, 13 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | /// See the License for the specific language governing permissions and 15 | /// limitations under the License. 16 | 17 | library diff_match_patch; 18 | 19 | export 'package:diff_match_patch/src/diff.dart' 20 | show 21 | Diff, 22 | diff, 23 | cleanupSemantic, 24 | cleanupEfficiency, 25 | levenshtein, 26 | DIFF_DELETE, 27 | DIFF_INSERT, 28 | DIFF_EQUAL; 29 | 30 | export 'package:diff_match_patch/src/match.dart' show match; 31 | 32 | export 'package:diff_match_patch/src/patch.dart' 33 | show Patch, patchMake, patchToText, patchFromText, patchApply; 34 | 35 | export 'package:diff_match_patch/src/api.dart' show DiffMatchPatch; 36 | -------------------------------------------------------------------------------- /lib/src/api.dart: -------------------------------------------------------------------------------- 1 | /// Copyright 2011 Google Inc. 2 | /// Copyright 2014 Boris Kaul 3 | /// http://github.com/localvoid/diff-match-patch 4 | /// 5 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 6 | /// you may not use this file except in compliance with the License. 7 | /// You may obtain a copy of the License at 8 | /// 9 | /// http://www.apache.org/licenses/LICENSE-2.0 10 | /// 11 | /// Unless required by applicable law or agreed to in writing, software 12 | /// distributed under the License is distributed on an 'AS IS' BASIS, 13 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | /// See the License for the specific language governing permissions and 15 | /// limitations under the License. 16 | 17 | library api; 18 | 19 | import 'package:diff_match_patch/src/diff.dart' as d; 20 | import 'package:diff_match_patch/src/match.dart' as m; 21 | import 'package:diff_match_patch/src/patch.dart' as p; 22 | 23 | /// Class containing the [diff], [match] and [patch] methods. 24 | /// Also contains the behaviour settings. 25 | class DiffMatchPatch { 26 | // Defaults. 27 | // Set these on your diff_match_patch instance to override the defaults. 28 | 29 | /// Number of seconds to map a diff before giving up (0 for infinity). 30 | double diffTimeout = 1.0; 31 | 32 | /// Cost of an empty edit operation in terms of edit characters. 33 | int diffEditCost = 4; 34 | 35 | /// At what point is no match declared (0.0 = perfection, 1.0 = very loose). 36 | double matchThreshold = 0.5; 37 | 38 | /// How far to search for a match (0 = exact location, 1000+ = broad match). 39 | /// A match this many characters away from the expected location will add 40 | /// 1.0 to the score (0.0 is a perfect match). 41 | int matchDistance = 1000; 42 | 43 | /// When deleting a large block of text (over ~64 characters), how close do 44 | /// the contents have to be to match the expected contents. (0.0 = perfection, 45 | /// 1.0 = very loose). Note that [matchThreshold] controls how closely the 46 | /// end points of a delete need to match. 47 | double patchDeleteThreshold = 0.5; 48 | 49 | /// Chunk size for context length. 50 | int patchMargin = 4; 51 | 52 | /// Find the differences between two texts. Simplifies the problem by 53 | /// stripping any common prefix or suffix off the texts before diffing. 54 | /// 55 | /// * [text1] is the old string to be diffed. 56 | /// * [text2] is the new string to be diffed. 57 | /// * [checklines] is an optional speedup flag. If false, then don't 58 | /// run a line-level diff first to identify the changed areas. 59 | /// Defaults to true, which does a faster, slightly less optimal diff. 60 | /// * [deadline] is an optional time when the diff should be complete by. Used 61 | /// internally for recursive calls. Users should set [diffTimeout] instead. 62 | /// 63 | /// Returns a List of [Diff] objects. 64 | List diff(String text1, String text2, 65 | [bool checklines = true, DateTime? deadline]) { 66 | return d.diff(text1, text2, 67 | checklines: checklines, deadline: deadline, timeout: diffTimeout); 68 | } 69 | 70 | /// Reduce the number of edits by eliminating semantically trivial equalities. 71 | /// 72 | /// [diffs] is a List of Diff objects. 73 | void diffCleanupSemantic(List diffs) { 74 | d.cleanupSemantic(diffs); 75 | } 76 | 77 | /// Reduce the number of edits by eliminating operationally trivial equalities. 78 | /// 79 | /// [diffs] is a List of Diff objects. 80 | void diffCleanupEfficiency(List diffs) { 81 | d.cleanupEfficiency(diffs, diffEditCost); 82 | } 83 | 84 | /// Compute the Levenshtein distance; the number of inserted, deleted or 85 | /// substituted characters. 86 | /// 87 | /// [diffs] is a List of Diff objects. 88 | /// 89 | /// Returns the number of changes. 90 | int diff_levenshtein(List diffs) { 91 | return d.levenshtein(diffs); 92 | } 93 | 94 | /// Locate the best instance of [pattern] in [text] near [loc]. 95 | /// Returns -1 if no match found. 96 | /// 97 | /// * [text] is the text to search. 98 | /// * [pattern] is the pattern to search for. 99 | /// * [loc] is the location to search around. 100 | /// 101 | /// Returns the best match index or -1. 102 | int match(String text, String pattern, int loc) { 103 | return m.match(text, pattern, loc, 104 | threshold: matchThreshold, distance: matchDistance); 105 | } 106 | 107 | /// Compute a list of patches to turn text1 into text2. 108 | /// Use diffs if provided, otherwise compute it ourselves. 109 | /// 110 | /// There are four ways to call this function, depending on what data is 111 | /// available to the caller: 112 | /// 113 | /// * Method 1: 114 | /// [a] = text1, [opt_b] = text2 115 | /// * Method 2: 116 | /// [a] = diffs 117 | /// * Method 3 (optimal): 118 | /// [a] = text1, [opt_b] = diffs 119 | /// * Method 4 (deprecated, use method 3): 120 | /// [a] = text1, [opt_b] = text2, [opt_c] = diffs 121 | /// 122 | /// Returns a List of Patch objects. 123 | List patch(Object a, [Object? opt_b, Object? opt_c]) { 124 | return p.patchMake(a, 125 | b: opt_b, 126 | c: opt_c, 127 | diffTimeout: diffTimeout, 128 | diffEditCost: diffEditCost, 129 | deleteThreshold: patchDeleteThreshold, 130 | margin: patchMargin); 131 | } 132 | 133 | /// Merge a set of patches onto the text. Return a patched text, as well 134 | /// as an array of true/false values indicating which patches were applied. 135 | /// 136 | /// * [patches] is a List of Patch objects 137 | /// * [text] is the old text. 138 | /// 139 | /// Returns a two element List, containing the new text and a List of 140 | /// bool values. 141 | List patch_apply(List patches, String text) { 142 | return p.patchApply(patches, text, 143 | diffTimeout: diffTimeout, 144 | deleteThreshold: patchDeleteThreshold, 145 | margin: patchMargin); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/src/common.dart: -------------------------------------------------------------------------------- 1 | /// Copyright 2011 Google Inc. 2 | /// Copyright 2014 Boris Kaul 3 | /// http://github.com/localvoid/diff-match-patch 4 | /// 5 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 6 | /// you may not use this file except in compliance with the License. 7 | /// You may obtain a copy of the License at 8 | /// 9 | /// http://www.apache.org/licenses/LICENSE-2.0 10 | /// 11 | /// Unless required by applicable law or agreed to in writing, software 12 | /// distributed under the License is distributed on an 'AS IS' BASIS, 13 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | /// See the License for the specific language governing permissions and 15 | /// limitations under the License. 16 | 17 | library _common; 18 | 19 | const int BITS_PER_INT = 32; 20 | -------------------------------------------------------------------------------- /lib/src/diff.dart: -------------------------------------------------------------------------------- 1 | /// Copyright 2011 Google Inc. 2 | /// Copyright 2014 Boris Kaul 3 | /// http://github.com/localvoid/diff-match-patch 4 | /// 5 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 6 | /// you may not use this file except in compliance with the License. 7 | /// You may obtain a copy of the License at 8 | /// 9 | /// http://www.apache.org/licenses/LICENSE-2.0 10 | /// 11 | /// Unless required by applicable law or agreed to in writing, software 12 | /// distributed under the License is distributed on an 'AS IS' BASIS, 13 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | /// See the License for the specific language governing permissions and 15 | /// limitations under the License. 16 | 17 | library diff; 18 | 19 | import 'dart:collection'; 20 | import 'dart:math'; 21 | 22 | part 'package:diff_match_patch/src/diff/utils.dart'; 23 | part 'package:diff_match_patch/src/diff/diff.dart'; 24 | part 'package:diff_match_patch/src/diff/cleanup.dart'; 25 | part 'package:diff_match_patch/src/diff/half_match.dart'; 26 | part 'package:diff_match_patch/src/diff/delta.dart'; 27 | part 'package:diff_match_patch/src/diff/main.dart'; 28 | -------------------------------------------------------------------------------- /lib/src/diff/cleanup.dart: -------------------------------------------------------------------------------- 1 | /// Cleanup functions 2 | /// 3 | /// Copyright 2011 Google Inc. 4 | /// Copyright 2014 Boris Kaul 5 | /// http://github.com/localvoid/diff-match-patch 6 | /// 7 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 8 | /// you may not use this file except in compliance with the License. 9 | /// You may obtain a copy of the License at 10 | /// 11 | /// http://www.apache.org/licenses/LICENSE-2.0 12 | /// 13 | /// Unless required by applicable law or agreed to in writing, software 14 | /// distributed under the License is distributed on an 'AS IS' BASIS, 15 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | /// See the License for the specific language governing permissions and 17 | /// limitations under the License. 18 | 19 | part of diff; 20 | 21 | // Define some regex patterns for matching boundaries. 22 | RegExp _nonAlphaNumericRegex = RegExp(r'[^a-zA-Z0-9]'); 23 | RegExp _whitespaceRegex = RegExp(r'\s'); 24 | RegExp _linebreakRegex = RegExp(r'[\r\n]'); 25 | RegExp _blanklineEndRegex = RegExp(r'\n\r?\n$'); 26 | RegExp _blanklineStartRegex = RegExp(r'^\r?\n\r?\n'); 27 | 28 | /// Reduce the number of edits by eliminating semantically trivial equalities. 29 | /// 30 | /// [diffs] is a List of Diff objects. 31 | void cleanupSemantic(List diffs) { 32 | var changes = false; 33 | // Stack of indices where equalities are found. 34 | final equalities = []; 35 | // Always equal to diffs[equalities.last()].text 36 | String? lastequality; 37 | var pointer = 0; // Index of current position. 38 | // Number of characters that changed prior to the equality. 39 | var length_insertions1 = 0; 40 | var length_deletions1 = 0; 41 | // Number of characters that changed after the equality. 42 | var length_insertions2 = 0; 43 | var length_deletions2 = 0; 44 | while (pointer < diffs.length) { 45 | if (diffs[pointer].operation == DIFF_EQUAL) { 46 | // Equality found. 47 | equalities.add(pointer); 48 | length_insertions1 = length_insertions2; 49 | length_deletions1 = length_deletions2; 50 | length_insertions2 = 0; 51 | length_deletions2 = 0; 52 | lastequality = diffs[pointer].text; 53 | } else { 54 | // An insertion or deletion. 55 | if (diffs[pointer].operation == DIFF_INSERT) { 56 | length_insertions2 += diffs[pointer].text.length; 57 | } else { 58 | length_deletions2 += diffs[pointer].text.length; 59 | } 60 | // Eliminate an equality that is smaller or equal to the edits on both 61 | // sides of it. 62 | if (lastequality != null && 63 | (lastequality.length <= max(length_insertions1, length_deletions1)) && 64 | (lastequality.length <= max(length_insertions2, length_deletions2))) { 65 | // Duplicate record. 66 | diffs.insert(equalities.last, Diff(DIFF_DELETE, lastequality)); 67 | // Change second copy to insert. 68 | diffs[equalities.last + 1].operation = DIFF_INSERT; 69 | // Throw away the equality we just deleted. 70 | equalities.removeLast(); 71 | // Throw away the previous equality (it needs to be reevaluated). 72 | if (equalities.isNotEmpty) { 73 | equalities.removeLast(); 74 | } 75 | pointer = equalities.isEmpty ? -1 : equalities.last; 76 | length_insertions1 = 0; // Reset the counters. 77 | length_deletions1 = 0; 78 | length_insertions2 = 0; 79 | length_deletions2 = 0; 80 | lastequality = null; 81 | changes = true; 82 | } 83 | } 84 | pointer++; 85 | } 86 | 87 | // Normalize the diff. 88 | if (changes) { 89 | cleanupMerge(diffs); 90 | } 91 | cleanupSemanticLossless(diffs); 92 | 93 | // Find any overlaps between deletions and insertions. 94 | // e.g: abcxxxxxxdef 95 | // -> abcxxxdef 96 | // e.g: xxxabcdefxxx 97 | // -> defxxxabc 98 | // Only extract an overlap if it is as big as the edit ahead or behind it. 99 | pointer = 1; 100 | while (pointer < diffs.length) { 101 | if (diffs[pointer - 1].operation == DIFF_DELETE && 102 | diffs[pointer].operation == DIFF_INSERT) { 103 | var deletion = diffs[pointer - 1].text; 104 | var insertion = diffs[pointer].text; 105 | var overlap_length1 = commonOverlap(deletion, insertion); 106 | var overlap_length2 = commonOverlap(insertion, deletion); 107 | if (overlap_length1 >= overlap_length2) { 108 | if (overlap_length1 >= deletion.length / 2 || 109 | overlap_length1 >= insertion.length / 2) { 110 | // Overlap found. 111 | // Insert an equality and trim the surrounding edits. 112 | diffs.insert(pointer, 113 | Diff(DIFF_EQUAL, insertion.substring(0, overlap_length1))); 114 | diffs[pointer - 1].text = 115 | deletion.substring(0, deletion.length - overlap_length1); 116 | diffs[pointer + 1].text = insertion.substring(overlap_length1); 117 | pointer++; 118 | } 119 | } else { 120 | if (overlap_length2 >= deletion.length / 2 || 121 | overlap_length2 >= insertion.length / 2) { 122 | // Reverse overlap found. 123 | // Insert an equality and swap and trim the surrounding edits. 124 | diffs.insert(pointer, 125 | Diff(DIFF_EQUAL, deletion.substring(0, overlap_length2))); 126 | diffs[pointer - 1] = Diff(DIFF_INSERT, 127 | insertion.substring(0, insertion.length - overlap_length2)); 128 | diffs[pointer + 1] = 129 | Diff(DIFF_DELETE, deletion.substring(overlap_length2)); 130 | pointer++; 131 | } 132 | } 133 | pointer++; 134 | } 135 | pointer++; 136 | } 137 | } 138 | 139 | /// Look for single edits surrounded on both sides by equalities 140 | /// which can be shifted sideways to align the edit to a word boundary. 141 | /// 142 | /// e.g: The cat came. -> The cat came. 143 | /// 144 | /// [diffs] is a List of Diff objects. 145 | void cleanupSemanticLossless(List diffs) { 146 | /// Given two strings, compute a score representing whether the internal 147 | /// boundary falls on logical boundaries. 148 | /// Scores range from 6 (best) to 0 (worst). 149 | /// Closure, but does not reference any external variables. 150 | /// [one] the first string. 151 | /// [two] the second string. 152 | /// Returns the score. 153 | 154 | int _cleanupSemanticScore(String one, String two) { 155 | if (one.isEmpty || two.isEmpty) { 156 | // Edges are the best. 157 | return 6; 158 | } 159 | 160 | // Each port of this function behaves slightly differently due to 161 | // subtle differences in each language's definition of things like 162 | // 'whitespace'. Since this function's purpose is largely cosmetic, 163 | // the choice has been made to use each language's native features 164 | // rather than force total conformity. 165 | var char1 = one[one.length - 1]; 166 | var char2 = two[0]; 167 | var nonAlphaNumeric1 = char1.contains(_nonAlphaNumericRegex); 168 | var nonAlphaNumeric2 = char2.contains(_nonAlphaNumericRegex); 169 | var whitespace1 = nonAlphaNumeric1 && char1.contains(_whitespaceRegex); 170 | var whitespace2 = nonAlphaNumeric2 && char2.contains(_whitespaceRegex); 171 | var lineBreak1 = whitespace1 && char1.contains(_linebreakRegex); 172 | var lineBreak2 = whitespace2 && char2.contains(_linebreakRegex); 173 | var blankLine1 = lineBreak1 && one.contains(_blanklineEndRegex); 174 | var blankLine2 = lineBreak2 && two.contains(_blanklineStartRegex); 175 | 176 | if (blankLine1 || blankLine2) { 177 | // Five points for blank lines. 178 | return 5; 179 | } else if (lineBreak1 || lineBreak2) { 180 | // Four points for line breaks. 181 | return 4; 182 | } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { 183 | // Three points for end of sentences. 184 | return 3; 185 | } else if (whitespace1 || whitespace2) { 186 | // Two points for whitespace. 187 | return 2; 188 | } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { 189 | // One point for non-alphanumeric. 190 | return 1; 191 | } 192 | return 0; 193 | } 194 | 195 | var pointer = 1; 196 | // Intentionally ignore the first and last element (don't need checking). 197 | while (pointer < diffs.length - 1) { 198 | if (diffs[pointer - 1].operation == DIFF_EQUAL && 199 | diffs[pointer + 1].operation == DIFF_EQUAL) { 200 | // This is a single edit surrounded by equalities. 201 | var equality1 = diffs[pointer - 1].text; 202 | var edit = diffs[pointer].text; 203 | var equality2 = diffs[pointer + 1].text; 204 | 205 | // First, shift the edit as far left as possible. 206 | var commonOffset = commonSuffix(equality1, edit); 207 | if (commonOffset != 0) { 208 | var commonString = edit.substring(edit.length - commonOffset); 209 | equality1 = equality1.substring(0, equality1.length - commonOffset); 210 | edit = '$commonString${edit.substring(0, edit.length - commonOffset)}'; 211 | equality2 = '$commonString$equality2'; 212 | } 213 | 214 | // Second, step character by character right, looking for the best fit. 215 | var bestEquality1 = equality1; 216 | var bestEdit = edit; 217 | var bestEquality2 = equality2; 218 | var bestScore = _cleanupSemanticScore(equality1, edit) + 219 | _cleanupSemanticScore(edit, equality2); 220 | while ( 221 | edit.isNotEmpty && equality2.isNotEmpty && edit[0] == equality2[0]) { 222 | equality1 = '$equality1${edit[0]}'; 223 | edit = '${edit.substring(1)}${equality2[0]}'; 224 | equality2 = equality2.substring(1); 225 | var score = _cleanupSemanticScore(equality1, edit) + 226 | _cleanupSemanticScore(edit, equality2); 227 | // The >= encourages trailing rather than leading whitespace on edits. 228 | if (score >= bestScore) { 229 | bestScore = score; 230 | bestEquality1 = equality1; 231 | bestEdit = edit; 232 | bestEquality2 = equality2; 233 | } 234 | } 235 | 236 | if (diffs[pointer - 1].text != bestEquality1) { 237 | // We have an improvement, save it back to the diff. 238 | if (bestEquality1.isNotEmpty) { 239 | diffs[pointer - 1].text = bestEquality1; 240 | } else { 241 | diffs.removeRange(pointer - 1, pointer); 242 | pointer--; 243 | } 244 | diffs[pointer].text = bestEdit; 245 | if (bestEquality2.isNotEmpty) { 246 | diffs[pointer + 1].text = bestEquality2; 247 | } else { 248 | diffs.removeRange(pointer + 1, pointer + 2); 249 | pointer--; 250 | } 251 | } 252 | } 253 | pointer++; 254 | } 255 | } 256 | 257 | /// Reduce the number of edits by eliminating operationally trivial equalities. 258 | /// 259 | /// [diffs] is a List of Diff objects. 260 | void cleanupEfficiency(List diffs, int diffEditCost) { 261 | var changes = false; 262 | // Stack of indices where equalities are found. 263 | final equalities = []; 264 | // Always equal to diffs[equalities.last()].text 265 | String? lastequality; 266 | var pointer = 0; // Index of current position. 267 | // Is there an insertion operation before the last equality. 268 | var pre_ins = false; 269 | // Is there a deletion operation before the last equality. 270 | var pre_del = false; 271 | // Is there an insertion operation after the last equality. 272 | var post_ins = false; 273 | // Is there a deletion operation after the last equality. 274 | var post_del = false; 275 | while (pointer < diffs.length) { 276 | if (diffs[pointer].operation == DIFF_EQUAL) { 277 | // Equality found. 278 | if (diffs[pointer].text.length < diffEditCost && (post_ins || post_del)) { 279 | // Candidate found. 280 | equalities.add(pointer); 281 | pre_ins = post_ins; 282 | pre_del = post_del; 283 | lastequality = diffs[pointer].text; 284 | } else { 285 | // Not a candidate, and can never become one. 286 | equalities.clear(); 287 | lastequality = null; 288 | } 289 | post_ins = post_del = false; 290 | } else { 291 | // An insertion or deletion. 292 | if (diffs[pointer].operation == DIFF_DELETE) { 293 | post_del = true; 294 | } else { 295 | post_ins = true; 296 | } 297 | /* 298 | * Five types to be split: 299 | * ABXYCD 300 | * AXCD 301 | * ABXC 302 | * AXCD 303 | * ABXC 304 | */ 305 | if (lastequality != null && 306 | ((pre_ins && pre_del && post_ins && post_del) || 307 | ((lastequality.length < diffEditCost / 2) && 308 | ((pre_ins ? 1 : 0) + 309 | (pre_del ? 1 : 0) + 310 | (post_ins ? 1 : 0) + 311 | (post_del ? 1 : 0)) == 312 | 3))) { 313 | // Duplicate record. 314 | diffs.insert(equalities.last, Diff(DIFF_DELETE, lastequality)); 315 | // Change second copy to insert. 316 | diffs[equalities.last + 1].operation = DIFF_INSERT; 317 | equalities.removeLast(); // Throw away the equality we just deleted. 318 | lastequality = null; 319 | if (pre_ins && pre_del) { 320 | // No changes made which could affect previous entry, keep going. 321 | post_ins = post_del = true; 322 | equalities.clear(); 323 | } else { 324 | if (equalities.isNotEmpty) { 325 | equalities.removeLast(); 326 | } 327 | pointer = equalities.isEmpty ? -1 : equalities.last; 328 | post_ins = post_del = false; 329 | } 330 | changes = true; 331 | } 332 | } 333 | pointer++; 334 | } 335 | 336 | if (changes) { 337 | cleanupMerge(diffs); 338 | } 339 | } 340 | 341 | /// Reorder and merge like edit sections. Merge equalities. 342 | /// Any edit section can move as long as it doesn't cross an equality. 343 | /// 344 | /// [diffs] is a List of Diff objects. 345 | void cleanupMerge(List diffs) { 346 | diffs.add(Diff(DIFF_EQUAL, '')); // Add a dummy entry at the end. 347 | var pointer = 0; 348 | var count_delete = 0; 349 | var count_insert = 0; 350 | var text_delete = ''; 351 | var text_insert = ''; 352 | int commonlength; 353 | while (pointer < diffs.length) { 354 | switch (diffs[pointer].operation) { 355 | case DIFF_INSERT: 356 | count_insert++; 357 | text_insert = '$text_insert${diffs[pointer].text}'; 358 | pointer++; 359 | break; 360 | case DIFF_DELETE: 361 | count_delete++; 362 | text_delete = '$text_delete${diffs[pointer].text}'; 363 | pointer++; 364 | break; 365 | case DIFF_EQUAL: 366 | // Upon reaching an equality, check for prior redundancies. 367 | if (count_delete + count_insert > 1) { 368 | if (count_delete != 0 && count_insert != 0) { 369 | // Factor out any common prefixies. 370 | commonlength = commonPrefix(text_insert, text_delete); 371 | if (commonlength != 0) { 372 | if ((pointer - count_delete - count_insert) > 0 && 373 | diffs[pointer - count_delete - count_insert - 1].operation == 374 | DIFF_EQUAL) { 375 | final i = pointer - count_delete - count_insert - 1; 376 | diffs[i].text = '${diffs[i].text}' 377 | '${text_insert.substring(0, commonlength)}'; 378 | } else { 379 | diffs.insert(0, 380 | Diff(DIFF_EQUAL, text_insert.substring(0, commonlength))); 381 | pointer++; 382 | } 383 | text_insert = text_insert.substring(commonlength); 384 | text_delete = text_delete.substring(commonlength); 385 | } 386 | // Factor out any common suffixies. 387 | commonlength = commonSuffix(text_insert, text_delete); 388 | if (commonlength != 0) { 389 | diffs[pointer].text = 390 | '${text_insert.substring(text_insert.length - commonlength)}${diffs[pointer].text}'; 391 | text_insert = 392 | text_insert.substring(0, text_insert.length - commonlength); 393 | text_delete = 394 | text_delete.substring(0, text_delete.length - commonlength); 395 | } 396 | } 397 | // Delete the offending records and add the merged ones. 398 | if (count_delete == 0) { 399 | diffs.removeRange(pointer - count_insert, pointer); 400 | diffs.insert( 401 | pointer - count_insert, Diff(DIFF_INSERT, text_insert)); 402 | } else if (count_insert == 0) { 403 | diffs.removeRange(pointer - count_delete, pointer); 404 | diffs.insert( 405 | pointer - count_delete, Diff(DIFF_DELETE, text_delete)); 406 | } else { 407 | diffs.removeRange(pointer - count_delete - count_insert, pointer); 408 | diffs.insert(pointer - count_delete - count_insert, 409 | Diff(DIFF_INSERT, text_insert)); 410 | diffs.insert(pointer - count_delete - count_insert, 411 | Diff(DIFF_DELETE, text_delete)); 412 | } 413 | pointer = pointer - 414 | count_delete - 415 | count_insert + 416 | (count_delete == 0 ? 0 : 1) + 417 | (count_insert == 0 ? 0 : 1) + 418 | 1; 419 | } else if (pointer != 0 && diffs[pointer - 1].operation == DIFF_EQUAL) { 420 | // Merge this equality with the previous one. 421 | diffs[pointer - 1].text = 422 | '${diffs[pointer - 1].text}${diffs[pointer].text}'; 423 | diffs.removeRange(pointer, pointer + 1); 424 | } else { 425 | pointer++; 426 | } 427 | count_insert = 0; 428 | count_delete = 0; 429 | text_delete = ''; 430 | text_insert = ''; 431 | break; 432 | } 433 | } 434 | if (diffs.last.text.isEmpty) { 435 | diffs.removeLast(); // Remove the dummy entry at the end. 436 | } 437 | 438 | // Second pass: look for single edits surrounded on both sides by equalities 439 | // which can be shifted sideways to eliminate an equality. 440 | // e.g: ABAC -> ABAC 441 | var changes = false; 442 | pointer = 1; 443 | // Intentionally ignore the first and last element (don't need checking). 444 | while (pointer < diffs.length - 1) { 445 | if (diffs[pointer - 1].operation == DIFF_EQUAL && 446 | diffs[pointer + 1].operation == DIFF_EQUAL) { 447 | // This is a single edit surrounded by equalities. 448 | if (diffs[pointer].text.endsWith(diffs[pointer - 1].text)) { 449 | // Shift the edit over the previous equality. 450 | diffs[pointer].text = '${diffs[pointer - 1].text}' 451 | '${diffs[pointer].text.substring(0, diffs[pointer].text.length - diffs[pointer - 1].text.length)}'; 452 | diffs[pointer + 1].text = 453 | '${diffs[pointer - 1].text}${diffs[pointer + 1].text}'; 454 | diffs.removeRange(pointer - 1, pointer); 455 | changes = true; 456 | } else if (diffs[pointer].text.startsWith(diffs[pointer + 1].text)) { 457 | // Shift the edit over the next equality. 458 | diffs[pointer - 1].text = 459 | '${diffs[pointer - 1].text}${diffs[pointer + 1].text}'; 460 | diffs[pointer].text = 461 | '${diffs[pointer].text.substring(diffs[pointer + 1].text.length)}' 462 | '${diffs[pointer + 1].text}'; 463 | diffs.removeRange(pointer + 1, pointer + 2); 464 | changes = true; 465 | } 466 | } 467 | pointer++; 468 | } 469 | // If shifts were made, the diff needs reordering and another shift sweep. 470 | if (changes) { 471 | cleanupMerge(diffs); 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /lib/src/diff/delta.dart: -------------------------------------------------------------------------------- 1 | /// Delta functions 2 | /// 3 | /// Copyright 2011 Google Inc. 4 | /// Copyright 2014 Boris Kaul 5 | /// http://github.com/localvoid/diff-match-patch 6 | /// 7 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 8 | /// you may not use this file except in compliance with the License. 9 | /// You may obtain a copy of the License at 10 | /// 11 | /// http://www.apache.org/licenses/LICENSE-2.0 12 | /// 13 | /// Unless required by applicable law or agreed to in writing, software 14 | /// distributed under the License is distributed on an 'AS IS' BASIS, 15 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | /// See the License for the specific language governing permissions and 17 | /// limitations under the License. 18 | 19 | part of diff; 20 | 21 | /// Crush the diff into an encoded String which describes the operations 22 | /// required to transform text1 into text2. 23 | /// 24 | /// E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. 25 | /// 26 | /// Operations are tab-separated. Inserted text is escaped using %xx notation. 27 | /// 28 | /// [diffs] is a List of Diff objects. 29 | /// 30 | /// Returns the delta text. 31 | 32 | String toDelta(List diffs) { 33 | final text = StringBuffer(); 34 | for (var aDiff in diffs) { 35 | switch (aDiff.operation) { 36 | case DIFF_INSERT: 37 | text..write('+')..write(Uri.encodeFull(aDiff.text))..write('\t'); 38 | break; 39 | case DIFF_DELETE: 40 | text..write('-')..write(aDiff.text.length)..write('\t'); 41 | break; 42 | case DIFF_EQUAL: 43 | text..write('=')..write(aDiff.text.length)..write('\t'); 44 | break; 45 | } 46 | } 47 | var delta = text.toString(); 48 | if (delta.isNotEmpty) { 49 | // Strip off trailing tab character. 50 | delta = delta.substring(0, delta.length - 1); 51 | } 52 | return delta.replaceAll('%20', ' '); 53 | } 54 | 55 | /// Given the original [text1], and an encoded String which describes the 56 | /// operations required to transform [text1] into text2, compute the full diff. 57 | /// 58 | /// * [text1] is the source string for the diff. 59 | /// * [delta] is the delta text. 60 | /// 61 | /// Returns a List of Diff objects or null if invalid. 62 | /// 63 | /// Throws ArgumentError if invalid input. 64 | List fromDelta(String text1, String delta) { 65 | final diffs = []; 66 | var pointer = 0; // Cursor in text1 67 | final tokens = delta.split('\t'); 68 | for (var token in tokens) { 69 | if (token.isEmpty) { 70 | // Blank tokens are ok (from a trailing \t). 71 | continue; 72 | } 73 | // Each token begins with a one character parameter which specifies the 74 | // operation of this token (delete, insert, equality). 75 | var param = token.substring(1); 76 | switch (token[0]) { 77 | case '+': 78 | // decode would change all "+" to " " 79 | param = param.replaceAll('+', '%2B'); 80 | try { 81 | param = Uri.decodeFull(param); 82 | } on ArgumentError { 83 | // Malformed URI sequence. 84 | throw ArgumentError('Illegal escape in diff_fromDelta: $param'); 85 | } 86 | diffs.add(Diff(DIFF_INSERT, param)); 87 | break; 88 | case '-': 89 | // Fall through. 90 | case '=': 91 | int n; 92 | try { 93 | n = int.parse(param); 94 | } on FormatException { 95 | throw ArgumentError('Invalid number in diff_fromDelta: $param'); 96 | } 97 | if (n < 0) { 98 | throw ArgumentError('Negative number in diff_fromDelta: $param'); 99 | } 100 | String text; 101 | try { 102 | text = text1.substring(pointer, pointer += n); 103 | } on RangeError { 104 | throw ArgumentError('Delta length ($pointer)' 105 | ' larger than source text length (${text1.length}).'); 106 | } 107 | if (token[0] == '=') { 108 | diffs.add(Diff(DIFF_EQUAL, text)); 109 | } else { 110 | diffs.add(Diff(DIFF_DELETE, text)); 111 | } 112 | break; 113 | default: 114 | // Anything else is an error. 115 | throw ArgumentError( 116 | 'Invalid diff operation in diff_fromDelta: ${token[0]}'); 117 | } 118 | } 119 | if (pointer != text1.length) { 120 | throw ArgumentError('Delta length ($pointer)' 121 | ' smaller than source text length (${text1.length}).'); 122 | } 123 | return diffs; 124 | } 125 | -------------------------------------------------------------------------------- /lib/src/diff/diff.dart: -------------------------------------------------------------------------------- 1 | /// Diff class 2 | /// 3 | /// Copyright 2011 Google Inc. 4 | /// Copyright 2014 Boris Kaul 5 | /// http://github.com/localvoid/diff-match-patch 6 | /// 7 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 8 | /// you may not use this file except in compliance with the License. 9 | /// You may obtain a copy of the License at 10 | /// 11 | /// http://www.apache.org/licenses/LICENSE-2.0 12 | /// 13 | /// Unless required by applicable law or agreed to in writing, software 14 | /// distributed under the License is distributed on an 'AS IS' BASIS, 15 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | /// See the License for the specific language governing permissions and 17 | /// limitations under the License. 18 | 19 | part of diff; 20 | 21 | /// The data structure representing a diff is a List of Diff objects: 22 | /// 23 | /// [Diff(DIFF_DELETE, 'Hello'), 24 | /// Diff(DIFF_INSERT, 'Goodbye'), 25 | /// Diff(DIFF_EQUAL, ' world.')] 26 | /// 27 | /// which means: delete 'Hello', add 'Goodbye' and keep ' world.' 28 | 29 | const DIFF_DELETE = -1; 30 | const DIFF_INSERT = 1; 31 | const DIFF_EQUAL = 0; 32 | 33 | /// Class representing one diff operation. 34 | class Diff { 35 | /// One of: [DIFF_INSERT], [DIFF_DELETE] or [DIFF_EQUAL]. 36 | int operation; 37 | 38 | /// The text associated with this diff operation. 39 | String text; 40 | 41 | /// Constructor. Initializes the diff with the provided values. 42 | /// 43 | /// * [operation] is one of [DIFF_INSERT], [DIFF_DELETE] or [DIFF_EQUAL]. 44 | /// * [text] is the text being applied. 45 | Diff(this.operation, this.text); 46 | 47 | /// Display a human-readable version of this Diff. 48 | /// 49 | /// Returns a text version. 50 | @override 51 | String toString() { 52 | var prettyText = text.replaceAll('\n', '\u00b6'); 53 | return 'Diff($operation,"$prettyText")'; 54 | } 55 | 56 | /// Is this Diff equivalent to another Diff? 57 | /// 58 | /// [other] is another Diff to compare against. 59 | /// 60 | /// Returns true or false. 61 | @override 62 | bool operator ==(Object other) => 63 | identical(this, other) || 64 | other is Diff && 65 | runtimeType == other.runtimeType && 66 | operation == other.operation && 67 | text == other.text; 68 | 69 | @override 70 | int get hashCode => operation.hashCode ^ text.hashCode; 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/diff/half_match.dart: -------------------------------------------------------------------------------- 1 | /// Half Match functions 2 | /// 3 | /// Copyright 2011 Google Inc. 4 | /// Copyright 2014 Boris Kaul 5 | /// http://github.com/localvoid/diff-match-patch 6 | /// 7 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 8 | /// you may not use this file except in compliance with the License. 9 | /// You may obtain a copy of the License at 10 | /// 11 | /// http://www.apache.org/licenses/LICENSE-2.0 12 | /// 13 | /// Unless required by applicable law or agreed to in writing, software 14 | /// distributed under the License is distributed on an 'AS IS' BASIS, 15 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | /// See the License for the specific language governing permissions and 17 | /// limitations under the License. 18 | 19 | part of diff; 20 | 21 | /// Do the two texts share a substring which is at least half the length of 22 | /// the longer text? 23 | /// 24 | /// This speedup can produce non-minimal diffs. 25 | /// 26 | /// * [text1] is the first string. 27 | /// * [text2] is the second string. 28 | /// 29 | /// Returns a five element List of Strings, containing the prefix of [text1], 30 | /// the suffix of [text1], the prefix of [text2], the suffix of [text2] and the 31 | /// common middle. Or null if there was no match. 32 | List? diffHalfMatch(String text1, String text2, double timeout) { 33 | if (timeout <= 0) { 34 | // Don't risk returning a non-optimal diff if we have unlimited time. 35 | return null; 36 | } 37 | final longtext = text1.length > text2.length ? text1 : text2; 38 | final shorttext = text1.length > text2.length ? text2 : text1; 39 | if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { 40 | return null; // Pointless. 41 | } 42 | 43 | // First check if the second quarter is the seed for a half-match. 44 | final hm1 = _diffHalfMatchI( 45 | longtext, shorttext, ((longtext.length + 3) / 4).ceil().toInt()); 46 | // Check again based on the third quarter. 47 | final hm2 = _diffHalfMatchI( 48 | longtext, shorttext, ((longtext.length + 1) / 2).ceil().toInt()); 49 | List? hm; 50 | if (hm1 == null && hm2 == null) { 51 | return null; 52 | } else if (hm2 == null) { 53 | hm = hm1; 54 | } else if (hm1 == null) { 55 | hm = hm2; 56 | } else { 57 | // Both matched. Select the longest. 58 | hm = hm1[4].length > hm2[4].length ? hm1 : hm2; 59 | } 60 | 61 | // A half-match was found, sort out the return data. 62 | if (text1.length > text2.length) { 63 | return hm; 64 | //return [hm[0], hm[1], hm[2], hm[3], hm[4]]; 65 | } else { 66 | return [hm![2], hm[3], hm[0], hm[1], hm[4]]; 67 | } 68 | } 69 | 70 | /// Does a substring of [shorttext] exist within [longtext] such that the 71 | /// substring is at least half the length of [longtext]? 72 | /// 73 | /// * [longtext] is the longer string. 74 | /// * [shorttext is the shorter string. 75 | /// * [i] Start index of quarter length substring within longtext. 76 | /// 77 | /// Returns a five element String array, containing the prefix of [longtext], 78 | /// the suffix of [longtext], the prefix of [shorttext], the suffix of 79 | /// [shorttext] and the common middle. Or null if there was no match. 80 | List? _diffHalfMatchI(String longtext, String shorttext, int i) { 81 | // Start with a 1/4 length substring at position i as a seed. 82 | final seed = longtext.substring(i, i + (longtext.length / 4).floor().toInt()); 83 | var j = -1; 84 | var best_common = ''; 85 | var best_longtext_a = '', best_longtext_b = ''; 86 | var best_shorttext_a = '', best_shorttext_b = ''; 87 | while ((j = shorttext.indexOf(seed, j + 1)) != -1) { 88 | var prefixLength = 89 | commonPrefix(longtext.substring(i), shorttext.substring(j)); 90 | var suffixLength = 91 | commonSuffix(longtext.substring(0, i), shorttext.substring(0, j)); 92 | if (best_common.length < suffixLength + prefixLength) { 93 | best_common = '${shorttext.substring(j - suffixLength, j)}' 94 | '${shorttext.substring(j, j + prefixLength)}'; 95 | best_longtext_a = longtext.substring(0, i - suffixLength); 96 | best_longtext_b = longtext.substring(i + prefixLength); 97 | best_shorttext_a = shorttext.substring(0, j - suffixLength); 98 | best_shorttext_b = shorttext.substring(j + prefixLength); 99 | } 100 | } 101 | if (best_common.length * 2 >= longtext.length) { 102 | return [ 103 | best_longtext_a, 104 | best_longtext_b, 105 | best_shorttext_a, 106 | best_shorttext_b, 107 | best_common 108 | ]; 109 | } else { 110 | return null; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/src/diff/main.dart: -------------------------------------------------------------------------------- 1 | /// Main functions 2 | /// 3 | /// Copyright 2011 Google Inc. 4 | /// Copyright 2014 Boris Kaul 5 | /// http://github.com/localvoid/diff-match-patch 6 | /// 7 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 8 | /// you may not use this file except in compliance with the License. 9 | /// You may obtain a copy of the License at 10 | /// 11 | /// http://www.apache.org/licenses/LICENSE-2.0 12 | /// 13 | /// Unless required by applicable law or agreed to in writing, software 14 | /// distributed under the License is distributed on an 'AS IS' BASIS, 15 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | /// See the License for the specific language governing permissions and 17 | /// limitations under the License. 18 | 19 | part of diff; 20 | 21 | /// Find the differences between two texts. Simplifies the problem by 22 | /// stripping any common prefix or suffix off the texts before diffing. 23 | /// 24 | /// * [text1] is the old string to be diffed. 25 | /// * [text2] is the new string to be diffed. 26 | /// * [timeout] is an optional number of seconds to map a diff before giving up 27 | /// (0 for infinity). 28 | /// * [checklines] is an optional speedup flag. If false, then don't 29 | /// run a line-level diff first to identify the changed areas. 30 | /// Defaults to true, which does a faster, slightly less optimal diff. 31 | /// * [deadline] is an optional time when the diff should be complete by. Used 32 | /// internally for recursive calls. Users should set [diffTimeout] instead. 33 | /// 34 | /// Returns a List of Diff objects. 35 | List diff(String text1, String text2, 36 | {double timeout = 1.0, bool checklines = true, DateTime? deadline}) { 37 | // Set a deadline by which time the diff must be complete. 38 | if (deadline == null) { 39 | deadline = DateTime.now(); 40 | if (timeout <= 0) { 41 | // One year should be sufficient for 'infinity'. 42 | deadline = deadline.add(Duration(days: 365)); 43 | } else { 44 | deadline = deadline.add(Duration(milliseconds: (timeout * 1000).toInt())); 45 | } 46 | } 47 | 48 | // Check for equality (speedup). 49 | List diffs; 50 | if (text1 == text2) { 51 | diffs = []; 52 | if (text1.isNotEmpty) { 53 | diffs.add(Diff(DIFF_EQUAL, text1)); 54 | } 55 | return diffs; 56 | } 57 | 58 | // Trim off common prefix (speedup). 59 | var commonlength = commonPrefix(text1, text2); 60 | var commonprefix = text1.substring(0, commonlength); 61 | text1 = text1.substring(commonlength); 62 | text2 = text2.substring(commonlength); 63 | 64 | // Trim off common suffix (speedup). 65 | commonlength = commonSuffix(text1, text2); 66 | var commonsuffix = text1.substring(text1.length - commonlength); 67 | text1 = text1.substring(0, text1.length - commonlength); 68 | text2 = text2.substring(0, text2.length - commonlength); 69 | 70 | // Compute the diff on the middle block. 71 | diffs = _diffCompute(text1, text2, timeout, checklines, deadline); 72 | 73 | // Restore the prefix and suffix. 74 | if (commonprefix.isNotEmpty) { 75 | diffs.insert(0, Diff(DIFF_EQUAL, commonprefix)); 76 | } 77 | if (commonsuffix.isNotEmpty) { 78 | diffs.add(Diff(DIFF_EQUAL, commonsuffix)); 79 | } 80 | 81 | cleanupMerge(diffs); 82 | return diffs; 83 | } 84 | 85 | /// Find the differences between two texts. Assumes that the texts do not 86 | /// have any common prefix or suffix. 87 | /// 88 | /// * [text1] is the old string to be diffed. 89 | /// * [text2] is the new string to be diffed. 90 | /// * [timeout] is a number of seconds to map a diff before giving up 91 | /// (0 for infinity). 92 | /// * [checklines] is a speedup flag. If false, then don't run a 93 | /// line-level diff first to identify the changed areas. 94 | /// If true, then run a faster slightly less optimal diff. 95 | /// * [deadline] is the time when the diff should be complete by. 96 | /// 97 | /// Returns a List of Diff objects. 98 | List _diffCompute(String text1, String text2, double timeout, 99 | bool checklines, DateTime? deadline) { 100 | var diffs = []; 101 | 102 | if (text1.isEmpty) { 103 | // Just add some text (speedup). 104 | diffs.add(Diff(DIFF_INSERT, text2)); 105 | return diffs; 106 | } 107 | 108 | if (text2.isEmpty) { 109 | // Just delete some text (speedup). 110 | diffs.add(Diff(DIFF_DELETE, text1)); 111 | return diffs; 112 | } 113 | 114 | var longtext = text1.length > text2.length ? text1 : text2; 115 | var shorttext = text1.length > text2.length ? text2 : text1; 116 | var i = longtext.indexOf(shorttext); 117 | if (i != -1) { 118 | // Shorter text is inside the longer text (speedup). 119 | var op = (text1.length > text2.length) ? DIFF_DELETE : DIFF_INSERT; 120 | diffs.add(Diff(op, longtext.substring(0, i))); 121 | diffs.add(Diff(DIFF_EQUAL, shorttext)); 122 | diffs.add(Diff(op, longtext.substring(i + shorttext.length))); 123 | return diffs; 124 | } 125 | 126 | if (shorttext.length == 1) { 127 | // Single character string. 128 | // After the previous speedup, the character can't be an equality. 129 | diffs.add(Diff(DIFF_DELETE, text1)); 130 | diffs.add(Diff(DIFF_INSERT, text2)); 131 | return diffs; 132 | } 133 | 134 | // Check to see if the problem can be split in two. 135 | final hm = diffHalfMatch(text1, text2, timeout); 136 | if (hm != null) { 137 | // A half-match was found, sort out the return data. 138 | final text1_a = hm[0]; 139 | final text1_b = hm[1]; 140 | final text2_a = hm[2]; 141 | final text2_b = hm[3]; 142 | final mid_common = hm[4]; 143 | // Send both pairs off for separate processing. 144 | final diffs_a = diff(text1_a, text2_a, 145 | timeout: timeout, checklines: checklines, deadline: deadline); 146 | final diffs_b = diff(text1_b, text2_b, 147 | timeout: timeout, checklines: checklines, deadline: deadline); 148 | // Merge the results. 149 | diffs = diffs_a; 150 | diffs.add(Diff(DIFF_EQUAL, mid_common)); 151 | diffs.addAll(diffs_b); 152 | return diffs; 153 | } 154 | 155 | if (checklines && text1.length > 100 && text2.length > 100) { 156 | return _diffLineMode(text1, text2, timeout, deadline); 157 | } 158 | 159 | return diffBisect(text1, text2, timeout, deadline); 160 | } 161 | 162 | /// Do a quick line-level diff on both strings, then rediff the parts for 163 | /// greater accuracy. 164 | /// This speedup can produce non-minimal diffs. 165 | /// 166 | /// * [text1] is the old string to be diffed. 167 | /// * [text2] is the new string to be diffed. 168 | /// * [timeout] is a number of seconds to map a diff before giving up 169 | /// (0 for infinity). 170 | /// * [deadline] is the time when the diff should be complete by. 171 | /// 172 | /// Returns a List of Diff objects. 173 | List _diffLineMode( 174 | String text1, String text2, double timeout, DateTime? deadline) { 175 | // Scan the text on a line-by-line basis first. 176 | final a = linesToChars(text1, text2); 177 | text1 = a['chars1'] as String; 178 | text2 = a['chars2'] as String; 179 | final linearray = a['lineArray'] as List? ?? []; 180 | 181 | final diffs = diff(text1, text2, 182 | timeout: timeout, checklines: false, deadline: deadline); 183 | 184 | // Convert the diff back to original text. 185 | charsToLines(diffs, linearray); 186 | // Eliminate freak matches (e.g. blank lines) 187 | cleanupSemantic(diffs); 188 | 189 | // Rediff any replacement blocks, this time character-by-character. 190 | // Add a dummy entry at the end. 191 | diffs.add(Diff(DIFF_EQUAL, '')); 192 | var pointer = 0; 193 | var count_delete = 0; 194 | var count_insert = 0; 195 | final text_delete = StringBuffer(); 196 | final text_insert = StringBuffer(); 197 | while (pointer < diffs.length) { 198 | switch (diffs[pointer].operation) { 199 | case DIFF_INSERT: 200 | count_insert++; 201 | text_insert.write(diffs[pointer].text); 202 | break; 203 | case DIFF_DELETE: 204 | count_delete++; 205 | text_delete.write(diffs[pointer].text); 206 | break; 207 | case DIFF_EQUAL: 208 | // Upon reaching an equality, check for prior redundancies. 209 | if (count_delete >= 1 && count_insert >= 1) { 210 | // Delete the offending records and add the merged ones. 211 | diffs.removeRange(pointer - count_delete - count_insert, pointer); 212 | pointer = pointer - count_delete - count_insert; 213 | final a = diff(text_delete.toString(), text_insert.toString(), 214 | timeout: timeout, checklines: false, deadline: deadline); 215 | for (var j = a.length - 1; j >= 0; j--) { 216 | diffs.insert(pointer, a[j]); 217 | } 218 | pointer = pointer + a.length; 219 | } 220 | count_insert = 0; 221 | count_delete = 0; 222 | text_delete.clear(); 223 | text_insert.clear(); 224 | break; 225 | } 226 | pointer++; 227 | } 228 | diffs.removeLast(); // Remove the dummy entry at the end. 229 | 230 | return diffs; 231 | } 232 | 233 | /// Find the 'middle snake' of a diff, split the problem in two 234 | /// and return the recursively constructed diff. 235 | /// 236 | /// See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. 237 | /// 238 | /// * [text1] is the old string to be diffed. 239 | /// * [text2] is the new string to be diffed. 240 | /// * [timeout] is a number of seconds to map a diff before giving up 241 | /// (0 for infinity). 242 | /// * [deadline] is the time at which to bail if not yet complete. 243 | /// 244 | /// Returns a List of Diff objects. 245 | List diffBisect( 246 | String text1, String text2, double timeout, DateTime? deadline) { 247 | // Cache the text lengths to prevent multiple calls. 248 | final text1_length = text1.length; 249 | final text2_length = text2.length; 250 | final max_d = (text1_length + text2_length + 1) ~/ 2; 251 | final v_offset = max_d; 252 | final v_length = 2 * max_d; 253 | final v1 = List.filled(v_length, 0); 254 | final v2 = List.filled(v_length, 0); 255 | for (var x = 0; x < v_length; x++) { 256 | v1[x] = -1; 257 | v2[x] = -1; 258 | } 259 | v1[v_offset + 1] = 0; 260 | v2[v_offset + 1] = 0; 261 | final delta = text1_length - text2_length; 262 | // If the total number of characters is odd, then the front path will 263 | // collide with the reverse path. 264 | final front = (delta % 2 != 0); 265 | // Offsets for start and end of k loop. 266 | // Prevents mapping of space beyond the grid. 267 | var k1start = 0; 268 | var k1end = 0; 269 | var k2start = 0; 270 | var k2end = 0; 271 | for (var d = 0; d < max_d; d++) { 272 | // Bail out if deadline is reached. 273 | if (deadline != null && (DateTime.now()).compareTo(deadline) == 1) { 274 | break; 275 | } 276 | 277 | // Walk the front path one step. 278 | for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { 279 | var k1_offset = v_offset + k1; 280 | var x1 = 0; 281 | if (k1 == -d || k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1]) { 282 | x1 = v1[k1_offset + 1]; 283 | } else { 284 | x1 = v1[k1_offset - 1] + 1; 285 | } 286 | var y1 = x1 - k1; 287 | while (x1 < text1_length && y1 < text2_length && text1[x1] == text2[y1]) { 288 | x1++; 289 | y1++; 290 | } 291 | v1[k1_offset] = x1; 292 | if (x1 > text1_length) { 293 | // Ran off the right of the graph. 294 | k1end += 2; 295 | } else if (y1 > text2_length) { 296 | // Ran off the bottom of the graph. 297 | k1start += 2; 298 | } else if (front) { 299 | var k2_offset = v_offset + delta - k1; 300 | if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { 301 | // Mirror x2 onto top-left coordinate system. 302 | var x2 = text1_length - v2[k2_offset]; 303 | if (x1 >= x2) { 304 | // Overlap detected. 305 | return _diffBisectSplit(text1, text2, x1, y1, timeout, deadline); 306 | } 307 | } 308 | } 309 | } 310 | 311 | // Walk the reverse path one step. 312 | for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { 313 | var k2_offset = v_offset + k2; 314 | var x2 = 0; 315 | if (k2 == -d || k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1]) { 316 | x2 = v2[k2_offset + 1]; 317 | } else { 318 | x2 = v2[k2_offset - 1] + 1; 319 | } 320 | var y2 = x2 - k2; 321 | while (x2 < text1_length && 322 | y2 < text2_length && 323 | text1[text1_length - x2 - 1] == text2[text2_length - y2 - 1]) { 324 | x2++; 325 | y2++; 326 | } 327 | v2[k2_offset] = x2; 328 | if (x2 > text1_length) { 329 | // Ran off the left of the graph. 330 | k2end += 2; 331 | } else if (y2 > text2_length) { 332 | // Ran off the top of the graph. 333 | k2start += 2; 334 | } else if (!front) { 335 | var k1_offset = v_offset + delta - k2; 336 | if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { 337 | var x1 = v1[k1_offset]; 338 | var y1 = v_offset + x1 - k1_offset; 339 | // Mirror x2 onto top-left coordinate system. 340 | x2 = text1_length - x2; 341 | if (x1 >= x2) { 342 | // Overlap detected. 343 | return _diffBisectSplit(text1, text2, x1, y1, timeout, deadline); 344 | } 345 | } 346 | } 347 | } 348 | } 349 | // Diff took too long and hit the deadline or 350 | // number of diffs equals number of characters, no commonality at all. 351 | return [Diff(DIFF_DELETE, text1), Diff(DIFF_INSERT, text2)]; 352 | } 353 | 354 | /// Given the location of the 'middle snake', split the diff in two parts 355 | /// and recurse. 356 | /// 357 | /// * [text1] is the old string to be diffed. 358 | /// * [text2] is the new string to be diffed. 359 | /// * [x] is the index of split point in text1. 360 | /// * [y] is the index of split point in text2. 361 | /// * [timeout] is a number of seconds to map a diff before giving up 362 | /// (0 for infinity). 363 | /// * [deadline] is the time at which to bail if not yet complete. 364 | /// 365 | /// Returns a List of Diff objects. 366 | List _diffBisectSplit(String text1, String text2, int x, int y, 367 | double timeout, DateTime? deadline) { 368 | final text1a = text1.substring(0, x); 369 | final text2a = text2.substring(0, y); 370 | final text1b = text1.substring(x); 371 | final text2b = text2.substring(y); 372 | 373 | // Compute both diffs serially. 374 | final diffs = diff(text1a, text2a, 375 | timeout: timeout, checklines: false, deadline: deadline); 376 | final diffsb = diff(text1b, text2b, 377 | timeout: timeout, checklines: false, deadline: deadline); 378 | 379 | diffs.addAll(diffsb); 380 | return diffs; 381 | } 382 | -------------------------------------------------------------------------------- /lib/src/diff/utils.dart: -------------------------------------------------------------------------------- 1 | /// Misc functions 2 | /// 3 | /// Copyright 2011 Google Inc. 4 | /// Copyright 2014 Boris Kaul 5 | /// http://github.com/localvoid/diff-match-patch 6 | /// 7 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 8 | /// you may not use this file except in compliance with the License. 9 | /// You may obtain a copy of the License at 10 | /// 11 | /// http://www.apache.org/licenses/LICENSE-2.0 12 | /// 13 | /// Unless required by applicable law or agreed to in writing, software 14 | /// distributed under the License is distributed on an 'AS IS' BASIS, 15 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | /// See the License for the specific language governing permissions and 17 | /// limitations under the License. 18 | 19 | part of diff; 20 | 21 | /// Split a text into a list of strings. Reduce the texts to a string of 22 | /// hashes where each Unicode character represents one line. 23 | /// 24 | /// * [text] is the string to encode. 25 | /// * [lineArray] is a List of unique strings. 26 | /// * [lineHash] is a Map of strings to indices. 27 | /// 28 | /// Returns an encoded string. 29 | String _linesToCharsMunge( 30 | String text, List lineArray, Map lineHash) { 31 | var lineStart = 0; 32 | var lineEnd = -1; 33 | String line; 34 | final chars = StringBuffer(); 35 | // Walk the text, pulling out a substring for each line. 36 | // text.split('\n') would would temporarily double our memory footprint. 37 | // Modifying text would create many large strings to garbage collect. 38 | while (lineEnd < text.length - 1) { 39 | lineEnd = text.indexOf('\n', lineStart); 40 | if (lineEnd == -1) { 41 | lineEnd = text.length - 1; 42 | } 43 | line = text.substring(lineStart, lineEnd + 1); 44 | lineStart = lineEnd + 1; 45 | 46 | if (lineHash.containsKey(line)) { 47 | chars.write(String.fromCharCodes([lineHash[line]!])); 48 | } else { 49 | lineArray.add(line); 50 | lineHash[line] = lineArray.length - 1; 51 | chars.write(String.fromCharCodes([lineArray.length - 1])); 52 | } 53 | } 54 | return chars.toString(); 55 | } 56 | 57 | /// Split two texts into a list of strings. Reduce the texts to a string of 58 | /// hashes where each Unicode character represents one line. 59 | /// 60 | /// * [text1] is the first string. 61 | /// * [text2] is the second string. 62 | /// 63 | /// Returns a Map containing the encoded [text1], the encoded [text2] and 64 | /// the List of unique strings. The zeroth element of the List of 65 | /// unique strings is intentionally blank. 66 | Map linesToChars(String text1, String text2) { 67 | final lineArray = []; 68 | final lineHash = HashMap(); 69 | // e.g. linearray[4] == 'Hello\n' 70 | // e.g. linehash['Hello\n'] == 4 71 | 72 | // '\x00' is a valid character, but various debuggers don't like it. 73 | // So we'll insert a junk entry to avoid generating a null character. 74 | lineArray.add(''); 75 | 76 | var chars1 = _linesToCharsMunge(text1, lineArray, lineHash); 77 | var chars2 = _linesToCharsMunge(text2, lineArray, lineHash); 78 | return { 79 | 'chars1': chars1, 80 | 'chars2': chars2, 81 | 'lineArray': lineArray 82 | }; 83 | } 84 | 85 | /// Rehydrate the text in a diff from a string of line hashes to real lines of 86 | /// text. 87 | /// 88 | /// * [diffs] is a List of Diff objects. 89 | /// * [lineArray] is a List of unique strings. 90 | void charsToLines(List diffs, List lineArray) { 91 | final text = StringBuffer(); 92 | for (var diff in diffs) { 93 | for (var y = 0; y < diff.text.length; y++) { 94 | text.write(lineArray[diff.text.codeUnitAt(y)]); 95 | } 96 | diff.text = text.toString(); 97 | text.clear(); 98 | } 99 | } 100 | 101 | /// Determine the common prefix of two strings 102 | /// 103 | /// * [text1] is the first string. 104 | /// * [text2] is the second string. 105 | /// 106 | /// Returns the number of characters common to the start of each string. 107 | int commonPrefix(String text1, String text2) { 108 | // TODO: Once Dart's performance stabilizes, determine if linear or binary 109 | // search is better. 110 | // Performance analysis: http://neil.fraser.name/news/2007/10/09/ 111 | final n = min(text1.length, text2.length); 112 | for (var i = 0; i < n; i++) { 113 | if (text1[i] != text2[i]) { 114 | return i; 115 | } 116 | } 117 | return n; 118 | } 119 | 120 | /// Determine the common suffix of two strings 121 | /// 122 | /// * [text1] is the first string. 123 | /// * [text2] is the second string. 124 | /// 125 | /// Returns the number of characters common to the end of each string. 126 | int commonSuffix(String text1, String text2) { 127 | // TODO: Once Dart's performance stabilizes, determine if linear or binary 128 | // search is better. 129 | // Performance analysis: http://neil.fraser.name/news/2007/10/09/ 130 | final text1_length = text1.length; 131 | final text2_length = text2.length; 132 | final n = min(text1_length, text2_length); 133 | for (var i = 1; i <= n; i++) { 134 | if (text1[text1_length - i] != text2[text2_length - i]) { 135 | return i - 1; 136 | } 137 | } 138 | return n; 139 | } 140 | 141 | /// Determine if the suffix of one string is the prefix of another. 142 | /// 143 | /// * [text1] is the first string. 144 | /// * [text2] is the second string. 145 | /// 146 | /// Returns the number of characters common to the end of the first 147 | /// string and the start of the second string. 148 | int commonOverlap(String text1, String text2) { 149 | // Eliminate the null case. 150 | if (text1.isEmpty || text2.isEmpty) { 151 | return 0; 152 | } 153 | // Cache the text lengths to prevent multiple calls. 154 | final text1_length = text1.length; 155 | final text2_length = text2.length; 156 | // Truncate the longer string. 157 | if (text1_length > text2_length) { 158 | text1 = text1.substring(text1_length - text2_length); 159 | } else if (text1_length < text2_length) { 160 | text2 = text2.substring(0, text1_length); 161 | } 162 | final text_length = min(text1_length, text2_length); 163 | // Quick check for the worst case. 164 | if (text1 == text2) { 165 | return text_length; 166 | } 167 | 168 | // Start by looking for a single character match 169 | // and increase length until no match is found. 170 | // Performance analysis: http://neil.fraser.name/news/2010/11/04/ 171 | var best = 0; 172 | var length = 1; 173 | while (true) { 174 | var pattern = text1.substring(text_length - length); 175 | var found = text2.indexOf(pattern); 176 | if (found == -1) { 177 | return best; 178 | } 179 | length += found; 180 | if (found == 0 || 181 | text1.substring(text_length - length) == text2.substring(0, length)) { 182 | best = length; 183 | length++; 184 | } 185 | } 186 | } 187 | 188 | /// Compute the Levenshtein distance; the number of inserted, deleted or 189 | /// substituted characters. 190 | /// 191 | /// [diffs] is a List of Diff objects. 192 | /// 193 | /// Returns the number of changes. 194 | int levenshtein(List diffs) { 195 | var levenshtein = 0; 196 | var insertions = 0; 197 | var deletions = 0; 198 | for (var aDiff in diffs) { 199 | switch (aDiff.operation) { 200 | case DIFF_INSERT: 201 | insertions += aDiff.text.length; 202 | break; 203 | case DIFF_DELETE: 204 | deletions += aDiff.text.length; 205 | break; 206 | case DIFF_EQUAL: 207 | // A deletion and an insertion is one substitution. 208 | levenshtein += max(insertions, deletions); 209 | insertions = 0; 210 | deletions = 0; 211 | break; 212 | } 213 | } 214 | levenshtein += max(insertions, deletions); 215 | return levenshtein; 216 | } 217 | 218 | /// [loc] is a location in text1, compute and return the equivalent location in 219 | /// text2. 220 | /// 221 | /// e.g. "The cat" vs "The big cat", 1->1, 5->8 222 | /// 223 | /// * [diffs] is a List of Diff objects. 224 | /// * [loc] is the location within text1. 225 | /// 226 | /// Returns the location within text2. 227 | int diffXIndex(List diffs, int loc) { 228 | var chars1 = 0; 229 | var chars2 = 0; 230 | var last_chars1 = 0; 231 | var last_chars2 = 0; 232 | Diff? lastDiff; 233 | for (var aDiff in diffs) { 234 | if (aDiff.operation != DIFF_INSERT) { 235 | // Equality or deletion. 236 | chars1 += aDiff.text.length; 237 | } 238 | if (aDiff.operation != DIFF_DELETE) { 239 | // Equality or insertion. 240 | chars2 += aDiff.text.length; 241 | } 242 | if (chars1 > loc) { 243 | // Overshot the location. 244 | lastDiff = aDiff; 245 | break; 246 | } 247 | last_chars1 = chars1; 248 | last_chars2 = chars2; 249 | } 250 | if (lastDiff != null && lastDiff.operation == DIFF_DELETE) { 251 | // The location was deleted. 252 | return last_chars2; 253 | } 254 | // Add the remaining character length. 255 | return last_chars2 + (loc - last_chars1); 256 | } 257 | 258 | /// Compute and return the source text (all equalities and deletions). 259 | /// 260 | /// [diffs] is a List of Diff objects. 261 | /// 262 | /// Returns the source text. 263 | String diffText1(List diffs) { 264 | final text = StringBuffer(); 265 | for (var aDiff in diffs) { 266 | if (aDiff.operation != DIFF_INSERT) { 267 | text.write(aDiff.text); 268 | } 269 | } 270 | return text.toString(); 271 | } 272 | 273 | /// Compute and return the destination text (all equalities and insertions). 274 | /// 275 | /// [diffs] is a List of Diff objects. 276 | /// 277 | /// Returns the destination text. 278 | String diffText2(List diffs) { 279 | final text = StringBuffer(); 280 | for (var aDiff in diffs) { 281 | if (aDiff.operation != DIFF_DELETE) { 282 | text.write(aDiff.text); 283 | } 284 | } 285 | return text.toString(); 286 | } 287 | -------------------------------------------------------------------------------- /lib/src/match.dart: -------------------------------------------------------------------------------- 1 | /// Copyright 2011 Google Inc. 2 | /// Copyright 2014 Boris Kaul 3 | /// http://github.com/localvoid/diff-match-patch 4 | /// 5 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 6 | /// you may not use this file except in compliance with the License. 7 | /// You may obtain a copy of the License at 8 | /// 9 | /// http://www.apache.org/licenses/LICENSE-2.0 10 | /// 11 | /// Unless required by applicable law or agreed to in writing, software 12 | /// distributed under the License is distributed on an 'AS IS' BASIS, 13 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | /// See the License for the specific language governing permissions and 15 | /// limitations under the License. 16 | 17 | library match; 18 | 19 | import 'dart:collection'; 20 | import 'dart:math'; 21 | import 'package:diff_match_patch/src/common.dart'; 22 | 23 | /// Locate the best instance of [pattern] in [text] near [loc]. 24 | /// Returns -1 if no match found. 25 | /// 26 | /// * [text] is the text to search. 27 | /// * [pattern] is the pattern to search for. 28 | /// * [loc] is the location to search around. 29 | /// * [threshold] At what point is no match declared (0.0 = perfection, 30 | /// 1.0 = very loose). 31 | /// * [distance] How far to search for a match (0 = exact location, 1000+ = broad 32 | /// match). A match this many characters away from the expected location will 33 | /// add 1.0 to the score (0.0 is a perfect match). 34 | /// 35 | /// Returns the best match index or -1. 36 | int match(String text, String pattern, int loc, 37 | {double threshold = 0.5, int distance = 1000}) { 38 | loc = max(0, min(loc, text.length)); 39 | if (text == pattern) { 40 | // Shortcut (potentially not guaranteed by the algorithm) 41 | return 0; 42 | } else if (text.isEmpty) { 43 | // Nothing to match. 44 | return -1; 45 | } else if (loc + pattern.length <= text.length && 46 | text.substring(loc, loc + pattern.length) == pattern) { 47 | // Perfect match at the perfect spot! (Includes case of null pattern) 48 | return loc; 49 | } else { 50 | // Do a fuzzy compare. 51 | return matchBitap(text, pattern, loc, threshold, distance); 52 | } 53 | } 54 | 55 | /// Compute and return the score for a match with [e] errors and [x] location. 56 | /// 57 | /// * [e] is the number of errors in match. 58 | /// * [x] is the location of match. 59 | /// * [loc] is the expected location of match. 60 | /// * [pattern] is the pattern being sought. 61 | /// * [distance] How far to search for a match (0 = exact location, 1000+ = broad 62 | /// match). A match this many characters away from the expected location will 63 | /// add 1.0 to the score (0.0 is a perfect match). 64 | /// 65 | /// Returns the overall score for match (0.0 = good, 1.0 = bad). 66 | double _bitapScore(int e, int x, int loc, String pattern, int distance) { 67 | final accuracy = e / pattern.length; 68 | final proximity = (loc - x).abs(); 69 | if (distance == 0) { 70 | // Dodge divide by zero error. 71 | return proximity == 0 ? accuracy : 1.0; 72 | } 73 | return accuracy + proximity / distance; 74 | } 75 | 76 | /// Locate the best instance of [pattern] in [text] near [loc] using the 77 | /// Bitap algorithm. Returns -1 if no match found. 78 | /// 79 | /// * [text] is the the text to search. 80 | /// * [pattern] is the pattern to search for. 81 | /// * [loc] is the location to search around. 82 | /// * [threshold] At what point is no match declared (0.0 = perfection, 83 | /// 1.0 = very loose). 84 | /// * [distance] How far to search for a match (0 = exact location, 1000+ = broad 85 | /// match). A match this many characters away from the expected location will 86 | /// add 1.0 to the score (0.0 is a perfect match). 87 | /// 88 | /// Returns the best match index or -1. 89 | int matchBitap( 90 | String text, String pattern, int loc, double threshold, int distance) { 91 | // Pattern too long for this application. 92 | assert(BITS_PER_INT == 0 || pattern.length <= BITS_PER_INT); 93 | 94 | // Initialise the alphabet. 95 | // ignore: omit_local_variable_types 96 | Map s = matchAlphabet(pattern); 97 | 98 | // Highest score beyond which we give up. 99 | var score_threshold = threshold; 100 | // Is there a nearby exact match? (speedup) 101 | var best_loc = text.indexOf(pattern, loc); 102 | if (best_loc != -1) { 103 | score_threshold = 104 | min(_bitapScore(0, best_loc, loc, pattern, distance), score_threshold); 105 | // What about in the other direction? (speedup) 106 | best_loc = text.lastIndexOf(pattern, loc + pattern.length); 107 | if (best_loc != -1) { 108 | score_threshold = min( 109 | _bitapScore(0, best_loc, loc, pattern, distance), score_threshold); 110 | } 111 | } 112 | 113 | // Initialise the bit arrays. 114 | final match_mask = 1 << (pattern.length - 1); 115 | best_loc = -1; 116 | 117 | int bin_min, bin_mid; 118 | var bin_max = pattern.length + text.length; 119 | var last_rd = []; 120 | for (var d = 0; d < pattern.length; d++) { 121 | // Scan for the best match; each iteration allows for one more error. 122 | // Run a binary search to determine how far from 'loc' we can stray at 123 | // this error level. 124 | bin_min = 0; 125 | bin_mid = bin_max; 126 | while (bin_min < bin_mid) { 127 | if (_bitapScore(d, loc + bin_mid, loc, pattern, distance) <= 128 | score_threshold) { 129 | bin_min = bin_mid; 130 | } else { 131 | bin_max = bin_mid; 132 | } 133 | bin_mid = ((bin_max - bin_min) / 2 + bin_min).toInt(); 134 | } 135 | // Use the result from this iteration as the maximum for the next. 136 | bin_max = bin_mid; 137 | var start = max(1, loc - bin_mid + 1); 138 | var finish = min(loc + bin_mid, text.length) + pattern.length; 139 | 140 | final rd = List.filled(finish + 2, 0); 141 | rd[finish + 1] = (1 << d) - 1; 142 | for (var j = finish; j >= start; j--) { 143 | var charMatch = 0; 144 | if (text.length <= j - 1 || !s.containsKey(text[j - 1])) { 145 | // Out of range. 146 | charMatch = 0; 147 | } else { 148 | charMatch = s[text[j - 1]] ?? 0; 149 | } 150 | if (d == 0) { 151 | // First pass: exact match. 152 | rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; 153 | } else { 154 | // Subsequent passes: fuzzy match. 155 | rd[j] = ((rd[j + 1] << 1) | 1) & charMatch | 156 | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | 157 | last_rd[j + 1]; 158 | } 159 | if ((rd[j] & match_mask) != 0) { 160 | var score = _bitapScore(d, j - 1, loc, pattern, distance); 161 | // This match will almost certainly be better than any existing 162 | // match. But check anyway. 163 | if (score <= score_threshold) { 164 | // Told you so. 165 | score_threshold = score; 166 | best_loc = j - 1; 167 | if (best_loc > loc) { 168 | // When passing loc, don't exceed our current distance from loc. 169 | start = max(1, 2 * loc - best_loc); 170 | } else { 171 | // Already passed loc, downhill from here on in. 172 | break; 173 | } 174 | } 175 | } 176 | } 177 | if (_bitapScore(d + 1, loc, loc, pattern, distance) > score_threshold) { 178 | // No hope for a (better) match at greater error levels. 179 | break; 180 | } 181 | last_rd = rd; 182 | } 183 | return best_loc; 184 | } 185 | 186 | /// Initialise the alphabet for the Bitap algorithm. 187 | /// 188 | /// [pattern] is the the text to encode. 189 | /// Returns a Map of character locations. 190 | Map matchAlphabet(String pattern) { 191 | final s = HashMap(); 192 | for (var i = 0; i < pattern.length; i++) { 193 | s[pattern[i]] = 0; 194 | } 195 | for (var i = 0; i < pattern.length; i++) { 196 | s[pattern[i]] = s[pattern[i]]! | (1 << (pattern.length - i - 1)); 197 | } 198 | return s; 199 | } 200 | -------------------------------------------------------------------------------- /lib/src/patch.dart: -------------------------------------------------------------------------------- 1 | /// Copyright 2011 Google Inc. 2 | /// Copyright 2014 Boris Kaul 3 | /// http://github.com/localvoid/diff-match-patch 4 | /// 5 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 6 | /// you may not use this file except in compliance with the License. 7 | /// You may obtain a copy of the License at 8 | /// 9 | /// http://www.apache.org/licenses/LICENSE-2.0 10 | /// 11 | /// Unless required by applicable law or agreed to in writing, software 12 | /// distributed under the License is distributed on an 'AS IS' BASIS, 13 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | /// See the License for the specific language governing permissions and 15 | /// limitations under the License. 16 | 17 | library patch; 18 | 19 | import 'package:diff_match_patch/src/common.dart'; 20 | import 'package:diff_match_patch/src/diff.dart'; 21 | import 'package:diff_match_patch/src/match.dart'; 22 | import 'dart:math'; 23 | 24 | /// Class representing one patch operation. 25 | class Patch { 26 | List diffs = []; 27 | int start1 = 0; 28 | int start2 = 0; 29 | int length1 = 0; 30 | int length2 = 0; 31 | 32 | /// Emmulate GNU diff's format. 33 | /// 34 | /// Header: @@ -382,8 +481,9 @@ 35 | /// 36 | /// Indicies are printed as 1-based, not 0-based. 37 | /// Returns the GNU diff string. 38 | @override 39 | String toString() { 40 | String coords1, coords2; 41 | if (length1 == 0) { 42 | coords1 = '$start1,0'; 43 | } else if (length1 == 1) { 44 | coords1 = (start1 + 1).toString(); 45 | } else { 46 | coords1 = '${start1 + 1},$length1'; 47 | } 48 | if (length2 == 0) { 49 | coords2 = '$start2,0'; 50 | } else if (length2 == 1) { 51 | coords2 = (start2 + 1).toString(); 52 | } else { 53 | coords2 = '${start2 + 1},$length2'; 54 | } 55 | final text = StringBuffer('@@ -$coords1 +$coords2 @@\n'); 56 | // Escape the body of the patch with %xx notation. 57 | // ignore: omit_local_variable_types 58 | for (Diff aDiff in diffs) { 59 | switch (aDiff.operation) { 60 | case DIFF_INSERT: 61 | text.write('+'); 62 | break; 63 | case DIFF_DELETE: 64 | text.write('-'); 65 | break; 66 | case DIFF_EQUAL: 67 | text.write(' '); 68 | break; 69 | } 70 | text..write(Uri.encodeFull(aDiff.text))..write('\n'); 71 | } 72 | return text.toString().replaceAll('%20', ' '); 73 | } 74 | } 75 | 76 | /// Take a list of patches and return a textual representation. 77 | /// 78 | /// [patches] is a List of Patch objects. 79 | /// Returns a text representation of patches. 80 | String patchToText(List patches) { 81 | final text = StringBuffer(); 82 | // ignore: omit_local_variable_types 83 | for (Patch aPatch in patches) { 84 | text.write(aPatch); 85 | } 86 | return text.toString(); 87 | } 88 | 89 | /// Parse a textual representation of patches and return a List of Patch objects. 90 | /// 91 | /// [textline] is a text representation of patches. 92 | /// 93 | /// Returns a List of Patch objects. 94 | /// 95 | /// Throws ArgumentError if invalid input. 96 | List patchFromText(String textline) { 97 | final patches = []; 98 | if (textline.isEmpty) { 99 | return patches; 100 | } 101 | final text = textline.split('\n'); 102 | var textPointer = 0; 103 | final patchHeader = RegExp('^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@\$'); 104 | while (textPointer < text.length) { 105 | Match? m = patchHeader.firstMatch(text[textPointer]); 106 | if (m == null) { 107 | throw ArgumentError('Invalid patch string: ${text[textPointer]}'); 108 | } 109 | final patch = Patch(); 110 | patches.add(patch); 111 | patch.start1 = int.parse(m.group(1)!); 112 | if (m.group(2)!.isEmpty) { 113 | patch.start1--; 114 | patch.length1 = 1; 115 | } else if (m.group(2) == '0') { 116 | patch.length1 = 0; 117 | } else { 118 | patch.start1--; 119 | patch.length1 = int.parse(m.group(2)!); 120 | } 121 | 122 | patch.start2 = int.parse(m.group(3)!); 123 | if (m.group(4)!.isEmpty) { 124 | patch.start2--; 125 | patch.length2 = 1; 126 | } else if (m.group(4) == '0') { 127 | patch.length2 = 0; 128 | } else { 129 | patch.start2--; 130 | patch.length2 = int.parse(m.group(4)!); 131 | } 132 | textPointer++; 133 | 134 | while (textPointer < text.length) { 135 | if (text[textPointer].isNotEmpty) { 136 | final sign = text[textPointer][0]; 137 | var line = ''; 138 | try { 139 | line = Uri.decodeFull(text[textPointer].substring(1)); 140 | } on ArgumentError { 141 | // Malformed URI sequence. 142 | throw ArgumentError('Illegal escape in patch_fromText: $line'); 143 | } 144 | if (sign == '-') { 145 | // Deletion. 146 | patch.diffs.add(Diff(DIFF_DELETE, line)); 147 | } else if (sign == '+') { 148 | // Insertion. 149 | patch.diffs.add(Diff(DIFF_INSERT, line)); 150 | } else if (sign == ' ') { 151 | // Minor equality. 152 | patch.diffs.add(Diff(DIFF_EQUAL, line)); 153 | } else if (sign == '@') { 154 | // Start of next patch. 155 | break; 156 | } else { 157 | // WTF? 158 | throw ArgumentError('Invalid patch mode "$sign" in: $line'); 159 | } 160 | } 161 | textPointer++; 162 | } 163 | } 164 | return patches; 165 | } 166 | 167 | /// Increase the context until it is unique, 168 | /// but don't let the pattern expand beyond Match_MaxBits. 169 | /// 170 | /// * [patch] is the patch to grow. 171 | /// * [text] is the source text. 172 | /// * [patchMargin] Chunk size for context length. 173 | void patchAddContext(Patch patch, String text, int patchMargin) { 174 | if (text.isEmpty) { 175 | return; 176 | } 177 | var pattern = text.substring(patch.start2, patch.start2 + patch.length1); 178 | var padding = 0; 179 | 180 | // Look for the first and last matches of pattern in text. If two different 181 | // matches are found, increase the pattern length. 182 | while ((text.indexOf(pattern) != text.lastIndexOf(pattern)) && 183 | (pattern.length < ((BITS_PER_INT - patchMargin) - patchMargin))) { 184 | padding += patchMargin; 185 | pattern = text.substring(max(0, patch.start2 - padding), 186 | min(text.length, patch.start2 + patch.length1 + padding)); 187 | } 188 | // Add one chunk for good luck. 189 | padding += patchMargin; 190 | 191 | // Add the prefix. 192 | final prefix = text.substring(max(0, patch.start2 - padding), patch.start2); 193 | if (prefix.isNotEmpty) { 194 | patch.diffs.insert(0, Diff(DIFF_EQUAL, prefix)); 195 | } 196 | // Add the suffix. 197 | final suffix = text.substring(patch.start2 + patch.length1, 198 | min(text.length, patch.start2 + patch.length1 + padding)); 199 | if (suffix.isNotEmpty) { 200 | patch.diffs.add(Diff(DIFF_EQUAL, suffix)); 201 | } 202 | 203 | // Roll back the start points. 204 | patch.start1 -= prefix.length; 205 | patch.start2 -= prefix.length; 206 | // Extend the lengths. 207 | patch.length1 += prefix.length + suffix.length; 208 | patch.length2 += prefix.length + suffix.length; 209 | } 210 | 211 | /// Compute a List of Patches to turn [text1] into [text2]. 212 | /// 213 | /// Use diffs if provided, otherwise compute it ourselves. 214 | /// There are four ways to call this function, depending on what data is 215 | /// available to the caller: 216 | /// 217 | /// * Method 1: 218 | /// [a] = text1, [b] = text2 219 | /// * Method 2: 220 | /// [a] = diffs 221 | /// * Method 3 (optimal): 222 | /// [a] = text1, [b] = diffs 223 | /// * Method 4 (deprecated, use method 3): 224 | /// [a] = text1, [b] = text2, [c] = diffs 225 | /// 226 | /// Returns a List of Patch objects. 227 | List patchMake(Object? a, 228 | {Object? b, 229 | Object? c, 230 | double diffTimeout = 1.0, 231 | DateTime? diffDeadline, 232 | int diffEditCost = 4, 233 | double deleteThreshold = 0.5, 234 | int margin = 4}) { 235 | String text1; 236 | List diffs; 237 | if (a is String && b is String && c == null) { 238 | // Method 1: text1, text2 239 | // Compute diffs from text1 and text2. 240 | text1 = a; 241 | diffs = diff(text1, b, 242 | checklines: true, timeout: diffTimeout, deadline: diffDeadline); 243 | if (diffs.length > 2) { 244 | cleanupSemantic(diffs); 245 | cleanupEfficiency(diffs, diffEditCost); 246 | } 247 | } else if (a is List && b == null && c == null) { 248 | // Method 2: diffs 249 | // Compute text1 from diffs. 250 | diffs = a; 251 | text1 = diffText1(diffs); 252 | } else if (a is String && b is List && c == null) { 253 | // Method 3: text1, diffs 254 | text1 = a; 255 | diffs = b; 256 | } else if (a is String && b is String && c is List) { 257 | // Method 4: text1, text2, diffs 258 | // text2 is not used. 259 | text1 = a; 260 | diffs = c; 261 | } else { 262 | throw ArgumentError('Unknown call format to patch_make.'); 263 | } 264 | 265 | final patches = []; 266 | if (diffs.isEmpty) { 267 | return patches; // Get rid of the null case. 268 | } 269 | var patch = Patch(); 270 | final postpatch_buffer = StringBuffer(); 271 | var char_count1 = 0; // Number of characters into the text1 string. 272 | var char_count2 = 0; // Number of characters into the text2 string. 273 | // Start with text1 (prepatch_text) and apply the diffs until we arrive at 274 | // text2 (postpatch_text). We recreate the patches one by one to determine 275 | // context info. 276 | var prepatch_text = text1; 277 | var postpatch_text = text1; 278 | for (var aDiff in diffs) { 279 | if (patch.diffs.isEmpty && aDiff.operation != DIFF_EQUAL) { 280 | // A new patch starts here. 281 | patch.start1 = char_count1; 282 | patch.start2 = char_count2; 283 | } 284 | 285 | switch (aDiff.operation) { 286 | case DIFF_INSERT: 287 | patch.diffs.add(aDiff); 288 | patch.length2 += aDiff.text.length; 289 | postpatch_buffer.clear(); 290 | postpatch_buffer 291 | ..write(postpatch_text.substring(0, char_count2)) 292 | ..write(aDiff.text) 293 | ..write(postpatch_text.substring(char_count2)); 294 | postpatch_text = postpatch_buffer.toString(); 295 | break; 296 | case DIFF_DELETE: 297 | patch.length1 += aDiff.text.length; 298 | patch.diffs.add(aDiff); 299 | postpatch_buffer.clear(); 300 | postpatch_buffer 301 | ..write(postpatch_text.substring(0, char_count2)) 302 | ..write(postpatch_text.substring(char_count2 + aDiff.text.length)); 303 | postpatch_text = postpatch_buffer.toString(); 304 | break; 305 | case DIFF_EQUAL: 306 | if (aDiff.text.length <= 2 * margin && 307 | patch.diffs.isNotEmpty && 308 | aDiff != diffs.last) { 309 | // Small equality inside a patch. 310 | patch.diffs.add(aDiff); 311 | patch.length1 += aDiff.text.length; 312 | patch.length2 += aDiff.text.length; 313 | } 314 | 315 | if (aDiff.text.length >= 2 * margin) { 316 | // Time for a new patch. 317 | if (patch.diffs.isNotEmpty) { 318 | patchAddContext(patch, prepatch_text, margin); 319 | patches.add(patch); 320 | patch = Patch(); 321 | // Unlike Unidiff, our patch lists have a rolling context. 322 | // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff 323 | // Update prepatch text & pos to reflect the application of the 324 | // just completed patch. 325 | prepatch_text = postpatch_text; 326 | char_count1 = char_count2; 327 | } 328 | } 329 | break; 330 | } 331 | 332 | // Update the current character count. 333 | if (aDiff.operation != DIFF_INSERT) { 334 | char_count1 += aDiff.text.length; 335 | } 336 | if (aDiff.operation != DIFF_DELETE) { 337 | char_count2 += aDiff.text.length; 338 | } 339 | } 340 | // Pick up the leftover patch if not empty. 341 | if (patch.diffs.isNotEmpty) { 342 | patchAddContext(patch, prepatch_text, margin); 343 | patches.add(patch); 344 | } 345 | 346 | return patches; 347 | } 348 | 349 | /// Given an array of patches, return another array that is identical. 350 | /// [patches] is a List of Patch objects. 351 | /// Returns a List of Patch objects. 352 | List patchDeepCopy(List patches) { 353 | final patchesCopy = []; 354 | for (var aPatch in patches) { 355 | final patchCopy = Patch(); 356 | for (var aDiff in aPatch.diffs) { 357 | patchCopy.diffs.add(Diff(aDiff.operation, aDiff.text)); 358 | } 359 | patchCopy.start1 = aPatch.start1; 360 | patchCopy.start2 = aPatch.start2; 361 | patchCopy.length1 = aPatch.length1; 362 | patchCopy.length2 = aPatch.length2; 363 | patchesCopy.add(patchCopy); 364 | } 365 | return patchesCopy; 366 | } 367 | 368 | /// Merge a set of patches onto the text. 369 | /// 370 | /// Return a patched text, as well 371 | /// as an array of true/false values indicating which patches were applied. 372 | /// 373 | /// * [patches] is a List of Patch objects 374 | /// * [text] is the old text. 375 | /// 376 | /// Returns a two element List, containing the new text and a List of bool values. 377 | List patchApply(List patches, String text, 378 | {double deleteThreshold = 0.5, 379 | double diffTimeout = 1.0, 380 | DateTime? diffDeadline, 381 | double matchThreshold = 0.5, 382 | int matchDistance = 1000, 383 | int margin = 4}) { 384 | if (patches.isEmpty) { 385 | return [text, []]; 386 | } 387 | 388 | // Deep copy the patches so that no changes are made to originals. 389 | patches = patchDeepCopy(patches); 390 | 391 | final nullPadding = patchAddPadding(patches, margin: margin); 392 | text = '$nullPadding$text$nullPadding'; 393 | patchSplitMax(patches, margin: margin); 394 | 395 | final text_buffer = StringBuffer(); 396 | var x = 0; 397 | // delta keeps track of the offset between the expected and actual location 398 | // of the previous patch. If there are patches expected at positions 10 and 399 | // 20, but the first patch was found at 12, delta is 2 and the second patch 400 | // has an effective expected position of 22. 401 | var delta = 0; 402 | final results = List.filled(patches.length, false); 403 | for (var aPatch in patches) { 404 | var expected_loc = aPatch.start2 + delta; 405 | var text1 = diffText1(aPatch.diffs); 406 | int start_loc; 407 | var end_loc = -1; 408 | if (text1.length > BITS_PER_INT) { 409 | // patch_splitMax will only provide an oversized pattern in the case of 410 | // a monster delete. 411 | start_loc = match(text, text1.substring(0, BITS_PER_INT), expected_loc, 412 | threshold: matchThreshold, distance: matchDistance); 413 | if (start_loc != -1) { 414 | end_loc = match(text, text1.substring(text1.length - BITS_PER_INT), 415 | expected_loc + text1.length - BITS_PER_INT, 416 | threshold: matchThreshold, distance: matchDistance); 417 | if (end_loc == -1 || start_loc >= end_loc) { 418 | // Can't find valid trailing context. Drop this patch. 419 | start_loc = -1; 420 | } 421 | } 422 | } else { 423 | start_loc = match(text, text1, expected_loc, 424 | threshold: matchThreshold, distance: matchDistance); 425 | } 426 | if (start_loc == -1) { 427 | // No match found. :( 428 | results[x] = false; 429 | // Subtract the delta for this failed patch from subsequent patches. 430 | delta -= aPatch.length2 - aPatch.length1; 431 | } else { 432 | // Found a match. :) 433 | results[x] = true; 434 | delta = start_loc - expected_loc; 435 | String text2; 436 | if (end_loc == -1) { 437 | text2 = text.substring( 438 | start_loc, min(start_loc + text1.length, text.length)); 439 | } else { 440 | text2 = 441 | text.substring(start_loc, min(end_loc + BITS_PER_INT, text.length)); 442 | } 443 | if (text1 == text2) { 444 | // Perfect match, just shove the replacement text in. 445 | text_buffer.clear(); 446 | text_buffer 447 | ..write(text.substring(0, start_loc)) 448 | ..write(diffText2(aPatch.diffs)) 449 | ..write(text.substring(start_loc + text1.length)); 450 | text = text_buffer.toString(); 451 | } else { 452 | // Imperfect match. Run a diff to get a framework of equivalent 453 | // indices. 454 | final diffs = diff(text1, text2, 455 | checklines: false, deadline: diffDeadline, timeout: diffTimeout); 456 | if ((text1.length > BITS_PER_INT) && 457 | (levenshtein(diffs) / text1.length > deleteThreshold)) { 458 | // The end points match, but the content is unacceptably bad. 459 | results[x] = false; 460 | } else { 461 | cleanupSemanticLossless(diffs); 462 | var index1 = 0; 463 | for (var aDiff in aPatch.diffs) { 464 | if (aDiff.operation != DIFF_EQUAL) { 465 | var index2 = diffXIndex(diffs, index1); 466 | if (aDiff.operation == DIFF_INSERT) { 467 | // Insertion 468 | text_buffer.clear(); 469 | text_buffer 470 | ..write(text.substring(0, start_loc + index2)) 471 | ..write(aDiff.text) 472 | ..write(text.substring(start_loc + index2)); 473 | text = text_buffer.toString(); 474 | } else if (aDiff.operation == DIFF_DELETE) { 475 | // Deletion 476 | text_buffer.clear(); 477 | text_buffer 478 | ..write(text.substring(0, start_loc + index2)) 479 | ..write(text.substring(start_loc + 480 | diffXIndex(diffs, index1 + aDiff.text.length))); 481 | text = text_buffer.toString(); 482 | } 483 | } 484 | if (aDiff.operation != DIFF_DELETE) { 485 | index1 += aDiff.text.length; 486 | } 487 | } 488 | } 489 | } 490 | } 491 | x++; 492 | } 493 | // Strip the padding off. 494 | text = text.substring(nullPadding.length, text.length - nullPadding.length); 495 | return [text, results]; 496 | } 497 | 498 | /// Add some padding on text start and end so that edges can match something. 499 | /// 500 | /// Intended to be called only from within [patch_apply]. 501 | /// 502 | /// [patches] is a List of Patch objects. 503 | /// 504 | /// Returns the padding string added to each side. 505 | String patchAddPadding(List patches, {int margin = 4}) { 506 | final paddingLength = margin; 507 | final paddingCodes = []; 508 | for (var x = 1; x <= paddingLength; x++) { 509 | paddingCodes.add(x); 510 | } 511 | var nullPadding = String.fromCharCodes(paddingCodes); 512 | 513 | // Bump all the patches forward. 514 | for (var aPatch in patches) { 515 | aPatch.start1 += paddingLength; 516 | aPatch.start2 += paddingLength; 517 | } 518 | 519 | // Add some padding on start of first diff. 520 | var patch = patches[0]; 521 | var diffs = patch.diffs; 522 | if (diffs.isEmpty || diffs[0].operation != DIFF_EQUAL) { 523 | // Add nullPadding equality. 524 | diffs.insert(0, Diff(DIFF_EQUAL, nullPadding)); 525 | patch.start1 -= paddingLength; // Should be 0. 526 | patch.start2 -= paddingLength; // Should be 0. 527 | patch.length1 += paddingLength; 528 | patch.length2 += paddingLength; 529 | } else if (paddingLength > diffs[0].text.length) { 530 | // Grow first equality. 531 | var firstDiff = diffs[0]; 532 | var extraLength = paddingLength - firstDiff.text.length; 533 | firstDiff.text = 534 | '${nullPadding.substring(firstDiff.text.length)}${firstDiff.text}'; 535 | patch.start1 -= extraLength; 536 | patch.start2 -= extraLength; 537 | patch.length1 += extraLength; 538 | patch.length2 += extraLength; 539 | } 540 | 541 | // Add some padding on end of last diff. 542 | patch = patches.last; 543 | diffs = patch.diffs; 544 | if (diffs.isEmpty || diffs.last.operation != DIFF_EQUAL) { 545 | // Add nullPadding equality. 546 | diffs.add(Diff(DIFF_EQUAL, nullPadding)); 547 | patch.length1 += paddingLength; 548 | patch.length2 += paddingLength; 549 | } else if (paddingLength > diffs.last.text.length) { 550 | // Grow last equality. 551 | var lastDiff = diffs.last; 552 | var extraLength = paddingLength - lastDiff.text.length; 553 | lastDiff.text = '${lastDiff.text}${nullPadding.substring(0, extraLength)}'; 554 | patch.length1 += extraLength; 555 | patch.length2 += extraLength; 556 | } 557 | 558 | return nullPadding; 559 | } 560 | 561 | /// Look through the [patches] and break up any which are longer than the 562 | /// maximum limit of the match algorithm. 563 | /// 564 | /// Intended to be called only from within [patch_apply]. 565 | /// 566 | /// [patches] is a List of Patch objects. 567 | void patchSplitMax(List patches, {int margin = 4}) { 568 | final patch_size = BITS_PER_INT; 569 | for (var x = 0; x < patches.length; x++) { 570 | if (patches[x].length1 <= patch_size) { 571 | continue; 572 | } 573 | var bigpatch = patches[x]; 574 | // Remove the big old patch. 575 | patches.removeRange(x, x + 1); 576 | x--; 577 | var start1 = bigpatch.start1; 578 | var start2 = bigpatch.start2; 579 | var precontext = ''; 580 | while (bigpatch.diffs.isNotEmpty) { 581 | // Create one of several smaller patches. 582 | final patch = Patch(); 583 | var empty = true; 584 | patch.start1 = start1 - precontext.length; 585 | patch.start2 = start2 - precontext.length; 586 | if (precontext.isNotEmpty) { 587 | patch.length1 = patch.length2 = precontext.length; 588 | patch.diffs.add(Diff(DIFF_EQUAL, precontext)); 589 | } 590 | while (bigpatch.diffs.isNotEmpty && patch.length1 < patch_size - margin) { 591 | var diff_type = bigpatch.diffs[0].operation; 592 | var diff_text = bigpatch.diffs[0].text; 593 | if (diff_type == DIFF_INSERT) { 594 | // Insertions are harmless. 595 | patch.length2 += diff_text.length; 596 | start2 += diff_text.length; 597 | patch.diffs.add(bigpatch.diffs[0]); 598 | bigpatch.diffs.removeRange(0, 1); 599 | empty = false; 600 | } else if (diff_type == DIFF_DELETE && 601 | patch.diffs.length == 1 && 602 | patch.diffs[0].operation == DIFF_EQUAL && 603 | diff_text.length > 2 * patch_size) { 604 | // This is a large deletion. Let it pass in one chunk. 605 | patch.length1 += diff_text.length; 606 | start1 += diff_text.length; 607 | empty = false; 608 | patch.diffs.add(Diff(diff_type, diff_text)); 609 | bigpatch.diffs.removeRange(0, 1); 610 | } else { 611 | // Deletion or equality. Only take as much as we can stomach. 612 | diff_text = diff_text.substring( 613 | 0, min(diff_text.length, patch_size - patch.length1 - margin)); 614 | patch.length1 += diff_text.length; 615 | start1 += diff_text.length; 616 | if (diff_type == DIFF_EQUAL) { 617 | patch.length2 += diff_text.length; 618 | start2 += diff_text.length; 619 | } else { 620 | empty = false; 621 | } 622 | patch.diffs.add(Diff(diff_type, diff_text)); 623 | if (diff_text == bigpatch.diffs[0].text) { 624 | bigpatch.diffs.removeRange(0, 1); 625 | } else { 626 | bigpatch.diffs[0].text = 627 | bigpatch.diffs[0].text.substring(diff_text.length); 628 | } 629 | } 630 | } 631 | // Compute the head context for the next patch. 632 | precontext = diffText2(patch.diffs); 633 | precontext = precontext.substring(max(0, precontext.length - margin)); 634 | // Append the end context for this patch. 635 | String postcontext; 636 | if (diffText1(bigpatch.diffs).length > margin) { 637 | postcontext = diffText1(bigpatch.diffs).substring(0, margin); 638 | } else { 639 | postcontext = diffText1(bigpatch.diffs); 640 | } 641 | if (postcontext.isNotEmpty) { 642 | patch.length1 += postcontext.length; 643 | patch.length2 += postcontext.length; 644 | if (patch.diffs.isNotEmpty && 645 | patch.diffs.last.operation == DIFF_EQUAL) { 646 | patch.diffs.last.text = '${patch.diffs.last.text}$postcontext'; 647 | } else { 648 | patch.diffs.add(Diff(DIFF_EQUAL, postcontext)); 649 | } 650 | } 651 | if (!empty) { 652 | patches.insert(++x, patch); 653 | } 654 | } 655 | } 656 | } 657 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: diff_match_patch 2 | version: 0.4.1 3 | description: The Diff Match and Patch libraries offer robust algorithms to perform the operations required for synchronizing plain text. 4 | homepage: http://github.com/jheyne/diff-match-patch 5 | 6 | environment: 7 | sdk: '>=2.12.0 <3.0.0' 8 | 9 | dev_dependencies: 10 | test: any 11 | pedantic: ^1.11.0 12 | -------------------------------------------------------------------------------- /test/diff_test.dart: -------------------------------------------------------------------------------- 1 | /// Tests for Diff functions 2 | /// 3 | /// Copyright 2011 Google Inc. 4 | /// Copyright 2014 Boris Kaul 5 | /// http://github.com/localvoid/diff-match-patch 6 | /// 7 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 8 | /// you may not use this file except in compliance with the License. 9 | /// You may obtain a copy of the License at 10 | /// 11 | /// http://www.apache.org/licenses/LICENSE-2.0 12 | /// 13 | /// Unless required by applicable law or agreed to in writing, software 14 | /// distributed under the License is distributed on an 'AS IS' BASIS, 15 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | /// See the License for the specific language governing permissions and 17 | /// limitations under the License. 18 | 19 | import 'package:test/test.dart'; 20 | import 'package:diff_match_patch/src/diff.dart'; 21 | 22 | Diff deq(String t) => Diff(DIFF_EQUAL, t); 23 | Diff ddel(String t) => Diff(DIFF_DELETE, t); 24 | Diff dins(String t) => Diff(DIFF_INSERT, t); 25 | 26 | List _rebuildTexts(List diffs) { 27 | // Construct the two texts which made up the diff originally. 28 | final text1 = StringBuffer(); 29 | final text2 = StringBuffer(); 30 | for (var x = 0; x < diffs.length; x++) { 31 | if (diffs[x].operation != DIFF_INSERT) { 32 | text1.write(diffs[x].text); 33 | } 34 | if (diffs[x].operation != DIFF_DELETE) { 35 | text2.write(diffs[x].text); 36 | } 37 | } 38 | return [text1.toString(), text2.toString()]; 39 | } 40 | 41 | // ignore: always_declare_return_types 42 | main() { 43 | group('Diff', () { 44 | group('Common Prefix', () { 45 | test('Null', () { 46 | expect(commonPrefix('abc', 'xyz'), equals(0)); 47 | }); 48 | test('Non-null', () { 49 | expect(commonPrefix('1234abcdef', '1234xyz'), equals(4)); 50 | }); 51 | test('Whole', () { 52 | expect(commonPrefix('1234', '1234xyz'), equals(4)); 53 | }); 54 | }); 55 | 56 | group('Common Suffix', () { 57 | test('Null', () { 58 | expect(commonSuffix('abc', 'xyz'), equals(0)); 59 | }); 60 | test('Non-null', () { 61 | expect(commonSuffix('abcdef1234', 'xyz1234'), equals(4)); 62 | }); 63 | test('Whole', () { 64 | expect(commonSuffix('1234', 'xyz1234'), equals(4)); 65 | }); 66 | }); 67 | 68 | group('Common Overlap', () { 69 | test('Null', () { 70 | expect(commonOverlap('', 'abcd'), equals(0)); 71 | }); 72 | test('Whole', () { 73 | expect(commonOverlap('abc', 'abcd'), equals(3)); 74 | }); 75 | test('No overlap', () { 76 | expect(commonOverlap('123456', 'abcd'), equals(0)); 77 | }); 78 | test('Overlap', () { 79 | expect(commonOverlap('123456xxx', 'xxxabcd'), equals(3)); 80 | }); 81 | test('Unicode', () { 82 | expect(commonOverlap('fi', '\ufb01i'), equals(0)); 83 | }); 84 | }); 85 | 86 | group('Half Match', () { 87 | test('No match #1', () { 88 | expect(diffHalfMatch('1234567890', 'abcdef', 1.0), isNull); 89 | }); 90 | test('No match #2', () { 91 | expect(diffHalfMatch('12345', '23', 1.0), isNull); 92 | }); 93 | test('Single match #1', () { 94 | expect(diffHalfMatch('1234567890', 'a345678z', 1.0), 95 | equals(['12', '90', 'a', 'z', '345678'])); 96 | }); 97 | test('Single match #2', () { 98 | expect(diffHalfMatch('a345678z', '1234567890', 1.0), 99 | equals(['a', 'z', '12', '90', '345678'])); 100 | }); 101 | test('Single match #3', () { 102 | expect(diffHalfMatch('abc56789z', '1234567890', 1.0), 103 | equals(['abc', 'z', '1234', '0', '56789'])); 104 | }); 105 | test('Single match #4', () { 106 | expect(diffHalfMatch('a23456xyz', '1234567890', 1.0), 107 | equals(['a', 'xyz', '1', '7890', '23456'])); 108 | }); 109 | test('Multiple matches #1', () { 110 | expect( 111 | diffHalfMatch('121231234123451234123121', 'a1234123451234z', 1.0), 112 | equals(['12123', '123121', 'a', 'z', '1234123451234'])); 113 | }); 114 | test('Multiple matches #2', () { 115 | expect( 116 | diffHalfMatch('x-=-=-=-=-=-=-=-=-=-=-=-=', 'xx-=-=-=-=-=-=-=', 1.0), 117 | equals(['', '-=-=-=-=-=', 'x', '', 'x-=-=-=-=-=-=-='])); 118 | }); 119 | test('Multiple matches #3', () { 120 | expect( 121 | diffHalfMatch('-=-=-=-=-=-=-=-=-=-=-=-=y', '-=-=-=-=-=-=-=yy', 1.0), 122 | equals(['-=-=-=-=-=', '', '', 'y', '-=-=-=-=-=-=-=y'])); 123 | }); 124 | test('Non-optimal halfmatch', () { 125 | // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy 126 | expect(diffHalfMatch('qHilloHelloHew', 'xHelloHeHulloy', 1.0), 127 | equals(['qHillo', 'w', 'x', 'Hulloy', 'HelloHe'])); 128 | }); 129 | test('Optimal no halfmatch', () { 130 | expect(diffHalfMatch('qHilloHelloHew', 'xHelloHeHulloy', 0.0), isNull); 131 | }); 132 | }); 133 | 134 | group('Lines To Chars', () { 135 | void testLinesToCharsResultEquals( 136 | Map a, Map b) { 137 | expect(a['chars1'], equals(b['chars1'])); 138 | expect(a['chars2'], equals(b['chars2'])); 139 | expect(a['lineArray'], equals(b['lineArray'])); 140 | } 141 | 142 | // Convert lines down to characters. 143 | test('Shared lines', () { 144 | testLinesToCharsResultEquals({ 145 | 'chars1': '\u0001\u0002\u0001', 146 | 'chars2': '\u0002\u0001\u0002', 147 | 'lineArray': ['', 'alpha\n', 'beta\n'] 148 | }, linesToChars('alpha\nbeta\nalpha\n', 'beta\nalpha\nbeta\n')); 149 | }); 150 | test('Empty string and blank lines', () { 151 | testLinesToCharsResultEquals({ 152 | 'chars1': '', 153 | 'chars2': '\u0001\u0002\u0003\u0003', 154 | 'lineArray': ['', 'alpha\r\n', 'beta\r\n', '\r\n'] 155 | }, linesToChars('', 'alpha\r\nbeta\r\n\r\n\r\n')); 156 | }); 157 | test('No linebreaks', () { 158 | testLinesToCharsResultEquals({ 159 | 'chars1': '\u0001', 160 | 'chars2': '\u0002', 161 | 'lineArray': ['', 'a', 'b'] 162 | }, linesToChars('a', 'b')); 163 | }); 164 | 165 | test('More than 256', () { 166 | // More than 256 to reveal any 8-bit limitations. 167 | var n = 300; 168 | var lineList = []; 169 | var charList = StringBuffer(); 170 | for (var x = 1; x < n + 1; x++) { 171 | lineList.add('$x\n'); 172 | charList.write(String.fromCharCodes([x])); 173 | } 174 | expect(lineList.length, equals(n)); 175 | var lines = lineList.join(''); 176 | var chars = charList.toString(); 177 | expect(chars.length, equals(n)); 178 | lineList.insert(0, ''); 179 | testLinesToCharsResultEquals( 180 | {'chars1': chars, 'chars2': '', 'lineArray': lineList}, 181 | linesToChars(lines, '')); 182 | }); 183 | }); 184 | 185 | group('Chars To Lines', () { 186 | test('Equality #1', () { 187 | expect(deq('a') == deq('a'), isTrue); 188 | }); 189 | test('Equality #2', () { 190 | expect(deq('a'), equals(deq('a'))); 191 | }); 192 | 193 | test('Shared lines', () { 194 | // Convert chars up to lines. 195 | var diffs = [ 196 | deq('\u0001\u0002\u0001'), 197 | dins('\u0002\u0001\u0002') 198 | ]; 199 | charsToLines(diffs, ['', 'alpha\n', 'beta\n']); 200 | expect(diffs, 201 | equals([deq('alpha\nbeta\nalpha\n'), dins('beta\nalpha\nbeta\n')])); 202 | }); 203 | 204 | test('More than 256', () { 205 | // More than 256 to reveal any 8-bit limitations. 206 | var n = 300; 207 | var lineList = []; 208 | var charList = StringBuffer(); 209 | for (var x = 1; x < n + 1; x++) { 210 | lineList.add('$x\n'); 211 | charList.write(String.fromCharCodes([x])); 212 | } 213 | expect(lineList.length, equals(n)); 214 | var lines = lineList.join(''); 215 | var chars = charList.toString(); 216 | expect(chars.length, equals(n)); 217 | lineList.insert(0, ''); 218 | var diffs = [ddel(chars)]; 219 | charsToLines(diffs, lineList); 220 | expect(diffs, equals([ddel(lines)])); 221 | }); 222 | }); 223 | 224 | group('CleanupMerge', () { 225 | test('Null', () { 226 | var diffs = []; 227 | cleanupMerge(diffs); 228 | expect(diffs, equals([])); 229 | }); 230 | test('No change case', () { 231 | var diffs = [deq('a'), ddel('b'), dins('c')]; 232 | cleanupMerge(diffs); 233 | expect(diffs, equals([deq('a'), ddel('b'), dins('c')])); 234 | }); 235 | test('Merge equalities', () { 236 | var diffs = [deq('a'), deq('b'), deq('c')]; 237 | cleanupMerge(diffs); 238 | expect(diffs, equals([deq('abc')])); 239 | }); 240 | test('Merge deletions', () { 241 | var diffs = [ddel('a'), ddel('b'), ddel('c')]; 242 | cleanupMerge(diffs); 243 | expect(diffs, equals([ddel('abc')])); 244 | }); 245 | test('Merge insertions', () { 246 | var diffs = [dins('a'), dins('b'), dins('c')]; 247 | cleanupMerge(diffs); 248 | expect(diffs, equals([dins('abc')])); 249 | }); 250 | test('Merge interweave', () { 251 | var diffs = [ 252 | ddel('a'), 253 | dins('b'), 254 | ddel('c'), 255 | dins('d'), 256 | deq('e'), 257 | deq('f') 258 | ]; 259 | cleanupMerge(diffs); 260 | expect(diffs, equals([ddel('ac'), dins('bd'), deq('ef')])); 261 | }); 262 | test('Prefix and suffix detection', () { 263 | var diffs = [ddel('a'), dins('abc'), ddel('dc')]; 264 | cleanupMerge(diffs); 265 | expect(diffs, equals([deq('a'), ddel('d'), dins('b'), deq('c')])); 266 | }); 267 | test('Prefix and suffix detection with equalities', () { 268 | var diffs = [ 269 | deq('x'), 270 | ddel('a'), 271 | dins('abc'), 272 | ddel('dc'), 273 | deq('y') 274 | ]; 275 | cleanupMerge(diffs); 276 | expect(diffs, equals([deq('xa'), ddel('d'), dins('b'), deq('cy')])); 277 | }); 278 | test('Slide edit left', () { 279 | var diffs = [deq('a'), dins('ba'), deq('c')]; 280 | cleanupMerge(diffs); 281 | expect(diffs, equals([dins('ab'), deq('ac')])); 282 | }); 283 | test('Slide edit right', () { 284 | var diffs = [deq('c'), dins('ab'), deq('a')]; 285 | cleanupMerge(diffs); 286 | expect(diffs, equals([deq('ca'), dins('ba')])); 287 | }); 288 | test('Slide edit left recursive', () { 289 | var diffs = [deq('a'), ddel('b'), deq('c'), ddel('ac'), deq('x')]; 290 | cleanupMerge(diffs); 291 | expect(diffs, equals([ddel('abc'), deq('acx')])); 292 | }); 293 | test('diff_cleanupMerge: Slide edit right recursive', () { 294 | var diffs = [deq('x'), ddel('ca'), deq('c'), ddel('b'), deq('a')]; 295 | cleanupMerge(diffs); 296 | expect(diffs, equals([deq('xca'), ddel('cba')])); 297 | }); 298 | }); 299 | 300 | group('Cleanup Semantic Lossless', () { 301 | // Slide diffs to match logical boundaries. 302 | test('Null case', () { 303 | var diffs = []; 304 | cleanupSemanticLossless(diffs); 305 | expect(diffs, equals([])); 306 | }); 307 | test('Blank lines', () { 308 | var diffs = [ 309 | deq('AAA\r\n\r\nBBB'), 310 | dins('\r\nDDD\r\n\r\nBBB'), 311 | deq('\r\nEEE') 312 | ]; 313 | cleanupSemanticLossless(diffs); 314 | expect( 315 | diffs, 316 | equals([ 317 | deq('AAA\r\n\r\n'), 318 | dins('BBB\r\nDDD\r\n\r\n'), 319 | deq('BBB\r\nEEE') 320 | ])); 321 | }); 322 | test('Line boundaries', () { 323 | var diffs = [deq('AAA\r\nBBB'), dins(' DDD\r\nBBB'), deq(' EEE')]; 324 | cleanupSemanticLossless(diffs); 325 | expect(diffs, 326 | equals([deq('AAA\r\n'), dins('BBB DDD\r\n'), deq('BBB EEE')])); 327 | }); 328 | test('Word boundaries', () { 329 | var diffs = [deq('The c'), dins('ow and the c'), deq('at.')]; 330 | cleanupSemanticLossless(diffs); 331 | expect(diffs, equals([deq('The '), dins('cow and the '), deq('cat.')])); 332 | }); 333 | test('Alphanumeric boundaries', () { 334 | var diffs = [deq('The-c'), dins('ow-and-the-c'), deq('at.')]; 335 | cleanupSemanticLossless(diffs); 336 | expect(diffs, equals([deq('The-'), dins('cow-and-the-'), deq('cat.')])); 337 | }); 338 | test('Hitting the start', () { 339 | var diffs = [deq('a'), ddel('a'), deq('ax')]; 340 | cleanupSemanticLossless(diffs); 341 | expect(diffs, equals([ddel('a'), deq('aax')])); 342 | }); 343 | test('Hitting the end', () { 344 | var diffs = [deq('xa'), ddel('a'), deq('a')]; 345 | cleanupSemanticLossless(diffs); 346 | expect(diffs, equals([deq('xaa'), ddel('a')])); 347 | }); 348 | test('Sentence boundaries', () { 349 | var diffs = [ 350 | deq('The xxx. The '), 351 | dins('zzz. The '), 352 | deq('yyy.') 353 | ]; 354 | cleanupSemanticLossless(diffs); 355 | expect(diffs, 356 | equals([deq('The xxx.'), dins(' The zzz.'), deq(' The yyy.')])); 357 | }); 358 | }); 359 | 360 | group('Cleanup Semantic', () { 361 | // Cleanup semantically trivial equalities. 362 | test('Null case', () { 363 | var diffs = []; 364 | cleanupSemantic(diffs); 365 | expect(diffs, equals([])); 366 | }); 367 | test('No elimination #1', () { 368 | var diffs = [ddel('ab'), dins('cd'), deq('12'), ddel('e')]; 369 | cleanupSemantic(diffs); 370 | expect(diffs, equals([ddel('ab'), dins('cd'), deq('12'), ddel('e')])); 371 | }); 372 | test('No elimination #2', () { 373 | var diffs = [ddel('abc'), dins('ABC'), deq('1234'), ddel('wxyz')]; 374 | cleanupSemantic(diffs); 375 | expect(diffs, 376 | equals([ddel('abc'), dins('ABC'), deq('1234'), ddel('wxyz')])); 377 | }); 378 | test('Simple elimination', () { 379 | var diffs = [ddel('a'), deq('b'), ddel('c')]; 380 | cleanupSemantic(diffs); 381 | expect(diffs, equals([ddel('abc'), dins('b')])); 382 | }); 383 | test('Backpass elimination', () { 384 | var diffs = [ 385 | ddel('ab'), 386 | deq('cd'), 387 | ddel('e'), 388 | deq('f'), 389 | dins('g') 390 | ]; 391 | cleanupSemantic(diffs); 392 | expect(diffs, equals([ddel('abcdef'), dins('cdfg')])); 393 | }); 394 | test('Multiple elimination', () { 395 | var diffs = [ 396 | dins('1'), 397 | deq('A'), 398 | ddel('B'), 399 | dins('2'), 400 | deq('_'), 401 | dins('1'), 402 | deq('A'), 403 | ddel('B'), 404 | dins('2') 405 | ]; 406 | cleanupSemantic(diffs); 407 | expect(diffs, equals([ddel('AB_AB'), dins('1A2_1A2')])); 408 | }); 409 | test('Word boundaries', () { 410 | var diffs = [deq('The c'), ddel('ow and the c'), deq('at.')]; 411 | cleanupSemantic(diffs); 412 | expect(diffs, equals([deq('The '), ddel('cow and the '), deq('cat.')])); 413 | }); 414 | test('No overlap elimination', () { 415 | var diffs = [ddel('abcxx'), dins('xxdef')]; 416 | cleanupSemantic(diffs); 417 | expect(diffs, equals([ddel('abcxx'), dins('xxdef')])); 418 | }); 419 | test('Overlap elimination', () { 420 | var diffs = [ddel('abcxxx'), dins('xxxdef')]; 421 | cleanupSemantic(diffs); 422 | expect(diffs, equals([ddel('abc'), deq('xxx'), dins('def')])); 423 | }); 424 | test('Reverse overlap elimination', () { 425 | var diffs = [ddel('xxxabc'), dins('defxxx')]; 426 | cleanupSemantic(diffs); 427 | expect(diffs, equals([dins('def'), deq('xxx'), ddel('abc')])); 428 | }); 429 | test('Two overlap eliminations', () { 430 | var diffs = [ 431 | ddel('abcd1212'), 432 | dins('1212efghi'), 433 | deq('----'), 434 | ddel('A3'), 435 | dins('3BC') 436 | ]; 437 | cleanupSemantic(diffs); 438 | expect( 439 | diffs, 440 | equals([ 441 | ddel('abcd'), 442 | deq('1212'), 443 | dins('efghi'), 444 | deq('----'), 445 | ddel('A'), 446 | deq('3'), 447 | dins('BC') 448 | ])); 449 | }); 450 | }); 451 | 452 | group('Cleanup Efficiency', () { 453 | // Cleanup operationally trivial equalities. 454 | test('Null case', () { 455 | var diffs = []; 456 | cleanupEfficiency(diffs, 4); 457 | expect(diffs, equals([])); 458 | }); 459 | test('No elimination', () { 460 | var diffs = [ 461 | ddel('ab'), 462 | dins('12'), 463 | deq('wxyz'), 464 | ddel('cd'), 465 | dins('34') 466 | ]; 467 | cleanupEfficiency(diffs, 4); 468 | expect( 469 | diffs, 470 | equals( 471 | [ddel('ab'), dins('12'), deq('wxyz'), ddel('cd'), dins('34')])); 472 | }); 473 | test('Four-edit elimination', () { 474 | var diffs = [ 475 | ddel('ab'), 476 | dins('12'), 477 | deq('xyz'), 478 | ddel('cd'), 479 | dins('34') 480 | ]; 481 | cleanupEfficiency(diffs, 4); 482 | expect(diffs, equals([ddel('abxyzcd'), dins('12xyz34')])); 483 | }); 484 | test('Three-edit elimination', () { 485 | var diffs = [dins('12'), deq('x'), ddel('cd'), dins('34')]; 486 | cleanupEfficiency(diffs, 4); 487 | expect(diffs, equals([ddel('xcd'), dins('12x34')])); 488 | }); 489 | test('Backpass elimination', () { 490 | var diffs = [ 491 | ddel('ab'), 492 | dins('12'), 493 | deq('xy'), 494 | dins('34'), 495 | deq('z'), 496 | ddel('cd'), 497 | dins('56') 498 | ]; 499 | cleanupEfficiency(diffs, 4); 500 | expect(diffs, equals([ddel('abxyzcd'), dins('12xy34z56')])); 501 | }); 502 | test('High cost elimination', () { 503 | var diffs = [ 504 | ddel('ab'), 505 | dins('12'), 506 | deq('wxyz'), 507 | ddel('cd'), 508 | dins('34') 509 | ]; 510 | cleanupEfficiency(diffs, 5); 511 | expect(diffs, equals([ddel('abwxyzcd'), dins('12wxyz34')])); 512 | }); 513 | }); 514 | 515 | group('Text', () { 516 | var diffs = [ 517 | deq('jump'), 518 | ddel('s'), 519 | dins('ed'), 520 | deq(' over '), 521 | ddel('the'), 522 | dins('a'), 523 | deq(' lazy') 524 | ]; 525 | 526 | test('#1', () { 527 | expect(diffText1(diffs), equals('jumps over the lazy')); 528 | }); 529 | test('#2', () { 530 | expect(diffText2(diffs), equals('jumped over a lazy')); 531 | }); 532 | }); 533 | 534 | group('Delta', () { 535 | var diffs = [ 536 | deq('jump'), 537 | ddel('s'), 538 | dins('ed'), 539 | deq(' over '), 540 | ddel('the'), 541 | dins('a'), 542 | deq(' lazy'), 543 | dins('old dog') 544 | ]; 545 | test('Base text', () { 546 | expect(diffText1(diffs), equals('jumps over the lazy')); 547 | }); 548 | test('Normal', () { 549 | var delta = toDelta(diffs); 550 | expect(delta, equals('=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog')); 551 | }); 552 | test('Too long', () { 553 | // Generates error (19 < 20) 554 | var text = diffText1(diffs); 555 | var delta = toDelta(diffs); 556 | expect(() => fromDelta('${text}x', delta), throwsArgumentError); 557 | }); 558 | test('Too short', () { 559 | // Generates error (19 > 18). 560 | var text = diffText1(diffs); 561 | var delta = toDelta(diffs); 562 | expect(() => fromDelta(text.substring(1), delta), throwsArgumentError); 563 | }); 564 | test('Too short', () { 565 | // Generates error (%c3%xy invalid Unicode). 566 | expect(() => fromDelta('', '+%c3%xy'), throwsArgumentError); 567 | }); 568 | test('Unicode text', () { 569 | // Test deltas with special characters. 570 | var diffs = [ 571 | deq('\u0680 \x00 \t %'), 572 | ddel('\u0681 \x01 \n ^'), 573 | dins('\u0682 \x02 \\ |') 574 | ]; 575 | var text = diffText1(diffs); 576 | expect(text, equals('\u0680 \x00 \t %\u0681 \x01 \n ^')); 577 | }); 578 | test('Unicode', () { 579 | var diffs = [ 580 | deq('\u0680 \x00 \t %'), 581 | ddel('\u0681 \x01 \n ^'), 582 | dins('\u0682 \x02 \\ |') 583 | ]; 584 | var text = diffText1(diffs); 585 | var delta = toDelta(diffs); 586 | expect(delta, equals('=7\t-7\t+%DA%82 %02 %5C %7C')); 587 | expect(fromDelta(text, delta), unorderedEquals(diffs)); 588 | }); 589 | test('Unchanged characters', () { 590 | // Verify pool of unchanged characters. 591 | var diffs = [ 592 | dins('A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + \$ , # ') 593 | ]; 594 | var text2 = diffText2(diffs); 595 | expect(text2, 596 | equals('A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + \$ , # ')); 597 | 598 | var delta = toDelta(diffs); 599 | expect(delta, 600 | equals('+A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + \$ , # ')); 601 | 602 | // Convert delta string into a diff. 603 | expect(fromDelta('', delta), unorderedEquals(diffs)); 604 | }); 605 | }); 606 | 607 | group('XIndex', () { 608 | test('Translation on equality', () { 609 | expect(diffXIndex([ddel('a'), dins('1234'), deq('xyz')], 2), equals(5)); 610 | }); 611 | test('Translation on deletion', () { 612 | expect(diffXIndex([deq('a'), ddel('1234'), deq('xyz')], 3), equals(1)); 613 | }); 614 | }); 615 | 616 | group('Levenshtein', () { 617 | test('Trailing equality', () { 618 | expect(levenshtein([ddel('abc'), dins('1234'), deq('xyz')]), equals(4)); 619 | }); 620 | test('Leading equality', () { 621 | expect(levenshtein([deq('xyz'), ddel('abc'), dins('1234')]), equals(4)); 622 | }); 623 | test('Middle equality', () { 624 | expect(levenshtein([ddel('abc'), deq('xyz'), dins('1234')]), equals(7)); 625 | }); 626 | }); 627 | 628 | group('Bisect', () { 629 | test('Normal', () { 630 | // Since the resulting diff hasn't been normalized, it would be ok if 631 | // the insertion and deletion pairs are swapped. 632 | // If the order changes, tweak this test as required. 633 | var diffs = [ddel('c'), dins('m'), deq('a'), ddel('t'), dins('p')]; 634 | // One year should be sufficient. 635 | var deadline = DateTime.now().add(Duration(days: 365)); 636 | expect(diffBisect('cat', 'map', 1.0, deadline), equals(diffs)); 637 | }); 638 | 639 | test('Timeout', () { 640 | var diffs = [ddel('cat'), dins('map')]; 641 | // Set deadline to one year ago. 642 | var deadline = DateTime.now().subtract(Duration(days: 365)); 643 | expect(diffBisect('cat', 'map', 1.0, deadline), equals(diffs)); 644 | }); 645 | }); 646 | 647 | group('Main', () { 648 | test('Null', () { 649 | expect(diff('', '', checklines: false), equals([])); 650 | }); 651 | test('Equality', () { 652 | expect(diff('abc', 'abc', checklines: false), equals([deq('abc')])); 653 | }); 654 | test('Simple insertion', () { 655 | expect(diff('abc', 'ab123c', checklines: false), 656 | equals([deq('ab'), dins('123'), deq('c')])); 657 | }); 658 | test('Simple deletion', () { 659 | expect(diff('a123bc', 'abc', checklines: false), 660 | equals([deq('a'), ddel('123'), deq('bc')])); 661 | }); 662 | test('Two insertions', () { 663 | expect(diff('abc', 'a123b456c', checklines: false), 664 | equals([deq('a'), dins('123'), deq('b'), dins('456'), deq('c')])); 665 | }); 666 | test('Two deletions', () { 667 | expect(diff('a123b456c', 'abc', checklines: false), 668 | equals([deq('a'), ddel('123'), deq('b'), ddel('456'), deq('c')])); 669 | }); 670 | test('Simple case #1', () { 671 | expect(diff('a', 'b', checklines: false, timeout: 0.0), 672 | equals([ddel('a'), dins('b')])); 673 | }); 674 | 675 | test('Simple case #2', () { 676 | expect( 677 | diff('Apples are a fruit.', 'Bananas are also fruit.', 678 | checklines: false, timeout: 0.0), 679 | equals([ 680 | ddel('Apple'), 681 | dins('Banana'), 682 | deq('s are a'), 683 | dins('lso'), 684 | deq(' fruit.') 685 | ])); 686 | }); 687 | 688 | test('Simple case #3', () { 689 | expect( 690 | diff('ax\t', '\u0680x\000', checklines: false, timeout: 0.0), 691 | equals([ 692 | ddel('a'), 693 | dins('\u0680'), 694 | deq('x'), 695 | ddel('\t'), 696 | dins('\000') 697 | ])); 698 | }); 699 | test('Overlap #1', () { 700 | expect( 701 | diff('1ayb2', 'abxab', checklines: false, timeout: 0.0), 702 | equals([ 703 | ddel('1'), 704 | deq('a'), 705 | ddel('y'), 706 | deq('b'), 707 | ddel('2'), 708 | dins('xab') 709 | ])); 710 | }); 711 | test('Overlap #2', () { 712 | expect(diff('abcy', 'xaxcxabc', checklines: false, timeout: 0.0), 713 | equals([dins('xaxcx'), deq('abc'), ddel('y')])); 714 | }); 715 | test('Overlap #3', () { 716 | expect( 717 | diff('ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg', 718 | 'a-bcd-efghijklmnopqrs', 719 | checklines: false, timeout: 0.0), 720 | equals([ 721 | ddel('ABCD'), 722 | deq('a'), 723 | ddel('='), 724 | dins('-'), 725 | deq('bcd'), 726 | ddel('='), 727 | dins('-'), 728 | deq('efghijklmnopqrs'), 729 | ddel('EFGHIJKLMNOefg') 730 | ])); 731 | }); 732 | test('Large equality', () { 733 | expect( 734 | diff('a [[Pennsylvania]] and [[New', ' and [[Pennsylvania]]', 735 | checklines: false, timeout: 0.0), 736 | equals([ 737 | dins(' '), 738 | deq('a'), 739 | dins('nd'), 740 | deq(' [[Pennsylvania]]'), 741 | ddel(' and [[New') 742 | ])); 743 | }); 744 | 745 | // Test the linemode speedup. 746 | // Must be long to pass the 100 char cutoff. 747 | test('Simple line-mode', () { 748 | var a = 749 | '1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n'; 750 | var b = 751 | 'abcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\n'; 752 | expect(diff(a, b, timeout: 0.0), equals(diff(a, b, checklines: false))); 753 | }); 754 | 755 | test('Single line-mode', () { 756 | var a = 757 | '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'; 758 | var b = 759 | 'abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij'; 760 | expect(diff(a, b, timeout: 0.0), equals(diff(a, b, checklines: false))); 761 | }); 762 | 763 | test('Overlap line-mode', () { 764 | var a = 765 | '1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n'; 766 | var b = 767 | 'abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n'; 768 | var texts_linemode = _rebuildTexts(diff(a, b)); 769 | var texts_textmode = _rebuildTexts(diff(a, b, checklines: false)); 770 | expect(texts_textmode, equals(texts_linemode)); 771 | }); 772 | 773 | test('Timeout min', () { 774 | var a = 775 | '`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n'; 776 | var b = 777 | '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'; 778 | // Increase the text lengths by 1024 times to ensure a timeout. 779 | for (var x = 0; x < 10; x++) { 780 | a = '$a$a'; 781 | b = '$b$b'; 782 | } 783 | var startTime = DateTime.now(); 784 | diff(a, b, timeout: 0.1); 785 | var endTime = DateTime.now(); 786 | var elapsedSeconds = 787 | endTime.difference(startTime).inMilliseconds / 1000; 788 | // Test that we took at least the timeout period. 789 | expect(0.1, lessThanOrEqualTo(elapsedSeconds)); 790 | }); 791 | }); 792 | }); 793 | } 794 | -------------------------------------------------------------------------------- /test/match_test.dart: -------------------------------------------------------------------------------- 1 | /// Tests for Match functions 2 | /// 3 | /// Copyright 2011 Google Inc. 4 | /// Copyright 2014 Boris Kaul 5 | /// http://github.com/localvoid/diff-match-patch 6 | /// 7 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 8 | /// you may not use this file except in compliance with the License. 9 | /// You may obtain a copy of the License at 10 | /// 11 | /// http://www.apache.org/licenses/LICENSE-2.0 12 | /// 13 | /// Unless required by applicable law or agreed to in writing, software 14 | /// distributed under the License is distributed on an 'AS IS' BASIS, 15 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | /// See the License for the specific language governing permissions and 17 | /// limitations under the License. 18 | 19 | import 'package:test/test.dart'; 20 | import 'package:diff_match_patch/src/match.dart'; 21 | 22 | // ignore: always_declare_return_types 23 | main() { 24 | group('Match', () { 25 | group('Alphabet', () { 26 | void testMapEquals(Map a, Map b, String error_msg) { 27 | test(error_msg, () { 28 | expect(a.keys, unorderedEquals(b.keys)); 29 | }); 30 | for (var x in a.keys) { 31 | test('$error_msg [Key: $x]', () { 32 | expect(a[x], equals(b[x])); 33 | }); 34 | } 35 | } 36 | 37 | // Initialise the bitmasks for Bitap. 38 | // ignore: omit_local_variable_types 39 | Map bitmask = {'a': 4, 'b': 2, 'c': 1}; 40 | testMapEquals(bitmask, matchAlphabet('abc'), 'Unique.'); 41 | 42 | bitmask = {'a': 37, 'b': 18, 'c': 8}; 43 | testMapEquals(bitmask, matchAlphabet('abcaba'), 'Duplicates.'); 44 | }); 45 | 46 | group('Bitap', () { 47 | test('Exact match #1', () { 48 | expect(matchBitap('abcdefghijk', 'fgh', 5, 0.5, 100), equals(5)); 49 | }); 50 | test('Exact match #2', () { 51 | expect(matchBitap('abcdefghijk', 'fgh', 0, 0.5, 100), equals(5)); 52 | }); 53 | test('Fuzzy match #1', () { 54 | expect(matchBitap('abcdefghijk', 'efxhi', 0, 0.5, 100), equals(4)); 55 | }); 56 | test('Fuzzy match #2', () { 57 | expect(matchBitap('abcdefghijk', 'cdefxyhijk', 5, 0.5, 100), equals(2)); 58 | }); 59 | test('Fuzzy match #3', () { 60 | expect(matchBitap('abcdefghijk', 'bxy', 1, 0.5, 100), equals(-1)); 61 | }); 62 | test('Overflow', () { 63 | expect(matchBitap('123456789xx0', '3456789x0', 2, 0.5, 100), equals(2)); 64 | }); 65 | test('Before start match', () { 66 | expect(matchBitap('abcdef', 'xxabc', 4, 0.5, 100), equals(0)); 67 | }); 68 | test('Beyond end match', () { 69 | expect(matchBitap('abcdef', 'defyy', 4, 0.5, 100), equals(3)); 70 | }); 71 | test('Oversized pattern', () { 72 | expect(matchBitap('abcdef', 'xabcdefy', 0, 0.5, 100), equals(0)); 73 | }); 74 | test('Threshold #1', () { 75 | expect(matchBitap('abcdefghijk', 'efxyhi', 1, 0.4, 100), equals(4)); 76 | }); 77 | test('Threshold #2', () { 78 | expect(matchBitap('abcdefghijk', 'efxyhi', 1, 0.3, 100), equals(-1)); 79 | }); 80 | test('Threshold #3', () { 81 | expect(matchBitap('abcdefghijk', 'bcdef', 1, 0.0, 100), equals(1)); 82 | }); 83 | test('Multiple select #1', () { 84 | expect(matchBitap('abcdexyzabcde', 'abccde', 3, 0.5, 100), equals(0)); 85 | }); 86 | test('Multiple select #2', () { 87 | expect(matchBitap('abcdexyzabcde', 'abccde', 5, 0.5, 100), equals(8)); 88 | }); 89 | test('Distance test #1', () { 90 | expect(matchBitap('abcdefghijklmnopqrstuvwxyz', 'abcdefg', 24, 0.5, 10), 91 | equals(-1)); 92 | }); 93 | test('Distance test #2', () { 94 | expect( 95 | matchBitap('abcdefghijklmnopqrstuvwxyz', 'abcdxxefg', 1, 0.5, 10), 96 | equals(0)); 97 | }); 98 | test('Distance test #3', () { 99 | expect( 100 | matchBitap('abcdefghijklmnopqrstuvwxyz', 'abcdefg', 24, 0.5, 1000), 101 | equals(0)); 102 | }); 103 | }); 104 | 105 | group('Main', () { 106 | test('Equality', () { 107 | expect(match('abcdef', 'abcdef', 1000), equals(0)); 108 | }); 109 | test('Null text', () { 110 | expect(match('', 'abcdef', 1), equals(-1)); 111 | }); 112 | test('Null pattern', () { 113 | expect(match('abcdef', '', 3), equals(3)); 114 | }); 115 | test('Exact match', () { 116 | expect(match('abcdef', 'de', 3), equals(3)); 117 | }); 118 | test('Beyond end match', () { 119 | expect(match('abcdef', 'defy', 4), equals(3)); 120 | }); 121 | test('Oversized pattern', () { 122 | expect(match('abcdef', 'abcdefy', 0), equals(0)); 123 | }); 124 | test('Complex match', () { 125 | expect( 126 | match('I am the very model of a modern major general.', 127 | ' that berry ', 5, 128 | threshold: 0.7), 129 | equals(4)); 130 | }); 131 | }); 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /test/patch_test.dart: -------------------------------------------------------------------------------- 1 | /// Tests for Patch functions 2 | /// 3 | /// Copyright 2011 Google Inc. 4 | /// Copyright 2014 Boris Kaul 5 | /// http://github.com/localvoid/diff-match-patch 6 | /// 7 | /// Licensed under the Apache License, Version 2.0 (the 'License'); 8 | /// you may not use this file except in compliance with the License. 9 | /// You may obtain a copy of the License at 10 | /// 11 | /// http://www.apache.org/licenses/LICENSE-2.0 12 | /// 13 | /// Unless required by applicable law or agreed to in writing, software 14 | /// distributed under the License is distributed on an 'AS IS' BASIS, 15 | /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | /// See the License for the specific language governing permissions and 17 | /// limitations under the License. 18 | 19 | import 'package:test/test.dart'; 20 | import 'package:diff_match_patch/src/diff.dart'; 21 | import 'package:diff_match_patch/src/patch.dart'; 22 | 23 | // ignore: always_declare_return_types 24 | main() { 25 | group('Patch', () { 26 | test('toString', () { 27 | var p = Patch(); 28 | p.start1 = 20; 29 | p.start2 = 21; 30 | p.length1 = 18; 31 | p.length2 = 17; 32 | p.diffs = [ 33 | Diff(DIFF_EQUAL, 'jump'), 34 | Diff(DIFF_DELETE, 's'), 35 | Diff(DIFF_INSERT, 'ed'), 36 | Diff(DIFF_EQUAL, ' over '), 37 | Diff(DIFF_DELETE, 'the'), 38 | Diff(DIFF_INSERT, 'a'), 39 | Diff(DIFF_EQUAL, '\nlaz') 40 | ]; 41 | var strp = 42 | '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n'; 43 | expect(p.toString(), equals(strp)); 44 | }); 45 | 46 | group('fromText', () { 47 | test('#0', () { 48 | expect(patchFromText('').isEmpty, equals(true)); 49 | }); 50 | test('#1', () { 51 | var strp = 52 | '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n'; 53 | expect(patchFromText(strp)[0].toString(), equals(strp)); 54 | }); 55 | test('#2', () { 56 | expect(patchFromText('@@ -1 +1 @@\n-a\n+b\n')[0].toString(), 57 | equals('@@ -1 +1 @@\n-a\n+b\n')); 58 | }); 59 | test('#3', () { 60 | expect(patchFromText('@@ -1,3 +0,0 @@\n-abc\n')[0].toString(), 61 | equals('@@ -1,3 +0,0 @@\n-abc\n')); 62 | }); 63 | test('#4', () { 64 | expect(patchFromText('@@ -0,0 +1,3 @@\n+abc\n')[0].toString(), 65 | equals('@@ -0,0 +1,3 @@\n+abc\n')); 66 | }); 67 | test('#5', () { 68 | expect(() => patchFromText('Bad\nPatch\n'), throwsArgumentError); 69 | }); 70 | }); 71 | 72 | group('toText', () { 73 | test('Single', () { 74 | var strp = 75 | '@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n'; 76 | var patches = patchFromText(strp); 77 | expect(patchToText(patches), equals(strp)); 78 | }); 79 | test('Double', () { 80 | var strp = 81 | '@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n'; 82 | var patches = patchFromText(strp); 83 | expect(patchToText(patches), equals(strp)); 84 | }); 85 | }); 86 | 87 | group('AddContext', () { 88 | const margin = 4; 89 | 90 | test('Simple case', () { 91 | var p = patchFromText('@@ -21,4 +21,10 @@\n-jump\n+somersault\n')[0]; 92 | patchAddContext( 93 | p, 'The quick brown fox jumps over the lazy dog.', margin); 94 | expect(p.toString(), 95 | equals('@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n')); 96 | }); 97 | test('Not enough trailing context', () { 98 | var p = patchFromText('@@ -21,4 +21,10 @@\n-jump\n+somersault\n')[0]; 99 | patchAddContext(p, 'The quick brown fox jumps.', margin); 100 | expect(p.toString(), 101 | equals('@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n')); 102 | }); 103 | test('Not enough leading context', () { 104 | var p = patchFromText('@@ -3 +3,2 @@\n-e\n+at\n')[0]; 105 | patchAddContext(p, 'The quick brown fox jumps.', margin); 106 | expect(p.toString(), equals('@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n')); 107 | }); 108 | test('Ambiguity', () { 109 | var p = patchFromText('@@ -3 +3,2 @@\n-e\n+at\n')[0]; 110 | patchAddContext(p, 111 | 'The quick brown fox jumps. The quick brown fox crashes.', margin); 112 | expect( 113 | p.toString(), 114 | equals( 115 | '@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n')); 116 | }); 117 | }); 118 | 119 | group('Make', () { 120 | const text1 = 'The quick brown fox jumps over the lazy dog.'; 121 | const text2 = 'That quick brown fox jumped over a lazy dog.'; 122 | var diffs = diff(text1, text2, checklines: false); 123 | 124 | test('Null', () { 125 | var patches = patchMake('', b: ''); 126 | expect(patchToText(patches), equals('')); 127 | }); 128 | 129 | test('Text2+Text1 inputs', () { 130 | var expectedPatch = 131 | '@@ -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'; 132 | // The second patch must be '-21,17 +21,18', not '-22,17 +21,18' due to rolling context. 133 | var patches = patchMake(text2, b: text1); 134 | expect(patchToText(patches), equals(expectedPatch)); 135 | }); 136 | 137 | test('Text1+Text2 inputs', () { 138 | var expectedPatch = 139 | '@@ -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'; 140 | var patches = patchMake(text1, b: text2); 141 | expect(patchToText(patches), equals(expectedPatch)); 142 | }); 143 | 144 | test('Diff input', () { 145 | var expectedPatch = 146 | '@@ -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'; 147 | var patches = patchMake(diffs); 148 | expect(patchToText(patches), equals(expectedPatch)); 149 | }); 150 | 151 | test('Text1+Diff', () { 152 | var expectedPatch = 153 | '@@ -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'; 154 | var patches = patchMake(text1, b: diffs); 155 | expect(patchToText(patches), equals(expectedPatch)); 156 | }); 157 | 158 | test('Character encoding', () { 159 | var patches = 160 | patchMake('`1234567890-=[]\\;\',./', b: '~!@#\$%^&*()_+{}|:"<>?'); 161 | expect( 162 | patchToText(patches), 163 | equals( 164 | '@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;\',./\n+~!@#\$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n')); 165 | }); 166 | 167 | test('Character decoding', () { 168 | var diffs = [ 169 | Diff(DIFF_DELETE, '`1234567890-=[]\\;\',./'), 170 | Diff(DIFF_INSERT, '~!@#\$%^&*()_+{}|:"<>?') 171 | ]; 172 | expect( 173 | patchFromText( 174 | '@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;\',./\n+~!@#\$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n')[0] 175 | .diffs, 176 | equals(diffs)); 177 | }); 178 | 179 | test('Long string with repeats', () { 180 | final sb = StringBuffer(); 181 | for (var x = 0; x < 100; x++) { 182 | sb.write('abcdef'); 183 | } 184 | var text1 = sb.toString(); 185 | var text2 = '${text1}123'; 186 | var expectedPatch = 187 | '@@ -573,28 +573,31 @@\n cdefabcdefabcdefabcdefabcdef\n+123\n'; 188 | var patches = patchMake(text1, b: text2); 189 | expect(patchToText(patches), equals(expectedPatch)); 190 | }); 191 | 192 | test('Null inputs', () { 193 | expect(() => patchMake(null), throwsArgumentError); 194 | }); 195 | }); 196 | 197 | group('Split Max', () { 198 | // Assumes that Match_MaxBits is 32. 199 | test('#1', () { 200 | var patches = patchMake('abcdefghijklmnopqrstuvwxyz01234567890', 201 | b: 'XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0'); 202 | patchSplitMax(patches); 203 | expect( 204 | patchToText(patches), 205 | equals( 206 | '@@ -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')); 207 | }); 208 | 209 | test('#2', () { 210 | var patches = patchMake( 211 | 'abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz', 212 | b: 'abcdefuvwxyz'); 213 | var oldToText = patchToText(patches); 214 | patchSplitMax(patches); 215 | expect(patchToText(patches), equals(oldToText)); 216 | }); 217 | 218 | test('#3', () { 219 | var patches = patchMake( 220 | '1234567890123456789012345678901234567890123456789012345678901234567890', 221 | b: 'abc'); 222 | patchSplitMax(patches); 223 | expect( 224 | patchToText(patches), 225 | equals( 226 | '@@ -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')); 227 | }); 228 | 229 | test('#4', () { 230 | var patches = patchMake( 231 | 'abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1', 232 | b: 'abcdefghij , h : 1 , t : 1 abcdefghij , h : 1 , t : 1 abcdefghij , h : 0 , t : 1'); 233 | patchSplitMax(patches); 234 | expect( 235 | patchToText(patches), 236 | equals( 237 | '@@ -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')); 238 | }); 239 | }); 240 | 241 | group('Add padding', () { 242 | test('Both edges full', () { 243 | var patches = patchMake('', b: 'test'); 244 | expect(patchToText(patches), equals('@@ -0,0 +1,4 @@\n+test\n')); 245 | patchAddPadding(patches); 246 | expect(patchToText(patches), 247 | equals('@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n')); 248 | }); 249 | test('Both edges partial', () { 250 | var patches = patchMake('XY', b: 'XtestY'); 251 | expect( 252 | patchToText(patches), equals('@@ -1,2 +1,6 @@\n X\n+test\n Y\n')); 253 | patchAddPadding(patches); 254 | expect(patchToText(patches), 255 | equals('@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n')); 256 | }); 257 | test('Both edges none', () { 258 | var patches = patchMake('XXXXYYYY', b: 'XXXXtestYYYY'); 259 | expect(patchToText(patches), 260 | equals('@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n')); 261 | patchAddPadding(patches); 262 | expect(patchToText(patches), 263 | equals('@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n')); 264 | }); 265 | }); 266 | 267 | group('Apply', () { 268 | test('Null', () { 269 | var patches = patchMake('', b: ''); 270 | var results = patchApply(patches, 'Hello world.'); 271 | var boolArray = results[1]; 272 | var resultStr = '${results[0]}\t${boolArray.length}'; 273 | expect(resultStr, equals('Hello world.\t0')); 274 | }); 275 | 276 | test('Exact match', () { 277 | var patches = patchMake('The quick brown fox jumps over the lazy dog.', 278 | b: 'That quick brown fox jumped over a lazy dog.'); 279 | var results = 280 | patchApply(patches, 'The quick brown fox jumps over the lazy dog.'); 281 | var boolArray = results[1]; 282 | var resultStr = '${results[0]}\t${boolArray[0]}\t${boolArray[1]}'; 283 | expect(resultStr, 284 | equals('That quick brown fox jumped over a lazy dog.\ttrue\ttrue')); 285 | }); 286 | 287 | test('Partial match', () { 288 | var patches = patchMake('The quick brown fox jumps over the lazy dog.', 289 | b: 'That quick brown fox jumped over a lazy dog.'); 290 | var results = patchApply( 291 | patches, 'The quick red rabbit jumps over the tired tiger.'); 292 | var boolArray = results[1]; 293 | var resultStr = '${results[0]}\t${boolArray[0]}\t${boolArray[1]}'; 294 | expect( 295 | resultStr, 296 | equals( 297 | 'That quick red rabbit jumped over a tired tiger.\ttrue\ttrue')); 298 | }); 299 | 300 | test('Failed match', () { 301 | var patches = patchMake('The quick brown fox jumps over the lazy dog.', 302 | b: 'That quick brown fox jumped over a lazy dog.'); 303 | var results = patchApply( 304 | patches, 'I am the very model of a modern major general.'); 305 | var boolArray = results[1]; 306 | var resultStr = '${results[0]}\t${boolArray[0]}\t${boolArray[1]}'; 307 | expect( 308 | resultStr, 309 | equals( 310 | 'I am the very model of a modern major general.\tfalse\tfalse')); 311 | }); 312 | 313 | test('Big delete, small change', () { 314 | var patches = patchMake( 315 | 'x1234567890123456789012345678901234567890123456789012345678901234567890y', 316 | b: 'xabcy'); 317 | var results = patchApply(patches, 318 | 'x123456789012345678901234567890-----++++++++++-----123456789012345678901234567890y'); 319 | var boolArray = results[1]; 320 | var resultStr = '${results[0]}\t${boolArray[0]}\t${boolArray[1]}'; 321 | expect(resultStr, equals('xabcy\ttrue\ttrue')); 322 | }); 323 | 324 | test('Big delete, big change #1', () { 325 | var patches = patchMake( 326 | 'x1234567890123456789012345678901234567890123456789012345678901234567890y', 327 | b: 'xabcy'); 328 | var results = patchApply(patches, 329 | 'x12345678901234567890---------------++++++++++---------------12345678901234567890y'); 330 | var boolArray = results[1]; 331 | var resultStr = '${results[0]}\t${boolArray[0]}\t${boolArray[1]}'; 332 | expect(resultStr, 333 | 'xabc12345678901234567890---------------++++++++++---------------12345678901234567890y\tfalse\ttrue'); 334 | }); 335 | 336 | test('Big delete, big change #2', () { 337 | var deleteThreshold = 0.6; 338 | var patches = patchMake( 339 | 'x1234567890123456789012345678901234567890123456789012345678901234567890y', 340 | b: 'xabcy', 341 | deleteThreshold: deleteThreshold); 342 | var results = patchApply(patches, 343 | 'x12345678901234567890---------------++++++++++---------------12345678901234567890y', 344 | deleteThreshold: deleteThreshold); 345 | var boolArray = results[1]; 346 | var resultStr = '${results[0]}\t${boolArray[0]}\t${boolArray[1]}'; 347 | expect(resultStr, equals('xabcy\ttrue\ttrue')); 348 | }); 349 | 350 | test('Compensate for failed patch', () { 351 | var matchThreshold = 0.0; 352 | var matchDistance = 0; 353 | var patches = patchMake( 354 | 'abcdefghijklmnopqrstuvwxyz--------------------1234567890', 355 | b: 'abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------1234567YYYYYYYYYY890'); 356 | var results = patchApply( 357 | patches, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890', 358 | matchThreshold: matchThreshold, matchDistance: matchDistance); 359 | var boolArray = results[1]; 360 | var resultStr = '${results[0]}\t${boolArray[0]}\t${boolArray[1]}'; 361 | expect( 362 | resultStr, 363 | equals( 364 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890\tfalse\ttrue')); 365 | }); 366 | 367 | test('No side effects', () { 368 | var patches = patchMake('', b: 'test'); 369 | var patchStr = patchToText(patches); 370 | patchApply(patches, ''); 371 | expect(patchToText(patches), equals(patchStr)); 372 | }); 373 | 374 | test('No side effects with major delete', () { 375 | var patches = patchMake('The quick brown fox jumps over the lazy dog.', 376 | b: 'Woof'); 377 | var patchStr = patchToText(patches); 378 | patchApply(patches, 'The quick brown fox jumps over the lazy dog.'); 379 | expect(patchToText(patches), equals(patchStr)); 380 | }); 381 | 382 | test('Edge exact match', () { 383 | var patches = patchMake('', b: 'test'); 384 | var results = patchApply(patches, ''); 385 | var boolArray = results[1]; 386 | var resultStr = '${results[0]}\t${boolArray[0]}'; 387 | expect(resultStr, equals('test\ttrue')); 388 | }); 389 | 390 | test('Near edge exact match', () { 391 | var patches = patchMake('XY', b: 'XtestY'); 392 | var results = patchApply(patches, 'XY'); 393 | var boolArray = results[1]; 394 | var resultStr = '${results[0]}\t${boolArray[0]}'; 395 | expect(resultStr, equals('XtestY\ttrue')); 396 | }); 397 | 398 | test('Edge partial match', () { 399 | var patches = patchMake('y', b: 'y123'); 400 | var results = patchApply(patches, 'x'); 401 | var boolArray = results[1]; 402 | var resultStr = '${results[0]}\t${boolArray[0]}'; 403 | expect(resultStr, equals('x123\ttrue')); 404 | }); 405 | }); 406 | }); 407 | } 408 | --------------------------------------------------------------------------------