├── .gitattributes ├── .github └── workflows │ ├── build_and_test.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── RELEASING.md ├── ROADMAP.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── io │ └── github │ └── petertrr │ ├── PublishingConfiguration.kt │ ├── ext │ └── Project.ext.kt │ └── jacoco-convention.gradle.kts ├── detekt.yml ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kotlin-js-store └── yarn.lock ├── renovate.json ├── settings.gradle.kts └── src ├── commonMain └── kotlin │ └── io │ └── github │ └── petertrr │ └── diffutils │ ├── DiffUtils.kt │ ├── StringUtils.kt │ ├── algorithm │ ├── Change.kt │ ├── DiffAlgorithm.kt │ ├── DiffAlgorithmListener.kt │ ├── DiffEqualizer.kt │ ├── EqualsDiffEqualizer.kt │ ├── IgnoreWsStringDiffEqualizer.kt │ ├── NoopAlgorithmListener.kt │ └── myers │ │ ├── MyersDiff.kt │ │ ├── MyersDiffWithLinearSpace.kt │ │ └── PathNode.kt │ ├── patch │ ├── ChangeDelta.kt │ ├── Chunk.kt │ ├── ConflictOutput.kt │ ├── ConflictProducingConflictOutput.kt │ ├── DeleteDelta.kt │ ├── Delta.kt │ ├── DeltaType.kt │ ├── DiffException.kt │ ├── EqualDelta.kt │ ├── ExceptionProducingConflictOutput.kt │ ├── InsertDelta.kt │ ├── Patch.kt │ ├── PatchFailedException.kt │ └── VerifyChunk.kt │ └── text │ ├── CharDiffSplitter.kt │ ├── DiffLineNormalizer.kt │ ├── DiffLineProcessor.kt │ ├── DiffRow.kt │ ├── DiffRowGenerator.kt │ ├── DiffSplitter.kt │ ├── DiffTagGenerator.kt │ ├── HtmlDiffTagGenerator.kt │ ├── HtmlLineNormalizer.kt │ └── WordDiffSplitter.kt └── commonTest └── kotlin └── io └── github └── petertrr └── diffutils ├── DiffUtilsTest.kt ├── algorithm └── myers │ ├── MyersDiffTest.kt │ └── MyersDiffWithLinearSpaceTest.kt ├── patch ├── ChunkTest.kt ├── PatchWithMyersDiffTest.kt └── PatchWithMyersDiffWithLinearSpaceTest.kt ├── text ├── DiffRowGeneratorTest.kt └── StringUtilsTest.kt └── utils └── DeltaUtils.kt /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.bat eol=crlf -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - 'main' 8 | 9 | permissions: 10 | contents: write # https://github.com/gradle/actions/blob/main/setup-gradle/README.md#basic-usage 11 | 12 | jobs: 13 | build_and_test_with_code_coverage: 14 | name: Build, test and upload code coverage 15 | runs-on: ubuntu-24.04 16 | env: 17 | CI: true 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Get Kotlin version 22 | id: get-kotlin-version 23 | run: | 24 | echo "version=$(cat gradle/libs.versions.toml | grep -m1 kotlin | cut -d'=' -f2 - | tr -d ' "')" >> $GITHUB_OUTPUT 25 | - uses: actions/cache@v4 26 | with: 27 | key: konan-${{ runner.os }}-${{ steps.get-kotlin-version.outputs.version }} 28 | path: ~/.konan 29 | - name: Set up JDK 11 30 | uses: actions/setup-java@v4 31 | with: 32 | distribution: temurin 33 | java-version: 11 34 | - uses: gradle/actions/setup-gradle@v4 35 | name: Setup Gradle 36 | with: 37 | gradle-version: wrapper 38 | cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} 39 | dependency-graph: generate-and-submit 40 | - name: build and test 41 | run: ./gradlew check build -x detekt -Pdetekt.multiplatform.disabled=true --scan 42 | - name: Upload test reports 43 | if: ${{ failure() }} # runs only if previous step has failed, the entire workflow will still be marked as failed 44 | uses: actions/upload-artifact@v4 45 | with: 46 | name: gradle-test-report 47 | path: '**/build/reports/' 48 | - name: Code coverage report 49 | uses: codecov/codecov-action@v5 50 | env: 51 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 52 | with: 53 | flags: unittests 54 | fail_ci_if_error: true # optional (default = false) 55 | 56 | - name: run detekt 57 | run: ./gradlew check -Pdetekt.multiplatform.disabled=false -PdetektAutoCorrect=true 58 | - run: git status && git diff 59 | if: ${{ always() }} 60 | - uses: reviewdog/action-suggester@v1 61 | # Fixme: run if only the previous step has failed 62 | if: ${{ failure() }} 63 | with: 64 | tool_name: detekt 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | env: 8 | GPG_SEC: ${{ secrets.PGP_SEC }} 9 | GPG_PASSWORD: ${{ secrets.PGP_PASSWORD }} 10 | OSSRH_USERNAME: ${{ secrets.SONATYPE_USER }} 11 | OSSRH_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 12 | 13 | jobs: 14 | release: 15 | name: Build and publish release 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Get Kotlin version 22 | id: get-kotlin-version 23 | run: | 24 | echo "version=$(cat gradle/libs.versions.toml | grep -m1 kotlin | cut -d'=' -f2 - | tr -d ' "')" >> $GITHUB_OUTPUT 25 | - uses: actions/cache@v4 26 | with: 27 | key: konan-${{ runner.os }}-${{ steps.get-kotlin-version.outputs.version }} 28 | path: ~/.konan 29 | - name: Set up JDK 30 | uses: actions/setup-java@v4 31 | with: 32 | distribution: temurin 33 | java-version: 17 34 | - uses: gradle/actions/setup-gradle@v4 35 | with: 36 | gradle-version: wrapper 37 | cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} 38 | - run: ./gradlew publishToSonatype closeSonatypeStagingRepository 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .gradle 3 | build 4 | !ktlint/src/main/resources/config/.idea 5 | /.idea 6 | *.iml 7 | out 8 | .DS_Store 9 | .kotlin/ 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kotlin-multiplatform-diff 2 | 3 | ![Build and test](https://github.com/petertrr/kotlin-multiplatform-diff/workflows/Build%20and%20test/badge.svg) 4 | [![Codecov](https://codecov.io/gh/petertrr/kotlin-multiplatform-diff/branch/main/graph/badge.svg)](https://codecov.io/gh/petertrr/kotlin-multiplatform-diff) 5 | [![License](https://img.shields.io/github/license/petertrr/kotlin-multiplatform-diff)](https://github.com/petertrr/kotlin-multiplatform-diff/blob/main/LICENSE) 6 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.petertrr/kotlin-multiplatform-diff)](https://mvnrepository.com/artifact/io.github.petertrr) 7 | [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) 8 | [![Kotlin](https://img.shields.io/badge/kotlin-2.1.20-blue.svg?logo=kotlin)](http://kotlinlang.org) 9 | 10 | This is a port of [java-diff-utils](https://github.com/java-diff-utils/java-diff-utils) to Kotlin with multiplatform support. 11 | All credit for the implementation goes to the original authors. 12 | 13 | ## Features 14 | 15 | All features from version `4.15` of the original library are present, except for: 16 | 17 | - fuzzy patches 18 | - unified diff, which heavily uses file read/write and therefore needs a more complicated rewrite 19 | - diff-utils-jgit, which uses JVM-only JGit 20 | 21 | Refer to the [original wiki][1] for more information. 22 | 23 | ## Supported platforms 24 | 25 | - JVM - targets 1.8+ 26 | - JS - `browser` and `nodejs` 27 | - WebAssembly - `wasmJs` and `wasmWasi` 28 | - Native - supports all targets 29 | 30 | [1]: https://github.com/java-diff-utils/java-diff-utils/wiki 31 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | 1. Create a Github release with a new tag in a format `vX.Y.Z`, e.g. `v0.1.2` 2 | 2. Github Actions workflow will start, building release and pushing it to maven central. 3 | 3. Staging repositories on Sonatype will be closed automatically, but need to be released manually. -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | Library: 2 | * [ ] More kotlin-way API 3 | * [ ] Refactor DiffRowGenerator to use kotlin-dsl-style builder 4 | * [x] Explicit API mode 5 | * [ ] Working with files 6 | * [ ] Binary compatibility validator for JVM 7 | * [ ] Benchmarks on JVM to compare with the original library 8 | 9 | Infra: 10 | * [ ] Static analysis (detekt, diktat) 11 | * [ ] Git hooks -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import io.github.petertrr.configurePublishing 2 | import io.github.petertrr.ext.booleanProperty 3 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 4 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 5 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion 6 | import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest 7 | 8 | plugins { 9 | kotlin("multiplatform") 10 | alias(libs.plugins.detekt) 11 | id("jacoco-convention") 12 | } 13 | 14 | group = "io.github.petertrr" 15 | description = "A multiplatform Kotlin library for calculating text differences" 16 | 17 | dependencies { 18 | detektPlugins(libs.detekt.formatting) 19 | } 20 | 21 | kotlin { 22 | explicitApi() 23 | 24 | compilerOptions { 25 | apiVersion = KotlinVersion.KOTLIN_2_1 26 | languageVersion = KotlinVersion.KOTLIN_2_1 27 | } 28 | 29 | jvm { 30 | compilations.configureEach { 31 | compileTaskProvider.configure { 32 | compilerOptions { 33 | // Minimum bytecode level is 52 34 | jvmTarget = JvmTarget.JVM_1_8 35 | 36 | // Output interfaces with default methods 37 | freeCompilerArgs.addAll( 38 | "-Xjvm-default=all", // Output interfaces with default methods 39 | "-Xno-param-assertions", // Remove Intrinsics.checkNotNullParameter 40 | ) 41 | } 42 | } 43 | } 44 | 45 | testRuns.configureEach { 46 | executionTask.configure { 47 | useJUnitPlatform() 48 | } 49 | } 50 | } 51 | 52 | js { 53 | val testConfig: (KotlinJsTest).() -> Unit = { 54 | useMocha { 55 | // Override default 2s timeout 56 | timeout = "120s" 57 | } 58 | } 59 | 60 | browser { 61 | testTask(testConfig) 62 | } 63 | 64 | nodejs { 65 | testTask(testConfig) 66 | } 67 | } 68 | 69 | @OptIn(ExperimentalWasmDsl::class) 70 | wasmJs { 71 | browser() 72 | nodejs() 73 | } 74 | 75 | @OptIn(ExperimentalWasmDsl::class) 76 | wasmWasi { 77 | nodejs() 78 | } 79 | 80 | // Tier 1 81 | macosX64() 82 | macosArm64() 83 | iosSimulatorArm64() 84 | iosX64() 85 | iosArm64() 86 | 87 | // Tier 2 88 | linuxX64() 89 | linuxArm64() 90 | watchosSimulatorArm64() 91 | watchosX64() 92 | watchosArm32() 93 | watchosArm64() 94 | tvosSimulatorArm64() 95 | tvosX64() 96 | tvosArm64() 97 | 98 | // Tier 3 99 | mingwX64() 100 | androidNativeArm32() 101 | androidNativeArm64() 102 | androidNativeX86() 103 | androidNativeX64() 104 | watchosDeviceArm64() 105 | 106 | // Deprecated. 107 | // Should follow the same route as official Kotlin libraries 108 | @Suppress("DEPRECATION") 109 | linuxArm32Hfp() 110 | 111 | sourceSets { 112 | commonTest { 113 | dependencies { 114 | implementation(kotlin("test")) 115 | } 116 | } 117 | } 118 | } 119 | 120 | configurePublishing() 121 | 122 | detekt { 123 | buildUponDefaultConfig = true 124 | config.setFrom(files("detekt.yml")) 125 | autoCorrect = booleanProperty("detektAutoCorrect", default = true) 126 | } 127 | 128 | tasks { 129 | check { 130 | dependsOn(detekt) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | dependencies { 6 | implementation(libs.dokka.plugin) 7 | implementation(libs.nexus.plugin) 8 | implementation(libs.kotlin.plugin) 9 | } 10 | -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | @Suppress("UnstableApiUsage") 3 | repositories { 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | 8 | versionCatalogs { 9 | create("libs") { 10 | from(files("../gradle/libs.versions.toml")) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/io/github/petertrr/PublishingConfiguration.kt: -------------------------------------------------------------------------------- 1 | package io.github.petertrr 2 | 3 | import org.gradle.api.Project 4 | import io.github.gradlenexus.publishplugin.NexusPublishPlugin 5 | import io.github.gradlenexus.publishplugin.NexusPublishExtension 6 | import org.gradle.api.publish.PublishingExtension 7 | import org.gradle.api.publish.maven.MavenPublication 8 | import org.gradle.api.publish.maven.plugins.MavenPublishPlugin 9 | import org.gradle.api.publish.maven.tasks.AbstractPublishToMaven 10 | import org.gradle.api.publish.maven.tasks.PublishToMavenRepository 11 | import org.gradle.api.tasks.bundling.Jar 12 | import org.gradle.kotlin.dsl.* 13 | import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform 14 | import org.gradle.plugins.signing.Sign 15 | import org.gradle.plugins.signing.SigningExtension 16 | import org.gradle.plugins.signing.SigningPlugin 17 | 18 | fun Project.configurePublishing() { 19 | apply() 20 | apply() 21 | apply() 22 | 23 | // If present, set properties from env variables. If any are absent, release will fail. 24 | System.getenv("OSSRH_USERNAME")?.let { 25 | extra.set("sonatypeUsername", it) 26 | } 27 | System.getenv("OSSRH_PASSWORD")?.let { 28 | extra.set("sonatypePassword", it) 29 | } 30 | System.getenv("GPG_SEC")?.let { 31 | extra.set("signingKey", it) 32 | } 33 | System.getenv("GPG_PASSWORD")?.let { 34 | extra.set("signingPassword", it) 35 | } 36 | 37 | configurePublications() 38 | 39 | if (hasProperty("signingKey")) { 40 | configureSigning() 41 | } 42 | if (hasProperty("sonatypeUsername")) { 43 | configureNexusPublishing() 44 | } 45 | } 46 | 47 | private fun Project.configurePublications() { 48 | val dokkaJar = tasks.create("dokkaJar") { 49 | group = "documentation" 50 | archiveClassifier.set("javadoc") 51 | from(tasks.findByName("dokkaHtml")) 52 | } 53 | configure { 54 | repositories { 55 | mavenLocal() 56 | } 57 | publications.withType().forEach { publication -> 58 | publication.artifact(dokkaJar) 59 | publication.pom { 60 | name.set(project.name) 61 | description.set(project.description ?: project.name) 62 | url.set("https://github.com/petertrr/kotlin-multiplatform-diff") 63 | licenses { 64 | license { 65 | name.set("The Apache Software License, Version 2.0") 66 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 67 | distribution.set("repo") 68 | } 69 | } 70 | developers { 71 | developer { 72 | id.set("petertrr") 73 | name.set("Petr Trifanov") 74 | email.set("peter.trifanov@mail.ru") 75 | } 76 | } 77 | scm { 78 | url.set("https://github.com/petertrr/kotlin-multiplatform-diff") 79 | connection.set("scm:git:git://github.com/petertrr/kotlin-multiplatform-diff.git") 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | private fun Project.configureSigning() { 87 | configure { 88 | useInMemoryPgpKeys(property("signingKey") as String?, property("signingPassword") as String?) 89 | logger.lifecycle("The following publications are getting signed: ${extensions.getByType().publications.map { it.name }}") 90 | sign(*extensions.getByType().publications.toTypedArray()) 91 | } 92 | 93 | tasks.withType().configureEach { 94 | // We have a single Javadoc artifact shared by all platforms, hence all publications depend on signing of this artifact. 95 | // This causes weird implicit dependencies, like `publishJsPublication...` depends on `signJvmPublication`. 96 | dependsOn(tasks.withType()) 97 | } 98 | } 99 | 100 | private fun Project.configureNexusPublishing() { 101 | configure { 102 | repositories { 103 | sonatype { //only for users registered in Sonatype after 24 Feb 2021 104 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) 105 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) 106 | username.set(property("sonatypeUsername") as String) 107 | password.set(property("sonatypePassword") as String) 108 | } 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/io/github/petertrr/ext/Project.ext.kt: -------------------------------------------------------------------------------- 1 | package io.github.petertrr.ext 2 | 3 | import org.gradle.api.Project 4 | 5 | /** 6 | * Returns a project property as a boolean (`"true" == true`, else `false`). 7 | */ 8 | fun Project.booleanProperty(name: String, default: Boolean = false): Boolean { 9 | val property = findProperty(name) ?: return default 10 | val propertyStr = property as? String ?: return default 11 | return propertyStr.trim().lowercase() == "true" 12 | } 13 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/io/github/petertrr/jacoco-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.tasks.testing.Test 2 | import org.gradle.kotlin.dsl.named 3 | import org.gradle.kotlin.dsl.getValue 4 | import org.gradle.kotlin.dsl.provideDelegate 5 | import org.gradle.kotlin.dsl.register 6 | import org.gradle.testing.jacoco.plugins.JacocoTaskExtension 7 | import org.gradle.testing.jacoco.tasks.JacocoReport 8 | 9 | plugins { 10 | kotlin("multiplatform") 11 | jacoco 12 | } 13 | 14 | kotlin { 15 | // to register `KotlinJvmTest` tasks before configuring Jacoco 16 | jvm() 17 | } 18 | 19 | // configure Jacoco-based code coverage reports for JVM tests executions 20 | jacoco { 21 | toolVersion = "0.8.11" 22 | } 23 | val jvmTestTask by tasks.named("jvmTest") { 24 | configure { 25 | // this is needed to generate jacoco/jvmTest.exec 26 | isEnabled = true 27 | } 28 | } 29 | val jacocoTestReportTask by tasks.register("jacocoTestReport") { 30 | executionData(jvmTestTask.extensions.getByType(JacocoTaskExtension::class.java).destinationFile) 31 | additionalSourceDirs(kotlin.sourceSets["commonMain"].kotlin.sourceDirectories) 32 | classDirectories.setFrom(layout.buildDirectory.file("classes/kotlin/jvm/main")) 33 | reports { 34 | xml.required.set(true) 35 | html.required.set(true) 36 | } 37 | } 38 | jvmTestTask.finalizedBy(jacocoTestReportTask) 39 | jacocoTestReportTask.dependsOn(jvmTestTask) 40 | -------------------------------------------------------------------------------- /detekt.yml: -------------------------------------------------------------------------------- 1 | style: 2 | active: true 3 | ReturnCount: 4 | max: 3 5 | MaxLineLength: 6 | active: true 7 | maxLineLength: 180 8 | ForbiddenComment: 9 | active: false 10 | formatting: 11 | active: true 12 | MaximumLineLength: 13 | active: false 14 | ParameterListWrapping: 15 | active: false 16 | TrailingCommaOnDeclarationSite: 17 | active: true 18 | complexity: 19 | ComplexCondition: 20 | active: false 21 | CyclomaticComplexMethod: 22 | active: false 23 | NestedBlockDepth: 24 | active: false 25 | LongParameterList: 26 | functionThreshold: 7 27 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ######################### 2 | # Gradle settings 3 | ######################### 4 | org.gradle.jvmargs = -Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 5 | org.gradle.parallel = true 6 | org.gradle.caching = true 7 | org.gradle.configuration-cache = false 8 | 9 | ######################### 10 | # Kotlin settings 11 | ######################### 12 | kotlin.code.style = official 13 | 14 | # Kotlin/Native 15 | kotlin.native.enableKlibsCrossCompilation = true 16 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.20" 3 | detekt = "1.23.5" 4 | dokka = "1.9.20" 5 | nexus = "2.0.0" 6 | 7 | [plugins] 8 | detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } 9 | 10 | [libraries] 11 | detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" } 12 | 13 | # Gradle plugins 14 | kotlin-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } 15 | dokka-plugin = { group = "org.jetbrains.dokka", name = "dokka-gradle-plugin", version.ref = "dokka" } 16 | nexus-plugin = { group = "io.github.gradle-nexus", name = "publish-plugin", version.ref = "nexus" } 17 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petertrr/kotlin-multiplatform-diff/bced3ac750b7ee7376388e709c5860550eb7f799/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=845952a9d6afa783db70bb3b0effaae45ae5542ca2bb7929619e8af49cb634cf 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "enabled": true, 3 | "schedule": [ 4 | "every 3 months on the first day of the month" 5 | ], 6 | "packageRules": [ 7 | { 8 | "managers": ["github-actions"], 9 | "groupName": "all github actions", 10 | "groupSlug": "all-github-actions" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | plugins { 9 | id("com.gradle.develocity") version ("3.19") 10 | id("org.ajoberstar.reckon.settings") version ("0.19.1") 11 | } 12 | 13 | rootProject.name = "kotlin-multiplatform-diff" 14 | 15 | dependencyResolutionManagement { 16 | @Suppress("UnstableApiUsage") 17 | repositories { 18 | mavenCentral() 19 | } 20 | } 21 | 22 | develocity { 23 | buildScan { 24 | val isCI = System.getenv("CI").toBoolean() 25 | 26 | if (isCI) { 27 | termsOfUseUrl = "https://gradle.com/terms-of-service" 28 | termsOfUseAgree = "yes" 29 | } 30 | 31 | publishing { 32 | onlyIf { isCI } 33 | } 34 | } 35 | } 36 | 37 | reckon { 38 | setDefaultInferredScope("minor") 39 | setScopeCalc(calcScopeFromProp()) 40 | 41 | stages("alpha", "rc", "final") 42 | setStageCalc(calcStageFromProp()) 43 | } 44 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/DiffUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2009-2017 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | @file:JvmName("DiffUtils") 20 | 21 | package io.github.petertrr.diffutils 22 | 23 | import io.github.petertrr.diffutils.algorithm.DiffAlgorithm 24 | import io.github.petertrr.diffutils.algorithm.DiffAlgorithmListener 25 | import io.github.petertrr.diffutils.algorithm.NoopAlgorithmListener 26 | import io.github.petertrr.diffutils.algorithm.myers.MyersDiff 27 | import io.github.petertrr.diffutils.patch.Patch 28 | import io.github.petertrr.diffutils.text.DiffRowGenerator 29 | import kotlin.jvm.JvmName 30 | import kotlin.jvm.JvmOverloads 31 | 32 | // Instead of asking consumers to normalize their line endings, we simply catch them all. 33 | private val lineBreak = Regex("\r\n|\r|\n") 34 | 35 | /** 36 | * Computes the difference between two strings. 37 | * 38 | * By default, uses the Myers algorithm. 39 | * 40 | * @param sourceText A string representing the original text 41 | * @param targetText A string representing the revised text 42 | * @param algorithm The diff algorithm to use 43 | * @param progress The diff algorithm progress listener 44 | * @param includeEqualParts Whether to include equal data parts into the patch. `false` by default. 45 | * @return The patch describing the difference between the original and revised strings 46 | */ 47 | @JvmOverloads 48 | public fun diff( 49 | sourceText: String, 50 | targetText: String, 51 | algorithm: DiffAlgorithm = MyersDiff(), 52 | progress: DiffAlgorithmListener = NoopAlgorithmListener(), 53 | includeEqualParts: Boolean = false, 54 | ): Patch = 55 | diff( 56 | source = sourceText.split(lineBreak), 57 | target = targetText.split(lineBreak), 58 | algorithm = algorithm, 59 | progress = progress, 60 | includeEqualParts = includeEqualParts, 61 | ) 62 | 63 | /** 64 | * Computes the difference between the original and target list of elements. 65 | * 66 | * By default, uses the Myers algorithm. 67 | * 68 | * @param source A list representing the original sequence of elements 69 | * @param target A list representing the revised sequence of elements 70 | * @param algorithm The diff algorithm to use 71 | * @param progress The diff algorithm progress listener 72 | * @param includeEqualParts Whether to include equal parts in the resulting patch. `false` by default. 73 | * @return The patch describing the difference between the original and revised sequences 74 | */ 75 | @JvmOverloads 76 | public fun diff( 77 | source: List, 78 | target: List, 79 | algorithm: DiffAlgorithm = MyersDiff(), 80 | progress: DiffAlgorithmListener = NoopAlgorithmListener(), 81 | includeEqualParts: Boolean = false, 82 | ): Patch = 83 | Patch.generate( 84 | original = source, 85 | revised = target, 86 | changes = algorithm.computeDiff(source, target, progress), 87 | includeEquals = includeEqualParts, 88 | ) 89 | 90 | /** 91 | * Computes the difference between the given texts inline. 92 | * 93 | * This one uses the "trick" to make out of texts lists of characters, 94 | * like [DiffRowGenerator] does and merges those changes at the end together again. 95 | * 96 | * @param original A string representing the original text 97 | * @param revised A string representing the revised text 98 | * @return The patch describing the difference between the original and revised text 99 | */ 100 | public fun diffInline(original: String, revised: String): Patch { 101 | val origChars = original.toCharArray() 102 | val origList = ArrayList(origChars.size) 103 | 104 | val revChars = revised.toCharArray() 105 | val revList = ArrayList(revChars.size) 106 | 107 | for (character in origChars) { 108 | origList.add(character.toString()) 109 | } 110 | 111 | for (character in revChars) { 112 | revList.add(character.toString()) 113 | } 114 | 115 | val patch = diff(origList, revList) 116 | patch.deltas = patch.deltas.mapTo(ArrayList(patch.deltas.size)) { 117 | it.withChunks( 118 | it.source.copy(lines = compressLines(it.source.lines, "")), 119 | it.target.copy(lines = compressLines(it.target.lines, "")), 120 | ) 121 | } 122 | 123 | return patch 124 | } 125 | 126 | /** 127 | * Applies the given patch to the original list and returns the revised list. 128 | * 129 | * @param original A list representing the original sequence of elements 130 | * @param patch The patch to apply 131 | * @return A list representing the revised sequence of elements 132 | */ 133 | public fun patch(original: List, patch: Patch): List = 134 | patch.applyTo(original) 135 | 136 | /** 137 | * Applies the given patch to the revised list and returns the original list. 138 | * 139 | * @param revised A list representing the revised sequence of elements 140 | * @param patch The patch to apply 141 | * @return A list representing the original sequence of elements 142 | */ 143 | public fun unpatch(revised: List, patch: Patch): List = 144 | patch.restore(revised) 145 | 146 | private fun compressLines(lines: List, delimiter: String): List = 147 | if (lines.isEmpty()) { 148 | emptyList() 149 | } else { 150 | listOf(lines.joinToString(delimiter)) 151 | } 152 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/StringUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2009-2017 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | @file:JvmName("StringUtils") 20 | 21 | package io.github.petertrr.diffutils 22 | 23 | import kotlin.jvm.JvmName 24 | 25 | /** 26 | * Replaces all opening and closing tags (`<` and `>`) 27 | * with their escaped sequences (`<` and `>`). 28 | */ 29 | internal fun String.htmlEntities(): String = 30 | replace("<", "<").replace(">", ">") 31 | 32 | /** 33 | * Normalizes a string by escaping some HTML meta characters 34 | * and replacing tabs with 4 spaces each. 35 | */ 36 | internal fun String.normalize(): String = 37 | htmlEntities().replace("\t", " ") 38 | 39 | /** 40 | * Wrap the text with the given column width. 41 | */ 42 | internal fun String.wrapText(columnWidth: Int): String { 43 | require(columnWidth >= 0) { "Column width must be greater than or equal to 0" } 44 | 45 | if (columnWidth == 0) { 46 | return this 47 | } 48 | 49 | val length = length 50 | val delimiterLength = "
".length 51 | var widthIndex = columnWidth 52 | val sb = StringBuilder(this) 53 | var count = 0 54 | 55 | while (length > widthIndex) { 56 | var breakPoint = widthIndex + delimiterLength * count 57 | 58 | if (sb[breakPoint - 1].isHighSurrogate() && sb[breakPoint].isLowSurrogate()) { 59 | // Shift a breakpoint that would split a supplemental code-point. 60 | breakPoint += 1 61 | 62 | if (breakPoint == sb.length) { 63 | // Break before instead of after if this is the last code-point. 64 | breakPoint -= 2 65 | } 66 | } 67 | 68 | sb.insert(breakPoint, "
") 69 | widthIndex += columnWidth 70 | count++ 71 | } 72 | 73 | return sb.toString() 74 | } 75 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/Change.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2017 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.algorithm 20 | 21 | import io.github.petertrr.diffutils.patch.DeltaType 22 | import kotlin.jvm.JvmField 23 | 24 | public data class Change( 25 | @JvmField val deltaType: DeltaType, 26 | @JvmField val startOriginal: Int, 27 | @JvmField val endOriginal: Int, 28 | @JvmField val startRevised: Int, 29 | @JvmField val endRevised: Int, 30 | ) 31 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/DiffAlgorithm.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2018 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.algorithm 20 | 21 | /** 22 | * Describes a diff algorithm. 23 | * 24 | * @param T The type of data that should be diffed 25 | */ 26 | public interface DiffAlgorithm { 27 | /** 28 | * Computes the changeset to patch the [source] list to the [target] list. 29 | */ 30 | public fun computeDiff( 31 | source: List, 32 | target: List, 33 | progress: DiffAlgorithmListener = NoopAlgorithmListener(), 34 | ): List 35 | } 36 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/DiffAlgorithmListener.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2018 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.algorithm 20 | 21 | public interface DiffAlgorithmListener { 22 | /** 23 | * Notifies computing a diff has started. 24 | */ 25 | public fun diffStart() 26 | 27 | /** 28 | * Notifies of a step within the diff algorithm. 29 | * 30 | * Due to different implementations the value is not strict incrementing 31 | * to the max and is not guarantee to reach the max. It could stop before. 32 | */ 33 | public fun diffStep(value: Int, max: Int) 34 | 35 | /** 36 | * Notifies computing a diff has ended. 37 | */ 38 | public fun diffEnd() 39 | } 40 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/DiffEqualizer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.github.petertrr.diffutils.algorithm 17 | 18 | public fun interface DiffEqualizer { 19 | public fun test(one: T, two: T): Boolean 20 | } 21 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/EqualsDiffEqualizer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.github.petertrr.diffutils.algorithm 17 | 18 | public class EqualsDiffEqualizer : DiffEqualizer { 19 | override fun test(one: T, two: T): Boolean = 20 | one == two 21 | } 22 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/IgnoreWsStringDiffEqualizer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.github.petertrr.diffutils.algorithm 17 | 18 | public class IgnoreWsStringDiffEqualizer : DiffEqualizer { 19 | private companion object { 20 | private val ws = Regex("\\s+") 21 | } 22 | 23 | override fun test(one: String, two: String): Boolean = 24 | ws.replace(one.trim(), " ") == ws.replace(two.trim(), " ") 25 | } 26 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/NoopAlgorithmListener.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 17 | */ 18 | package io.github.petertrr.diffutils.algorithm 19 | 20 | /** 21 | * A diff algorithm progress listener that does nothing. 22 | */ 23 | public class NoopAlgorithmListener : DiffAlgorithmListener { 24 | override fun diffStart() { 25 | // Noop 26 | } 27 | 28 | override fun diffStep(value: Int, max: Int) { 29 | // Noop 30 | } 31 | 32 | override fun diffEnd() { 33 | // Noop 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/myers/MyersDiff.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2009-2017 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.algorithm.myers 20 | 21 | import io.github.petertrr.diffutils.algorithm.Change 22 | import io.github.petertrr.diffutils.algorithm.DiffAlgorithm 23 | import io.github.petertrr.diffutils.algorithm.DiffAlgorithmListener 24 | import io.github.petertrr.diffutils.algorithm.DiffEqualizer 25 | import io.github.petertrr.diffutils.algorithm.EqualsDiffEqualizer 26 | import io.github.petertrr.diffutils.patch.DeltaType 27 | 28 | /** 29 | * A clean-room implementation of Eugene Myers greedy differencing algorithm. 30 | */ 31 | public class MyersDiff(private val equalizer: DiffEqualizer = EqualsDiffEqualizer()) : DiffAlgorithm { 32 | /** 33 | * Returns an empty diff if we get an error while procession the difference. 34 | */ 35 | override fun computeDiff(source: List, target: List, progress: DiffAlgorithmListener): List { 36 | progress.diffStart() 37 | 38 | val path = buildPath(source, target, progress) ?: error("Expected a non-null path node") 39 | val result = buildRevision(path) 40 | 41 | progress.diffEnd() 42 | return result 43 | } 44 | 45 | /** 46 | * Computes the minimum diffpath that expresses the differences between the original and revised 47 | * sequences, according to Eugene Myers differencing algorithm. 48 | * 49 | * @param orig The original sequence 50 | * @param rev The revised sequence 51 | * @return A minimum [PathNode] across the differences graph 52 | * @throws IllegalStateException If a diff path could not be found 53 | */ 54 | private fun buildPath(orig: List, rev: List, progress: DiffAlgorithmListener): PathNode? { 55 | // These are local constants 56 | val origSize = orig.size 57 | val revSize = rev.size 58 | val max = origSize + revSize + 1 59 | val size = 1 + 2 * max 60 | val middle = size / 2 61 | val diagonal = arrayOfNulls(size) 62 | diagonal[middle + 1] = PathNode(0, -1, snake = true, bootstrap = true, prev = null) 63 | 64 | for (d in 0..= origSize && j >= revSize) { 99 | return diagonal[kmiddle] 100 | } 101 | 102 | k += 2 103 | } 104 | 105 | diagonal[middle + d - 1] = null 106 | } 107 | 108 | error("Could not find a diff path") 109 | } 110 | 111 | /** 112 | * Constructs a patch from a difference path. 113 | * 114 | * @param actualPath The path 115 | * @return A list of [Change]s corresponding to the path 116 | * @throws IllegalStateException If a patch could not be built from the given path 117 | */ 118 | private fun buildRevision(actualPath: PathNode): List { 119 | var path = if (actualPath.snake) { 120 | actualPath.prev 121 | } else { 122 | actualPath 123 | } 124 | 125 | val changes = ArrayList() 126 | 127 | while (path != null) { 128 | val prevPath = path.prev 129 | 130 | if (prevPath == null || prevPath.j < 0) { 131 | break 132 | } 133 | 134 | check(!path.snake) { "Bad diffpath: found snake when looking for diff" } 135 | 136 | val i = path.i 137 | val j = path.j 138 | 139 | path = prevPath 140 | 141 | val iAnchor = path.i 142 | val jAnchor = path.j 143 | 144 | if (iAnchor == i && jAnchor != j) { 145 | changes.add(Change(DeltaType.INSERT, iAnchor, i, jAnchor, j)) 146 | } else if (iAnchor != i && jAnchor == j) { 147 | changes.add(Change(DeltaType.DELETE, iAnchor, i, jAnchor, j)) 148 | } else { 149 | changes.add(Change(DeltaType.CHANGE, iAnchor, i, jAnchor, j)) 150 | } 151 | 152 | if (path.snake) { 153 | path = path.prev 154 | } 155 | } 156 | 157 | return changes 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/myers/MyersDiffWithLinearSpace.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * Copyright 2009-2021 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.algorithm.myers 20 | 21 | import io.github.petertrr.diffutils.algorithm.Change 22 | import io.github.petertrr.diffutils.algorithm.DiffAlgorithm 23 | import io.github.petertrr.diffutils.algorithm.DiffAlgorithmListener 24 | import io.github.petertrr.diffutils.algorithm.DiffEqualizer 25 | import io.github.petertrr.diffutils.algorithm.EqualsDiffEqualizer 26 | import io.github.petertrr.diffutils.patch.DeltaType 27 | import kotlin.jvm.JvmField 28 | 29 | public class MyersDiffWithLinearSpace( 30 | private val equalizer: DiffEqualizer = EqualsDiffEqualizer(), 31 | ) : DiffAlgorithm { 32 | public override fun computeDiff(source: List, target: List, progress: DiffAlgorithmListener): List { 33 | progress.diffStart() 34 | 35 | val data = DiffData(source, target) 36 | val maxIdx = source.size + target.size 37 | val progressWrapper = DelegateAlgorithmListener(maxIdx, progress) 38 | buildScript(data, 0, source.size, 0, target.size, progressWrapper) 39 | 40 | progress.diffEnd() 41 | return data.script 42 | } 43 | 44 | private fun buildScript( 45 | data: DiffData, 46 | start1: Int, 47 | end1: Int, 48 | start2: Int, 49 | end2: Int, 50 | progress: DiffAlgorithmListener, 51 | ) { 52 | progress.diffStep((end1 - start1) / 2 + (end2 - start2) / 2, -1) 53 | val middle = getMiddleSnake(data, start1, end1, start2, end2) 54 | 55 | if (middle == null || 56 | middle.start == end1 && middle.diag == end1 - end2 || 57 | middle.end == start1 && middle.diag == start1 - start2 58 | ) { 59 | var i = start1 60 | var j = start2 61 | 62 | while (i < end1 || j < end2) { 63 | if (i < end1 && j < end2 && equalizer.test(data.source[i], data.target[j])) { 64 | ++i 65 | ++j 66 | } else { 67 | // index is less than 0 here if data.script is empty 68 | val index = data.script.size - 1 69 | 70 | // TODO: compress these commands 71 | if (end1 - start1 > end2 - start2) { 72 | if (index < 0 || 73 | data.script[index].endOriginal != i || 74 | data.script[index].deltaType != DeltaType.DELETE 75 | ) { 76 | data.script.add(Change(DeltaType.DELETE, i, i + 1, j, j)) 77 | } else { 78 | data.script[index] = data.script[index].copy(endOriginal = i + 1) 79 | } 80 | 81 | ++i 82 | } else { 83 | if (index < 0 || 84 | data.script[index].endRevised != j || 85 | data.script[index].deltaType != DeltaType.INSERT 86 | ) { 87 | data.script.add(Change(DeltaType.INSERT, i, i, j, j + 1)) 88 | } else { 89 | data.script[index] = data.script[index].copy(endRevised = j + 1) 90 | } 91 | 92 | ++j 93 | } 94 | } 95 | } 96 | } else { 97 | buildScript(data, start1, middle.start, start2, middle.start - middle.diag, progress) 98 | buildScript(data, middle.end, end1, middle.end - middle.diag, end2, progress) 99 | } 100 | } 101 | 102 | private fun getMiddleSnake(data: DiffData, start1: Int, end1: Int, start2: Int, end2: Int): Snake? { 103 | val m = end1 - start1 104 | val n = end2 - start2 105 | 106 | if (m == 0 || n == 0) { 107 | return null 108 | } 109 | 110 | val delta = m - n 111 | val sum = n + m 112 | val offset = (if (sum % 2 == 0) sum else sum + 1) / 2 113 | data.vDown[1 + offset] = start1 114 | data.vUp[1 + offset] = end1 + 1 115 | 116 | for (d in 0..offset) { 117 | // Down 118 | var k = -d 119 | 120 | while (k <= d) { 121 | // First step 122 | val i = k + offset 123 | 124 | if (k == -d || k != d && data.vDown[i - 1] < data.vDown[i + 1]) { 125 | data.vDown[i] = data.vDown[i + 1] 126 | } else { 127 | data.vDown[i] = data.vDown[i - 1] + 1 128 | } 129 | 130 | var x = data.vDown[i] 131 | var y = x - start1 + start2 - k 132 | 133 | while (x < end1 && y < end2 && equalizer.test(data.source[x], data.target[y])) { 134 | data.vDown[i] = ++x 135 | ++y 136 | } 137 | 138 | // Second step 139 | if (delta % 2 != 0 && delta - d <= k && k <= delta + d) { 140 | if (data.vUp[i - delta] <= data.vDown[i]) { 141 | return buildSnake(data, data.vUp[i - delta], k + start1 - start2, end1, end2) 142 | } 143 | } 144 | 145 | k += 2 146 | } 147 | 148 | // Up 149 | k = delta - d 150 | 151 | while (k <= delta + d) { 152 | // First step 153 | val i = k + offset - delta 154 | 155 | if (k == delta - d || k != delta + d && data.vUp[i + 1] <= data.vUp[i - 1]) { 156 | data.vUp[i] = data.vUp[i + 1] - 1 157 | } else { 158 | data.vUp[i] = data.vUp[i - 1] 159 | } 160 | 161 | var x = data.vUp[i] - 1 162 | var y = x - start1 + start2 - k 163 | 164 | while (x >= start1 && y >= start2 && equalizer.test(data.source[x], data.target[y])) { 165 | data.vUp[i] = x-- 166 | y-- 167 | } 168 | 169 | // Second step 170 | if (delta % 2 == 0 && -d <= k && k <= d) { 171 | if (data.vUp[i] <= data.vDown[i + delta]) { 172 | return buildSnake(data, data.vUp[i], k + start1 - start2, end1, end2) 173 | } 174 | } 175 | 176 | k += 2 177 | } 178 | } 179 | 180 | // According to Myers, this cannot happen 181 | error("Could not find a diff path") 182 | } 183 | 184 | private fun buildSnake(data: DiffData, start: Int, diag: Int, end1: Int, end2: Int): Snake { 185 | var end = start 186 | 187 | while (end - diag < end2 && end < end1 && equalizer.test(data.source[end], data.target[end - diag])) { 188 | ++end 189 | } 190 | 191 | return Snake(start, end, diag) 192 | } 193 | 194 | private class DelegateAlgorithmListener( 195 | val maxIdx: Int, 196 | val delegate: DiffAlgorithmListener, 197 | ) : DiffAlgorithmListener by delegate { 198 | override fun diffStep(value: Int, max: Int) { 199 | delegate.diffStep(value, maxIdx) 200 | } 201 | } 202 | 203 | private class DiffData( 204 | @JvmField val source: List, 205 | @JvmField val target: List, 206 | ) { 207 | @JvmField 208 | val size = source.size + target.size + 2 209 | 210 | @JvmField 211 | val vDown = IntArray(size) 212 | 213 | @JvmField 214 | val vUp = IntArray(size) 215 | 216 | @JvmField 217 | val script = ArrayList() 218 | } 219 | 220 | private class Snake( 221 | @JvmField val start: Int, 222 | @JvmField val end: Int, 223 | @JvmField val diag: Int, 224 | ) 225 | } 226 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/myers/PathNode.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2009-2017 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.algorithm.myers 20 | 21 | import kotlin.jvm.JvmField 22 | 23 | /** 24 | * A node in a diffpath. 25 | * 26 | * @param i Position in the original sequence 27 | * @param j Position in the revised sequence 28 | * @param snake 29 | * @param bootstrap Is this a bootstrap node? 30 | * In bootstrap nodes one of the two coordinates is less than zero. 31 | * @param prev The previous node in the path, if any 32 | */ 33 | internal class PathNode( 34 | @JvmField val i: Int, 35 | @JvmField val j: Int, 36 | @JvmField val snake: Boolean, 37 | @JvmField val bootstrap: Boolean, 38 | prev: PathNode? = null, 39 | ) { 40 | /** 41 | * The previous node in the path. 42 | */ 43 | @JvmField 44 | val prev: PathNode? = if (snake) prev else prev?.previousSnake() 45 | 46 | /** 47 | * Skips sequences of [PathNodes][PathNode] until a snake or bootstrap node is found, or the end of the 48 | * path is reached. 49 | * 50 | * @return The next first [PathNode] or bootstrap node in the path, or `null` if none found 51 | */ 52 | @Suppress("MemberVisibilityCanBePrivate") 53 | fun previousSnake(): PathNode? { 54 | if (bootstrap) { 55 | return null 56 | } 57 | 58 | return if (!snake && prev != null) prev.previousSnake() else this 59 | } 60 | 61 | override fun toString(): String { 62 | val buf = StringBuilder("[") 63 | var node: PathNode? = this 64 | 65 | while (node != null) { 66 | buf.append("(") 67 | buf.append(node.i) 68 | buf.append(",") 69 | buf.append(node.j) 70 | buf.append(")") 71 | node = node.prev 72 | } 73 | 74 | buf.append("]") 75 | return buf.toString() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/patch/ChangeDelta.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2018 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.patch 20 | 21 | public data class ChangeDelta( 22 | override val source: Chunk, 23 | override val target: Chunk, 24 | ) : Delta(DeltaType.CHANGE) { 25 | override fun applyTo(target: MutableList) { 26 | val position = source.position 27 | val size = source.size() 28 | 29 | for (i in 0..) { 39 | val position = this.target.position 40 | val size = this.target.size() 41 | 42 | for (i in 0.., revised: Chunk): Delta = 52 | ChangeDelta(original, revised) 53 | 54 | override fun toString(): String = 55 | "[ChangeDelta, position: ${source.position}, lines: ${source.lines} to ${target.lines}]" 56 | } 57 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/patch/Chunk.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2009-2017 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.patch 20 | 21 | import kotlin.jvm.JvmField 22 | 23 | /** 24 | * Holds the information about the part of text involved in the diff process. 25 | * 26 | * Text is represented as generic class `T` because the diff engine is capable of handling more 27 | * than plain ASCII. In fact, arrays or lists of any type that implements 28 | * `hashCode()` and `equals()` correctly can be subject to differencing using this library. 29 | * 30 | * @param position The start position of chunk in the text 31 | * @param lines The affected lines 32 | * @param changePosition The positions of changed lines of chunk in the text 33 | * @param T The type of the compared elements in the 'lines' 34 | */ 35 | public data class Chunk( 36 | @JvmField val position: Int, 37 | @JvmField val lines: List, 38 | @JvmField val changePosition: List? = null, 39 | ) { 40 | /** 41 | * Verifies that this chunk's saved text matches the corresponding text in the given sequence. 42 | * 43 | * @param target The sequence to verify against 44 | */ 45 | public fun verifyChunk(target: List): VerifyChunk { 46 | val targetSize = target.size 47 | 48 | if (position > targetSize || last() > targetSize) { 49 | return VerifyChunk.POSITION_OUT_OF_TARGET 50 | } 51 | 52 | for (i in 0.. { 22 | public fun processConflict(verifyChunk: VerifyChunk, delta: Delta, result: MutableList) 23 | } 24 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/patch/ConflictProducingConflictOutput.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2021 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.patch 20 | 21 | public class ConflictProducingConflictOutput : ConflictOutput { 22 | override fun processConflict(verifyChunk: VerifyChunk, delta: Delta, result: MutableList) { 23 | if (result.size <= delta.source.position) { 24 | throw UnsupportedOperationException("Not supported yet") 25 | } 26 | 27 | if (verifyChunk != VerifyChunk.OK) { 28 | val orgData = ArrayList() 29 | 30 | repeat(delta.source.size()) { 31 | orgData.add(result.removeAt(delta.source.position)) 32 | } 33 | 34 | orgData.add(0, "<<<<<< HEAD") 35 | orgData.add("======") 36 | orgData.addAll(delta.source.lines) 37 | orgData.add(">>>>>>> PATCH") 38 | 39 | result.addAll(delta.source.position, orgData) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/patch/DeleteDelta.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2018 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.patch 20 | 21 | public data class DeleteDelta( 22 | override val source: Chunk, 23 | override val target: Chunk, 24 | ) : Delta(DeltaType.DELETE) { 25 | override fun applyTo(target: MutableList) { 26 | val position = source.position 27 | 28 | for (i in 0..) { 34 | val position = this.target.position 35 | val lines = this.source.lines 36 | 37 | for ((i, line) in lines.withIndex()) { 38 | target.add(position + i, line) 39 | } 40 | } 41 | 42 | override fun withChunks(original: Chunk, revised: Chunk): Delta = 43 | DeleteDelta(original, revised) 44 | 45 | override fun toString(): String = 46 | "[DeleteDelta, position: ${source.position}, lines: ${source.lines}]" 47 | } 48 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/patch/Delta.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2018 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.patch 20 | 21 | /** 22 | * A delta between a source and a target. 23 | */ 24 | public sealed class Delta(public val type: DeltaType) { 25 | public abstract val source: Chunk 26 | public abstract val target: Chunk 27 | 28 | /** 29 | * Verify the chunk of this delta, to fit the target. 30 | */ 31 | protected open fun verifyChunkToFitTarget(target: List): VerifyChunk = 32 | source.verifyChunk(target) 33 | 34 | public open fun verifyAndApplyTo(target: MutableList): VerifyChunk { 35 | val verify = verifyChunkToFitTarget(target) 36 | 37 | if (verify == VerifyChunk.OK) { 38 | applyTo(target) 39 | } 40 | 41 | return verify 42 | } 43 | 44 | protected abstract fun applyTo(target: MutableList) 45 | 46 | public abstract fun restore(target: MutableList) 47 | 48 | /** 49 | * Create a new delta of the actual instance with customized chunk data. 50 | */ 51 | public abstract fun withChunks(original: Chunk, revised: Chunk): Delta 52 | } 53 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/patch/DeltaType.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2018 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.patch 20 | 21 | /** 22 | * Specifies the type of the delta. There are three types of modifications from 23 | * the original to get the revised text. 24 | * 25 | * - CHANGE: a block of data of the original is replaced by another block of data. 26 | * - DELETE: a block of data of the original is removed 27 | * - INSERT: at a position of the original a block of data is inserted 28 | * 29 | * To be complete there is also 30 | * 31 | * - EQUAL: a block of data of original and the revised text is equal 32 | * 33 | * which is no change at all. 34 | */ 35 | public enum class DeltaType { 36 | /** 37 | * A change in the original. 38 | */ 39 | CHANGE, 40 | 41 | /** 42 | * A delete from the original. 43 | */ 44 | DELETE, 45 | 46 | /** 47 | * An insert into the original. 48 | */ 49 | INSERT, 50 | 51 | /** 52 | * Do nothing. 53 | */ 54 | EQUAL, 55 | } 56 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/patch/DiffException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2009-2017 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.patch 20 | 21 | public abstract class DiffException(message: String) : Exception(message) 22 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/patch/EqualDelta.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2018 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.patch 20 | 21 | public data class EqualDelta( 22 | override val source: Chunk, 23 | override val target: Chunk, 24 | ) : Delta(DeltaType.EQUAL) { 25 | override fun applyTo(target: MutableList) { 26 | // Noop 27 | } 28 | 29 | override fun restore(target: MutableList) { 30 | // Noop 31 | } 32 | 33 | override fun withChunks(original: Chunk, revised: Chunk): Delta = 34 | EqualDelta(original, revised) 35 | 36 | override fun toString(): String = 37 | "[EqualDelta, position: ${source.position}, lines: ${source.lines}]" 38 | } 39 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/patch/ExceptionProducingConflictOutput.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2021 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.patch 20 | 21 | public class ExceptionProducingConflictOutput : ConflictOutput { 22 | override fun processConflict(verifyChunk: VerifyChunk, delta: Delta, result: MutableList) { 23 | if (verifyChunk != VerifyChunk.OK) { 24 | throw PatchFailedException("Could not apply patch due to: $verifyChunk") 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/patch/InsertDelta.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2018 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.patch 20 | 21 | public data class InsertDelta( 22 | override val source: Chunk, 23 | override val target: Chunk, 24 | ) : Delta(DeltaType.INSERT) { 25 | override fun applyTo(target: MutableList) { 26 | val position = this.source.position 27 | 28 | for ((i, line) in this.target.lines.withIndex()) { 29 | target.add(position + i, line) 30 | } 31 | } 32 | 33 | override fun restore(target: MutableList) { 34 | val position = this.target.position 35 | val size = this.target.size() 36 | 37 | for (i in 0.., revised: Chunk): Delta = 43 | InsertDelta(original, revised) 44 | 45 | override fun toString(): String = 46 | "[InsertDelta, position: ${source.position}, lines: ${target.lines}]" 47 | } 48 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/patch/Patch.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2009-2017 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.patch 20 | 21 | import io.github.petertrr.diffutils.algorithm.Change 22 | import kotlin.jvm.JvmOverloads 23 | import kotlin.jvm.JvmStatic 24 | 25 | /** 26 | * Describes the patch holding all deltas between the original and revised texts. 27 | * 28 | * @param conflictOutput Alter normal conflict output behaviour to e.g. include 29 | * some conflict statements in the result, like Git does it. 30 | * @param T The type of the compared elements in the 'lines'. 31 | */ 32 | public class Patch(private var conflictOutput: ConflictOutput = ExceptionProducingConflictOutput()) { 33 | public var deltas: MutableList> = ArrayList() 34 | get() { 35 | field.sortBy { it.source.position } 36 | return field 37 | } 38 | 39 | /** 40 | * Apply this patch to the given target list, returning a new list. 41 | * 42 | * @return The patched text 43 | * @throws PatchFailedException If the patch cannot be applied 44 | */ 45 | public fun applyTo(target: List): List { 46 | val result = target.toMutableList() 47 | applyToExisting(result) 48 | return result 49 | } 50 | 51 | /** 52 | * Apply this patch to the given target list, directly modifying it. 53 | * 54 | * @return The patched text 55 | * @throws PatchFailedException If the patch cannot be applied 56 | */ 57 | @Suppress("MemberVisibilityCanBePrivate") 58 | public fun applyToExisting(target: MutableList) { 59 | val it = deltas.listIterator(deltas.size) 60 | 61 | while (it.hasPrevious()) { 62 | val delta = it.previous() 63 | val verifyChunk = delta.verifyAndApplyTo(target) 64 | conflictOutput.processConflict(verifyChunk, delta, target) 65 | } 66 | } 67 | 68 | /** 69 | * Creates a new list, containing the restored state of the given target list. 70 | * Opposite of the [applyTo] method. 71 | * 72 | * @param target The given target 73 | * @return The restored text 74 | */ 75 | public fun restore(target: List): List { 76 | val result = target.toMutableList() 77 | restoreToExisting(result) 78 | return result 79 | } 80 | 81 | /** 82 | * Restores all changes within the given target list. 83 | * Opposite of the [applyToExisting] method. 84 | * 85 | * @param target The given target 86 | * @return The restored text 87 | */ 88 | @Suppress("MemberVisibilityCanBePrivate") 89 | public fun restoreToExisting(target: MutableList) { 90 | val it = deltas.listIterator(deltas.size) 91 | 92 | while (it.hasPrevious()) { 93 | val delta = it.previous() 94 | delta.restore(target) 95 | } 96 | } 97 | 98 | /** 99 | * Add the given delta to this patch. 100 | * 101 | * @param delta The delta to add 102 | */ 103 | public fun addDelta(delta: Delta): Boolean = 104 | deltas.add(delta) 105 | 106 | override fun toString(): String = 107 | "Patch{deltas=$deltas}" 108 | 109 | public fun withConflictOutput(conflictOutput: ConflictOutput) { 110 | this.conflictOutput = conflictOutput 111 | } 112 | 113 | public companion object { 114 | @JvmStatic 115 | @JvmOverloads 116 | public fun generate( 117 | original: List, 118 | revised: List, 119 | changes: List, 120 | includeEquals: Boolean = false, 121 | ): Patch { 122 | val patch = Patch() 123 | var startOriginal = 0 124 | var startRevised = 0 125 | 126 | val adjustedChanges = if (includeEquals) { 127 | changes.sortedBy(Change::startOriginal) 128 | } else { 129 | changes 130 | } 131 | 132 | for (change in adjustedChanges) { 133 | if (includeEquals && startOriginal < change.startOriginal) { 134 | patch.addDelta( 135 | EqualDelta( 136 | buildChunk(startOriginal, change.startOriginal, original), 137 | buildChunk(startRevised, change.startRevised, revised), 138 | ) 139 | ) 140 | } 141 | 142 | val orgChunk = buildChunk(change.startOriginal, change.endOriginal, original) 143 | val revChunk = buildChunk(change.startRevised, change.endRevised, revised) 144 | 145 | when (change.deltaType) { 146 | DeltaType.DELETE -> patch.addDelta(DeleteDelta(orgChunk, revChunk)) 147 | DeltaType.INSERT -> patch.addDelta(InsertDelta(orgChunk, revChunk)) 148 | DeltaType.CHANGE -> patch.addDelta(ChangeDelta(orgChunk, revChunk)) 149 | DeltaType.EQUAL -> {} 150 | } 151 | 152 | startOriginal = change.endOriginal 153 | startRevised = change.endRevised 154 | } 155 | 156 | if (includeEquals && startOriginal < original.size) { 157 | patch.addDelta( 158 | EqualDelta( 159 | buildChunk(startOriginal, original.size, original), 160 | buildChunk(startRevised, revised.size, revised), 161 | ) 162 | ) 163 | } 164 | 165 | return patch 166 | } 167 | 168 | private fun buildChunk(start: Int, end: Int, data: List): Chunk = 169 | Chunk(start, data.slice(start.. { 20 | val list = ArrayList(line.length) 21 | 22 | for (character in line.toCharArray()) { 23 | list.add(character.toString()) 24 | } 25 | 26 | return list 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/text/DiffLineNormalizer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.github.petertrr.diffutils.text 17 | 18 | public fun interface DiffLineNormalizer { 19 | public fun normalize(line: String): String 20 | } 21 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/text/DiffLineProcessor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.github.petertrr.diffutils.text 17 | 18 | public fun interface DiffLineProcessor { 19 | public fun process(line: String): String 20 | } 21 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/text/DiffRow.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2009-2017 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.text 20 | 21 | import kotlin.jvm.JvmField 22 | 23 | /** 24 | * Describes the diff row in form `[tag, oldLine, newLine]` for showing the difference between two texts. 25 | */ 26 | public data class DiffRow( 27 | @JvmField val tag: Tag, 28 | @JvmField val oldLine: String, 29 | @JvmField val newLine: String, 30 | ) { 31 | public enum class Tag { 32 | INSERT, 33 | DELETE, 34 | CHANGE, 35 | EQUAL, 36 | } 37 | 38 | override fun toString(): String = 39 | "[$tag, $oldLine, $newLine]" 40 | } 41 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/text/DiffRowGenerator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2009-2017 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.text 20 | 21 | import io.github.petertrr.diffutils.algorithm.DiffAlgorithm 22 | import io.github.petertrr.diffutils.algorithm.EqualsDiffEqualizer 23 | import io.github.petertrr.diffutils.algorithm.IgnoreWsStringDiffEqualizer 24 | import io.github.petertrr.diffutils.algorithm.myers.MyersDiff 25 | import io.github.petertrr.diffutils.diff 26 | import io.github.petertrr.diffutils.patch.ChangeDelta 27 | import io.github.petertrr.diffutils.patch.Chunk 28 | import io.github.petertrr.diffutils.patch.DeleteDelta 29 | import io.github.petertrr.diffutils.patch.Delta 30 | import io.github.petertrr.diffutils.patch.DeltaType 31 | import io.github.petertrr.diffutils.patch.InsertDelta 32 | import io.github.petertrr.diffutils.patch.Patch 33 | import io.github.petertrr.diffutils.wrapText 34 | import kotlin.math.max 35 | import kotlin.math.min 36 | 37 | /** 38 | * This class for generating [DiffRow]s for side-by-side view. You can customize 39 | * the way of generating. For example, show inline diffs on not, ignoring white 40 | * spaces or/and blank lines and so on. All parameters for generating are 41 | * optional. If you do not specify them, the class will use the default values. 42 | * 43 | * @param columnWidth Set the column width of generated lines of original and revised texts. 44 | * Making it < 0 doesn't make any sense. 45 | * @param ignoreWhiteSpaces Ignore white spaces in generating diff rows or not 46 | * @param algorithm The diffing algorithm to use. By default [MyersDiff]. 47 | * @param inlineDiffByWord Per default each character is separately processed. 48 | * Setting this parameter to `true` introduces processing by word, which does 49 | * not deliver in word changes. Therefore, the whole word will be tagged as changed: 50 | * ``` 51 | * false: (aBa : aba) -- changed: a(B)a : a(b)a 52 | * true: (aBa : aba) -- changed: (aBa) : (aba) 53 | * ``` 54 | * Default: `false` 55 | * @param inlineDiffSplitter To provide some customized splitting a splitter can be provided. 56 | * Here someone could think about sentence splitter, comma splitter or stuff like that. 57 | * @param mergeOriginalRevised Merge the complete result within the original text. 58 | * This makes sense for one line display. 59 | * Default: `false` 60 | * @param newTag Generator for New-Text-Tags 61 | * @param oldTag Generator for Old-Text-Tags 62 | * @param reportLinesUnchanged Report all lines without markup on the old or new text. 63 | * Default: `false` 64 | * @param lineNormalizer By default, [DiffRowGenerator] preprocesses lines for HTML output. 65 | * Tabs and special HTML characters like "<" are replaced with its encoded value. 66 | * To change this you can provide a customized line normalizer here. 67 | * @param processDiffs Optional processor for diffed text parts. 68 | * Here e.g. white characters could be replaced by something visible. 69 | * @param showInlineDiffs Show inline diffs in generating diff rows or not. 70 | * Default: `false` 71 | * @param replaceOriginalLinefeedInChangesWithSpaces Sometimes it happens that a change 72 | * contains multiple lines. If there is no correspondence in old and new. 73 | * To keep the merged line more readable the line feeds could be replaced by spaces. 74 | * Default: `false` 75 | * @param decompressDeltas Deltas could be in a state, that would produce some unreasonable 76 | * results within an inline diff. So the deltas are decompressed into smaller parts and rebuild. 77 | * But this could result in more differences. 78 | * Default: `true` 79 | */ 80 | @Suppress("LongParameterList") 81 | public class DiffRowGenerator( 82 | private val columnWidth: Int = 80, 83 | private val ignoreWhiteSpaces: Boolean = false, 84 | private val algorithm: DiffAlgorithm = defaultAlgorithm(ignoreWhiteSpaces), 85 | inlineDiffByWord: Boolean = false, 86 | private val inlineDiffSplitter: DiffSplitter = defaultSplitter(inlineDiffByWord), 87 | private val mergeOriginalRevised: Boolean = false, 88 | private val newTag: DiffTagGenerator = HtmlDiffTagGenerator("editNewInline"), 89 | private val oldTag: DiffTagGenerator = HtmlDiffTagGenerator("editOldInline"), 90 | private val reportLinesUnchanged: Boolean = false, 91 | private val lineNormalizer: DiffLineNormalizer = HtmlLineNormalizer(), 92 | private val processDiffs: DiffLineProcessor? = null, 93 | private val showInlineDiffs: Boolean = false, 94 | private val replaceOriginalLinefeedInChangesWithSpaces: Boolean = false, 95 | private val decompressDeltas: Boolean = true, 96 | ) { 97 | /** 98 | * Get the [DiffRow]s describing the difference between original and revised 99 | * texts. Useful for displaying side-by-side diff. 100 | * 101 | * @param original The original text 102 | * @param revised The revised text 103 | * @return The [DiffRow]s between original and revised texts 104 | */ 105 | public fun generateDiffRows(original: List, revised: List): List = 106 | generateDiffRows(original, diff(original, revised, algorithm)) 107 | 108 | /** 109 | * Generates the [DiffRow]s describing the difference between original and 110 | * revised texts using the given patch. Useful for displaying side-by-side 111 | * diff. 112 | * 113 | * @param original The original text 114 | * @param patch The given patch 115 | * @return The [DiffRow]s between original and revised texts 116 | */ 117 | @Suppress("MemberVisibilityCanBePrivate") 118 | public fun generateDiffRows(original: List, patch: Patch): List { 119 | val diffRows = ArrayList() 120 | var endPos = 0 121 | 122 | if (decompressDeltas) { 123 | for (originalDelta in patch.deltas) { 124 | for (delta in decompressDeltas(originalDelta)) { 125 | endPos = transformDeltaIntoDiffRow(original, endPos, diffRows, delta) 126 | } 127 | } 128 | } else { 129 | for (delta in patch.deltas) { 130 | endPos = transformDeltaIntoDiffRow(original, endPos, diffRows, delta) 131 | } 132 | } 133 | 134 | // Copy the final matching chunk if any 135 | for (line in original.subList(endPos, original.size)) { 136 | diffRows.add(buildDiffRow(DiffRow.Tag.EQUAL, line, line)) 137 | } 138 | 139 | return diffRows 140 | } 141 | 142 | private fun normalizeLines(list: List): List = 143 | if (reportLinesUnchanged) list else list.map(lineNormalizer::normalize) 144 | 145 | /** 146 | * Transforms one patch delta into a [DiffRow] object. 147 | * 148 | * @param endPos Line number after previous delta end 149 | */ 150 | private fun transformDeltaIntoDiffRow( 151 | original: List, 152 | endPos: Int, 153 | diffRows: MutableList, 154 | delta: Delta, 155 | ): Int { 156 | val orig = delta.source 157 | val rev = delta.target 158 | 159 | for (line in original.subList(endPos, orig.position)) { 160 | // All lines since previous delta until the start of the current delta 161 | diffRows.add(buildDiffRow(DiffRow.Tag.EQUAL, line, line)) 162 | } 163 | 164 | when (delta.type) { 165 | DeltaType.INSERT -> { 166 | for (line in rev.lines) { 167 | val row = buildDiffRow(DiffRow.Tag.INSERT, "", line) 168 | diffRows.add(row) 169 | } 170 | } 171 | DeltaType.DELETE -> { 172 | for (line in orig.lines) { 173 | val row = buildDiffRow(DiffRow.Tag.DELETE, line, "") 174 | diffRows.add(row) 175 | } 176 | } 177 | else -> { 178 | if (showInlineDiffs) { 179 | diffRows.addAll(generateInlineDiffs(delta)) 180 | } else { 181 | for (j in 0..): List> { 204 | if (delta.type == DeltaType.CHANGE && delta.source.size() != delta.target.size()) { 205 | val deltas = ArrayList>() 206 | val minSize = min(delta.source.size(), delta.target.size()) 207 | val orig = delta.source 208 | val rev = delta.target 209 | 210 | deltas.add( 211 | ChangeDelta( 212 | Chunk(orig.position, orig.lines.subList(0, minSize)), 213 | Chunk(rev.position, rev.lines.subList(0, minSize)), 214 | ) 215 | ) 216 | 217 | if (orig.lines.size < rev.lines.size) { 218 | deltas.add( 219 | InsertDelta( 220 | Chunk(orig.position + minSize, emptyList()), 221 | Chunk(rev.position + minSize, rev.lines.subList(minSize, rev.lines.size)), 222 | ) 223 | ) 224 | } else { 225 | deltas.add( 226 | DeleteDelta( 227 | Chunk(orig.position + minSize, orig.lines.subList(minSize, orig.lines.size)), 228 | Chunk(rev.position + minSize, emptyList()), 229 | ) 230 | ) 231 | } 232 | 233 | return deltas 234 | } 235 | 236 | return listOf(delta) 237 | } 238 | 239 | private fun buildDiffRow(type: DiffRow.Tag, orgLine: String, newLine: String): DiffRow { 240 | if (reportLinesUnchanged) { 241 | return DiffRow(type, orgLine, newLine) 242 | } 243 | 244 | var wrapOrg = preprocessLine(orgLine) 245 | 246 | if (DiffRow.Tag.DELETE == type) { 247 | if (mergeOriginalRevised || showInlineDiffs) { 248 | wrapOrg = oldTag.generateOpen(type) + wrapOrg + oldTag.generateClose(type) 249 | } 250 | } 251 | 252 | var wrapNew = preprocessLine(newLine) 253 | 254 | if (DiffRow.Tag.INSERT == type) { 255 | if (mergeOriginalRevised) { 256 | wrapOrg = newTag.generateOpen(type) + wrapNew + newTag.generateClose(type) 257 | } else if (showInlineDiffs) { 258 | wrapNew = newTag.generateOpen(type) + wrapNew + newTag.generateClose(type) 259 | } 260 | } 261 | 262 | return DiffRow(type, wrapOrg, wrapNew) 263 | } 264 | 265 | private fun buildDiffRowWithoutNormalizing(type: DiffRow.Tag, oldLine: String, newLine: String): DiffRow = 266 | DiffRow(type, oldLine.wrapText(columnWidth), newLine.wrapText(columnWidth)) 267 | 268 | /** 269 | * Add the inline diffs for given delta 270 | * 271 | * @param delta the given delta 272 | */ 273 | @Suppress("LongMethod") 274 | private fun generateInlineDiffs(delta: Delta): List { 275 | val orig = normalizeLines(delta.source.lines) 276 | val rev = normalizeLines(delta.target.lines) 277 | val joinedOrig = orig.joinToString("\n") 278 | val joinedRev = rev.joinToString("\n") 279 | val origList = inlineDiffSplitter.split(joinedOrig) 280 | val revList = inlineDiffSplitter.split(joinedRev) 281 | 282 | val diff = diff(origList, revList, algorithm) 283 | val inlineDeltas = diff.deltas.reversed() 284 | 285 | for (inlineDelta in inlineDeltas) { 286 | val inlineOrig = inlineDelta.source 287 | val inlineRev = inlineDelta.target 288 | 289 | when (inlineDelta.type) { 290 | DeltaType.DELETE -> { 291 | wrapInTag( 292 | sequence = origList, 293 | startPosition = inlineOrig.position, 294 | endPosition = inlineOrig.position + inlineOrig.size(), 295 | tag = DiffRow.Tag.DELETE, 296 | tagGenerator = oldTag, 297 | processDiffs = processDiffs, 298 | replaceLinefeedWithSpace = replaceOriginalLinefeedInChangesWithSpaces && mergeOriginalRevised, 299 | ) 300 | } 301 | DeltaType.INSERT -> { 302 | if (mergeOriginalRevised) { 303 | origList.addAll( 304 | inlineOrig.position, 305 | revList.subList(inlineRev.position, inlineRev.position + inlineRev.size()), 306 | ) 307 | wrapInTag( 308 | sequence = origList, 309 | startPosition = inlineOrig.position, 310 | endPosition = inlineOrig.position + inlineRev.size(), 311 | tag = DiffRow.Tag.INSERT, 312 | tagGenerator = newTag, 313 | processDiffs = processDiffs, 314 | replaceLinefeedWithSpace = false, 315 | ) 316 | } else { 317 | wrapInTag( 318 | sequence = revList, 319 | startPosition = inlineRev.position, 320 | endPosition = inlineRev.position + inlineRev.size(), 321 | tag = DiffRow.Tag.INSERT, 322 | tagGenerator = newTag, 323 | processDiffs = processDiffs, 324 | replaceLinefeedWithSpace = false, 325 | ) 326 | } 327 | } 328 | DeltaType.CHANGE -> { 329 | if (mergeOriginalRevised) { 330 | origList.addAll( 331 | inlineOrig.position + inlineOrig.size(), 332 | revList.subList(inlineRev.position, inlineRev.position + inlineRev.size()), 333 | ) 334 | wrapInTag( 335 | sequence = origList, 336 | startPosition = inlineOrig.position + inlineOrig.size(), 337 | endPosition = inlineOrig.position + inlineOrig.size() + inlineRev.size(), 338 | tag = DiffRow.Tag.CHANGE, 339 | tagGenerator = newTag, 340 | processDiffs = processDiffs, 341 | replaceLinefeedWithSpace = false, 342 | ) 343 | } else { 344 | wrapInTag( 345 | sequence = revList, 346 | startPosition = inlineRev.position, 347 | endPosition = inlineRev.position + inlineRev.size(), 348 | tag = DiffRow.Tag.CHANGE, 349 | tagGenerator = newTag, 350 | processDiffs = processDiffs, 351 | replaceLinefeedWithSpace = false, 352 | ) 353 | } 354 | wrapInTag( 355 | sequence = origList, 356 | startPosition = inlineOrig.position, 357 | endPosition = inlineOrig.position + inlineOrig.size(), 358 | tag = DiffRow.Tag.CHANGE, 359 | tagGenerator = oldTag, 360 | processDiffs = processDiffs, 361 | replaceLinefeedWithSpace = replaceOriginalLinefeedInChangesWithSpaces && mergeOriginalRevised, 362 | ) 363 | } 364 | else -> error("Unexpected delta type ${inlineDelta.type}") 365 | } 366 | } 367 | 368 | val origResult = StringBuilder(origList.size) 369 | val revResult = StringBuilder(revList.size) 370 | 371 | for (character in origList) { 372 | origResult.append(character) 373 | } 374 | 375 | for (character in revList) { 376 | revResult.append(character) 377 | } 378 | 379 | // Note: dropLastWhile here arose from compatibility with Java: Java's `split` discard 380 | // trailing empty string by default 381 | val original = origResult.split("\n").dropLastWhile(String::isEmpty) 382 | val revised = revResult.split("\n").dropLastWhile(String::isEmpty) 383 | 384 | val size = max(original.size, revised.size) 385 | val diffRows = ArrayList(size) 386 | 387 | for (j in 0.., 416 | startPosition: Int, 417 | endPosition: Int, 418 | tag: DiffRow.Tag, 419 | tagGenerator: DiffTagGenerator, 420 | processDiffs: DiffLineProcessor?, 421 | replaceLinefeedWithSpace: Boolean, 422 | ): List { 423 | var endPos = endPosition 424 | 425 | while (endPos >= startPosition) { 426 | // Search position for end tag 427 | while (endPos > startPosition) { 428 | if ("\n" != sequence[endPos - 1]) { 429 | break 430 | } else if (replaceLinefeedWithSpace) { 431 | sequence[endPos - 1] = " " 432 | break 433 | } 434 | 435 | endPos-- 436 | } 437 | 438 | if (endPos == startPosition) { 439 | break 440 | } 441 | 442 | sequence.add(endPos, tagGenerator.generateClose(tag)) 443 | 444 | if (processDiffs != null) { 445 | sequence[endPos - 1] = processDiffs.process(sequence[endPos - 1]) 446 | } 447 | 448 | endPos-- 449 | 450 | // Search position for end tag 451 | while (endPos > startPosition) { 452 | if ("\n" == sequence[endPos - 1]) { 453 | if (replaceLinefeedWithSpace) { 454 | sequence[endPos - 1] = " " 455 | } else { 456 | break 457 | } 458 | } 459 | 460 | if (processDiffs != null) { 461 | sequence[endPos - 1] = processDiffs.process(sequence[endPos - 1]) 462 | } 463 | 464 | endPos-- 465 | } 466 | 467 | sequence.add(endPos, tagGenerator.generateOpen(tag)) 468 | endPos-- 469 | } 470 | 471 | return sequence 472 | } 473 | } 474 | 475 | private fun defaultAlgorithm(ignoreWhiteSpaces: Boolean): DiffAlgorithm { 476 | val equalizer = if (ignoreWhiteSpaces) IgnoreWsStringDiffEqualizer() else EqualsDiffEqualizer() 477 | return MyersDiff(equalizer) 478 | } 479 | 480 | private fun defaultSplitter(inlineDiffByWord: Boolean): DiffSplitter = 481 | if (inlineDiffByWord) WordDiffSplitter() else CharDiffSplitter() 482 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/text/DiffSplitter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.github.petertrr.diffutils.text 17 | 18 | public fun interface DiffSplitter { 19 | public fun split(line: String): MutableList 20 | } 21 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/text/DiffTagGenerator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.github.petertrr.diffutils.text 17 | 18 | public interface DiffTagGenerator { 19 | public fun generateOpen(tag: DiffRow.Tag): String 20 | public fun generateClose(tag: DiffRow.Tag): String 21 | } 22 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/text/HtmlDiffTagGenerator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.github.petertrr.diffutils.text 17 | 18 | internal class HtmlDiffTagGenerator(private val className: String) : DiffTagGenerator { 19 | override fun generateOpen(tag: DiffRow.Tag): String = 20 | "" 21 | 22 | override fun generateClose(tag: DiffRow.Tag): String = 23 | "" 24 | } 25 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/text/HtmlLineNormalizer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.github.petertrr.diffutils.text 17 | 18 | import io.github.petertrr.diffutils.normalize 19 | 20 | internal class HtmlLineNormalizer : DiffLineNormalizer { 21 | override fun normalize(line: String): String = 22 | line.normalize() 23 | } 24 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/github/petertrr/diffutils/text/WordDiffSplitter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.github.petertrr.diffutils.text 17 | 18 | // As a "global" variable to avoid re-compiling the regex each time 19 | @Suppress("RegExpRedundantEscape") // To be PCRE compliant! 20 | private val defaultPattern = Regex("""\s+|[,.\[\](){}\/\\*+\-#<>;:&']+""") 21 | 22 | /** 23 | * Splitting lines by word to achieve word by word diff checking. 24 | */ 25 | internal class WordDiffSplitter(private val pattern: Regex = defaultPattern) : DiffSplitter { 26 | override fun split(line: String): MutableList { 27 | val matchResults = pattern.findAll(line) 28 | val list = ArrayList() 29 | var pos = 0 30 | 31 | for (matchResult in matchResults) { 32 | if (pos < matchResult.range.first) { 33 | list.add(line.substring(pos, matchResult.range.first)) 34 | } 35 | 36 | list.add(matchResult.value) 37 | pos = matchResult.range.last + 1 38 | } 39 | 40 | if (pos < line.length) { 41 | list.add(line.substring(pos)) 42 | } 43 | 44 | return list 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/github/petertrr/diffutils/DiffUtilsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2009-2021 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils 20 | 21 | import io.github.petertrr.diffutils.patch.ChangeDelta 22 | import io.github.petertrr.diffutils.patch.Chunk 23 | import io.github.petertrr.diffutils.patch.DeleteDelta 24 | import io.github.petertrr.diffutils.patch.EqualDelta 25 | import io.github.petertrr.diffutils.patch.InsertDelta 26 | import io.github.petertrr.diffutils.patch.Patch 27 | import io.github.petertrr.diffutils.utils.changeDeltaOf 28 | import io.github.petertrr.diffutils.utils.deleteDeltaOf 29 | import io.github.petertrr.diffutils.utils.insertDeltaOf 30 | import kotlin.test.Test 31 | import kotlin.test.assertEquals 32 | import kotlin.test.assertNotNull 33 | import kotlin.test.assertTrue 34 | 35 | class DiffUtilsTest { 36 | @Test 37 | fun testDiff_Insert() { 38 | val patch: Patch = diff(listOf("hhh"), listOf("hhh", "jjj", "kkk")) 39 | assertNotNull(patch) 40 | assertEquals(1, patch.deltas.size) 41 | val delta = patch.deltas[0] 42 | assertTrue(delta is InsertDelta) 43 | assertEquals(Chunk(1, emptyList()), delta.source) 44 | assertEquals(Chunk(1, listOf("jjj", "kkk")), delta.target) 45 | } 46 | 47 | @Test 48 | fun testDiff_Delete() { 49 | val patch = diff(listOf("ddd", "fff", "ggg"), listOf("ggg")) 50 | assertNotNull(patch) 51 | assertEquals(1, patch.deltas.size) 52 | val delta = patch.deltas[0] 53 | assertTrue(delta is DeleteDelta) 54 | assertEquals(Chunk(0, listOf("ddd", "fff")), delta.source) 55 | assertEquals(Chunk(0, emptyList()), delta.target) 56 | } 57 | 58 | @Test 59 | fun testDiff_Change() { 60 | val changeTest_from: List = listOf("aaa", "bbb", "ccc") 61 | val changeTest_to: List = listOf("aaa", "zzz", "ccc") 62 | val patch: Patch = diff(changeTest_from, changeTest_to) 63 | assertNotNull(patch) 64 | assertEquals(1, patch.deltas.size) 65 | val delta = patch.deltas[0] 66 | assertTrue(delta is ChangeDelta) 67 | assertEquals(Chunk(1, listOf("bbb")), delta.source) 68 | assertEquals(Chunk(1, listOf("zzz")), delta.target) 69 | } 70 | 71 | @Test 72 | fun testDiff_EmptyList() { 73 | val patch: Patch = diff(emptyList(), emptyList()) 74 | assertNotNull(patch) 75 | assertEquals(0, patch.deltas.size) 76 | } 77 | 78 | @Test 79 | fun testDiff_EmptyListWithNonEmpty() { 80 | val patch: Patch = diff(emptyList(), listOf("aaa")) 81 | assertNotNull(patch) 82 | assertEquals(1, patch.deltas.size) 83 | val delta = patch.deltas[0] 84 | assertTrue(delta is InsertDelta) 85 | } 86 | 87 | @Test 88 | fun testDiffInline() { 89 | val patch: Patch = diffInline("", "test") 90 | assertEquals(1, patch.deltas.size) 91 | assertTrue(patch.deltas[0] is InsertDelta) 92 | assertEquals(0, patch.deltas[0].source.position) 93 | assertEquals(0, patch.deltas[0].source.lines.size) 94 | assertEquals("test", patch.deltas[0].target.lines[0]) 95 | } 96 | 97 | @Test 98 | fun testDiffInline2() { 99 | val patch: Patch = diffInline("es", "fest") 100 | assertEquals(2, patch.deltas.size) 101 | assertTrue(patch.deltas[0] is InsertDelta) 102 | assertEquals(0, patch.deltas[0].source.position) 103 | assertEquals(2, patch.deltas[1].source.position) 104 | assertEquals(0, patch.deltas[0].source.lines.size) 105 | assertEquals(0, patch.deltas[1].source.lines.size) 106 | assertEquals("f", patch.deltas[0].target.lines[0]) 107 | assertEquals("t", patch.deltas[1].target.lines[0]) 108 | } 109 | 110 | @Test 111 | fun testDiffIntegerList() { 112 | val original: List = listOf(1, 2, 3, 4, 5) 113 | val revised: List = listOf(2, 3, 4, 6) 114 | val patch: Patch = diff(original, revised) 115 | for (delta in patch.deltas) { 116 | println(delta) 117 | } 118 | assertEquals(2, patch.deltas.size) 119 | assertEquals(deleteDeltaOf(0, listOf(1)), patch.deltas[0]) 120 | assertEquals(changeDeltaOf(4, listOf(5), 3, listOf(6)), patch.deltas[1]) 121 | } 122 | 123 | @Test 124 | fun testDiffMissesChangeForkDnaumenkoIssue31() { 125 | val original: List = listOf("line1", "line2", "line3") 126 | val revised: List = listOf("line1", "line2-2", "line4") 127 | val patch: Patch = diff(original, revised) 128 | assertEquals(1, patch.deltas.size) 129 | assertEquals( 130 | changeDeltaOf(1, listOf("line2", "line3"), 1, listOf("line2-2", "line4")), 131 | patch.deltas[0] 132 | ) 133 | } 134 | 135 | @Test 136 | fun testDiffMyersExample1() { 137 | val patch: Patch = diff(listOf("A", "B", "C", "A", "B", "B", "A"), listOf("C", "B", "A", "B", "A", "C")) 138 | assertNotNull(patch) 139 | assertEquals(4, patch.deltas.size) 140 | assertEquals( 141 | "Patch{deltas=[${deleteDeltaOf(0, listOf("A", "B"))}, ${insertDeltaOf(3, 1, listOf("B"))}, " + 142 | "${deleteDeltaOf(5, listOf("B"), 4)}, ${insertDeltaOf(7, 5, listOf("C"))}]}", 143 | patch.toString() 144 | ) 145 | } 146 | 147 | @Test 148 | fun testDiff_Equal() { 149 | val patch: Patch = diff( 150 | source = listOf("hhh", "jjj", "kkk"), 151 | target = listOf("hhh", "jjj", "kkk"), 152 | includeEqualParts = true, 153 | ) 154 | assertNotNull(patch) 155 | assertEquals(1, patch.deltas.size) 156 | val delta = patch.deltas[0] 157 | assertTrue(delta is EqualDelta) 158 | assertEquals(Chunk(0, listOf("hhh", "jjj", "kkk")), delta.source) 159 | assertEquals(Chunk(0, listOf("hhh", "jjj", "kkk")), delta.target) 160 | } 161 | 162 | @Test 163 | fun testDiff_InsertWithEqual() { 164 | val patch: Patch = diff( 165 | source = listOf("hhh"), 166 | target = listOf("hhh", "jjj", "kkk"), 167 | includeEqualParts = true, 168 | ) 169 | assertNotNull(patch) 170 | assertEquals(2, patch.deltas.size) 171 | var delta = patch.deltas[0] 172 | assertTrue(delta is EqualDelta) 173 | assertEquals(Chunk(0, listOf("hhh")), delta.source) 174 | assertEquals(Chunk(0, listOf("hhh")), delta.target) 175 | delta = patch.deltas[1] 176 | assertTrue(delta is InsertDelta) 177 | assertEquals(Chunk(1, emptyList()), delta.source) 178 | assertEquals(Chunk(1, listOf("jjj", "kkk")), delta.target) 179 | } 180 | 181 | @Test 182 | fun testDiff_ProblemIssue42() { 183 | val patch: Patch = diff( 184 | source = listOf("The", "dog", "is", "brown"), 185 | target = listOf("The", "fox", "is", "down"), 186 | includeEqualParts = true, 187 | ) 188 | println(patch) 189 | assertNotNull(patch) 190 | assertEquals(4, patch.deltas.size) 191 | assertEquals( 192 | listOf("EQUAL", "CHANGE", "EQUAL", "CHANGE"), 193 | patch.deltas.map { it.type.name } 194 | ) 195 | var delta = patch.deltas[0] 196 | assertTrue(delta is EqualDelta) 197 | assertEquals(Chunk(0, listOf("The")), delta.source) 198 | assertEquals(Chunk(0, listOf("The")), delta.target) 199 | delta = patch.deltas[1] 200 | assertTrue(delta is ChangeDelta) 201 | assertEquals(Chunk(1, listOf("dog")), delta.source) 202 | assertEquals(Chunk(1, listOf("fox")), delta.target) 203 | delta = patch.deltas[2] 204 | assertTrue(delta is EqualDelta) 205 | assertEquals(Chunk(2, listOf("is")), delta.source) 206 | assertEquals(Chunk(2, listOf("is")), delta.target) 207 | delta = patch.deltas[3] 208 | assertTrue(delta is ChangeDelta) 209 | assertEquals(Chunk(3, listOf("brown")), delta.source) 210 | assertEquals(Chunk(3, listOf("down")), delta.target) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/github/petertrr/diffutils/algorithm/myers/MyersDiffTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2017 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.algorithm.myers 20 | 21 | import io.github.petertrr.diffutils.algorithm.DiffAlgorithmListener 22 | import io.github.petertrr.diffutils.patch.Patch 23 | import kotlin.test.Test 24 | import kotlin.test.assertEquals 25 | 26 | class MyersDiffTest { 27 | @Test 28 | fun testDiffMyersExample1Forward() { 29 | val original = listOf("A", "B", "C", "A", "B", "B", "A") 30 | val revised = listOf("C", "B", "A", "B", "A", "C") 31 | val patch = Patch.generate(original, revised, MyersDiff().computeDiff(original, revised)) 32 | 33 | assertEquals(4, patch.deltas.size) 34 | assertEquals( 35 | "Patch{deltas=[" + 36 | "[DeleteDelta, position: 0, lines: [A, B]], " + 37 | "[InsertDelta, position: 3, lines: [B]], " + 38 | "[DeleteDelta, position: 5, lines: [B]], " + 39 | "[InsertDelta, position: 7, lines: [C]]" + 40 | "]}", 41 | patch.toString(), 42 | ) 43 | } 44 | 45 | @Test 46 | fun testDiffMyersExample1ForwardWithListener() { 47 | val original = listOf("A", "B", "C", "A", "B", "B", "A") 48 | val revised = listOf("C", "B", "A", "B", "A", "C") 49 | val logData = ArrayList() 50 | val patch = Patch.generate( 51 | original, revised, 52 | MyersDiff().computeDiff(original, revised, object : DiffAlgorithmListener { 53 | override fun diffStart() { 54 | logData.add("start") 55 | } 56 | 57 | override fun diffStep(value: Int, max: Int) { 58 | logData.add("$value - $max") 59 | } 60 | 61 | override fun diffEnd() { 62 | logData.add("end") 63 | } 64 | }) 65 | ) 66 | 67 | assertEquals(4, patch.deltas.size) 68 | assertEquals( 69 | "Patch{deltas=[" + 70 | "[DeleteDelta, position: 0, lines: [A, B]], " + 71 | "[InsertDelta, position: 3, lines: [B]], " + 72 | "[DeleteDelta, position: 5, lines: [B]], " + 73 | "[InsertDelta, position: 7, lines: [C]]" + 74 | "]}", 75 | patch.toString(), 76 | ) 77 | 78 | println(logData) 79 | assertEquals(8, logData.size) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/github/petertrr/diffutils/algorithm/myers/MyersDiffWithLinearSpaceTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * Copyright 2021 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.algorithm.myers 20 | 21 | import io.github.petertrr.diffutils.algorithm.DiffAlgorithmListener 22 | import io.github.petertrr.diffutils.diff 23 | import io.github.petertrr.diffutils.patch.Patch 24 | import kotlin.test.Ignore 25 | import kotlin.test.Test 26 | import kotlin.test.assertEquals 27 | import kotlin.time.measureTimedValue 28 | 29 | class MyersDiffWithLinearSpaceTest { 30 | @Test 31 | fun testDiffMyersExample1Forward() { 32 | val original = listOf("A", "B", "C", "A", "B", "B", "A") 33 | val revised = listOf("C", "B", "A", "B", "A", "C") 34 | val changes = MyersDiffWithLinearSpace().computeDiff(original, revised) 35 | val patch = Patch.generate(original, revised, changes) 36 | println(patch) 37 | 38 | assertEquals(5, patch.deltas.size) 39 | assertEquals( 40 | "Patch{deltas=[" + 41 | "[InsertDelta, position: 0, lines: [C]], " + 42 | "[DeleteDelta, position: 0, lines: [A]], " + 43 | "[DeleteDelta, position: 2, lines: [C]], " + 44 | "[DeleteDelta, position: 5, lines: [B]], " + 45 | "[InsertDelta, position: 7, lines: [C]]" + 46 | "]}", 47 | patch.toString(), 48 | ) 49 | } 50 | 51 | @Test 52 | fun testDiffMyersExample1ForwardWithListener() { 53 | val original = listOf("A", "B", "C", "A", "B", "B", "A") 54 | val revised = listOf("C", "B", "A", "B", "A", "C") 55 | val logData = ArrayList() 56 | val progress = object : DiffAlgorithmListener { 57 | override fun diffStart() { 58 | logData.add("start") 59 | } 60 | 61 | override fun diffStep(value: Int, max: Int) { 62 | logData.add("$value - $max") 63 | } 64 | 65 | override fun diffEnd() { 66 | logData.add("end") 67 | } 68 | } 69 | 70 | val patch = Patch.generate( 71 | original = original, 72 | revised = revised, 73 | changes = MyersDiffWithLinearSpace().computeDiff(original, revised, progress), 74 | ) 75 | 76 | println(patch) 77 | 78 | assertEquals(5, patch.deltas.size) 79 | assertEquals( 80 | "Patch{deltas=[" + 81 | "[InsertDelta, position: 0, lines: [C]], " + 82 | "[DeleteDelta, position: 0, lines: [A]], " + 83 | "[DeleteDelta, position: 2, lines: [C]], " + 84 | "[DeleteDelta, position: 5, lines: [B]], " + 85 | "[InsertDelta, position: 7, lines: [C]]" + 86 | "]}", 87 | patch.toString(), 88 | ) 89 | 90 | println(logData) 91 | assertEquals(11, logData.size) 92 | } 93 | 94 | @Test 95 | @Ignore 96 | fun testPerformanceProblemsIssue124() { 97 | val old = listOf("abcd") 98 | val new = (0..<90000) 99 | .map(Int::toString) 100 | .toList() 101 | 102 | val (patch, duration) = measureTimedValue { 103 | diff(old, new, MyersDiffWithLinearSpace()) 104 | } 105 | 106 | println("Finished in ${duration.inWholeMilliseconds}ms and resulted ${patch.deltas.size} deltas") 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/github/petertrr/diffutils/patch/ChunkTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * Copyright 2021 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.patch 20 | 21 | import io.github.petertrr.diffutils.patch.VerifyChunk.CONTENT_DOES_NOT_MATCH_TARGET 22 | import io.github.petertrr.diffutils.patch.VerifyChunk.OK 23 | import kotlin.test.Test 24 | import kotlin.test.assertEquals 25 | 26 | class ChunkTest { 27 | @Test 28 | fun verifyChunk() { 29 | val chunk = Chunk(7, "test".toChars()) 30 | 31 | // Normal check 32 | assertEquals(OK, chunk.verifyChunk("prefix test suffix".toChars())) 33 | assertEquals(CONTENT_DOES_NOT_MATCH_TARGET, chunk.verifyChunk("prefix es suffix".toChars())) 34 | } 35 | 36 | private fun String.toChars(): List = 37 | toCharArray().toList() 38 | } 39 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/github/petertrr/diffutils/patch/PatchWithMyersDiffTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * Copyright 2021 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.patch 20 | 21 | import io.github.petertrr.diffutils.diff 22 | import io.github.petertrr.diffutils.patch 23 | import kotlin.test.Test 24 | import kotlin.test.assertEquals 25 | 26 | class PatchWithMyersDiffTest { 27 | @Test 28 | fun testPatch_Insert() { 29 | val insertTestFrom = listOf("hhh") 30 | val insertTestTo = listOf("hhh", "jjj", "kkk", "lll") 31 | val patch = diff(insertTestFrom, insertTestTo) 32 | assertEquals(insertTestTo, patch(insertTestFrom, patch)) 33 | } 34 | 35 | @Test 36 | fun testPatch_Delete() { 37 | val deleteTestFrom = listOf("ddd", "fff", "ggg", "hhh") 38 | val deleteTestTo = listOf("ggg") 39 | val patch = diff(deleteTestFrom, deleteTestTo) 40 | assertEquals(deleteTestTo, patch(deleteTestFrom, patch)) 41 | } 42 | 43 | @Test 44 | fun testPatch_Change() { 45 | val changeTestFrom = listOf("aaa", "bbb", "ccc", "ddd") 46 | val changeTestTo = listOf("aaa", "bxb", "cxc", "ddd") 47 | val patch = diff(changeTestFrom, changeTestTo) 48 | assertEquals(changeTestTo, patch(changeTestFrom, patch)) 49 | } 50 | 51 | @Test 52 | fun testPatch_Change_withExceptionProcessor() { 53 | val changeTestFrom = mutableListOf("aaa", "bbb", "ccc", "ddd") 54 | val changeTestTo = listOf("aaa", "bxb", "cxc", "ddd") 55 | val patch = diff(changeTestFrom, changeTestTo) 56 | 57 | changeTestFrom[2] = "CDC" 58 | patch.withConflictOutput(ConflictProducingConflictOutput()) 59 | 60 | val data = patch(changeTestFrom, patch) 61 | assertEquals(9, data.size) 62 | assertEquals( 63 | mutableListOf( 64 | "aaa", 65 | "<<<<<< HEAD", 66 | "bbb", 67 | "CDC", 68 | "======", 69 | "bbb", 70 | "ccc", 71 | ">>>>>>> PATCH", 72 | "ddd" 73 | ), 74 | data 75 | ) 76 | } 77 | 78 | @Test 79 | fun testPatchThreeWayIssue138() { 80 | val base = "Imagine there's no heaven".split("\\s+".toRegex()) 81 | val left = "Imagine there's no HEAVEN".split("\\s+".toRegex()) 82 | val right = "IMAGINE there's no heaven".split("\\s+".toRegex()) 83 | 84 | val rightPatch = diff(base, right) 85 | rightPatch.withConflictOutput(ConflictProducingConflictOutput()) 86 | 87 | val applied = rightPatch.applyTo(left) 88 | assertEquals("IMAGINE there's no HEAVEN", applied.joinToString(separator = " ")) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/github/petertrr/diffutils/patch/PatchWithMyersDiffWithLinearSpaceTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Peter Trifanov. 3 | * Copyright 2021 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.patch 20 | 21 | import io.github.petertrr.diffutils.algorithm.myers.MyersDiffWithLinearSpace 22 | import io.github.petertrr.diffutils.diff 23 | import io.github.petertrr.diffutils.patch 24 | import kotlin.test.Test 25 | import kotlin.test.assertEquals 26 | 27 | class PatchWithMyersDiffWithLinearSpaceTest { 28 | @Test 29 | fun testPatch_Change_withExceptionProcessor() { 30 | val changeTestFrom = mutableListOf("aaa", "bbb", "ccc", "ddd") 31 | val changeTestTo = mutableListOf("aaa", "bxb", "cxc", "ddd") 32 | val patch = diff( 33 | source = changeTestFrom, 34 | target = changeTestTo, 35 | algorithm = MyersDiffWithLinearSpace(), 36 | ) 37 | 38 | changeTestFrom[2] = "CDC" 39 | patch.withConflictOutput(ConflictProducingConflictOutput()) 40 | 41 | val data = patch(changeTestFrom, patch) 42 | assertEquals(11, data.size) 43 | assertEquals( 44 | mutableListOf( 45 | "aaa", 46 | "bxb", 47 | "cxc", 48 | "<<<<<< HEAD", 49 | "bbb", 50 | "CDC", 51 | "======", 52 | "bbb", 53 | "ccc", 54 | ">>>>>>> PATCH", 55 | "ddd", 56 | ), 57 | data, 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/github/petertrr/diffutils/text/DiffRowGeneratorTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2009-2021 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.text 20 | 21 | import kotlin.test.Test 22 | import kotlin.test.assertEquals 23 | import kotlin.test.assertTrue 24 | 25 | class DiffRowGeneratorTest { 26 | @Test 27 | fun testGenerator_Default() { 28 | val first = "anything \n \nother" 29 | val second = "anything\n\nother" 30 | val generator = DiffRowGenerator( 31 | columnWidth = Int.MAX_VALUE // do not wrap 32 | ) 33 | val rows: List = generator.generateDiffRows(first.lines(), second.lines()) 34 | print(rows) 35 | assertEquals(3, rows.size) 36 | } 37 | 38 | /** 39 | * Test of normalize method, of class StringUtils. 40 | */ 41 | @Test 42 | fun testNormalize_List() { 43 | val normalizer = HtmlLineNormalizer() 44 | assertEquals(" test", normalizer.normalize("\ttest")) 45 | } 46 | 47 | @Test 48 | fun testGenerator_Default2() { 49 | val first = "anything \n \nother" 50 | val second = "anything\n\nother" 51 | val generator = DiffRowGenerator( 52 | columnWidth = 0 // do not wrap 53 | ) 54 | val rows: List = generator.generateDiffRows(first.lines(), second.lines()) 55 | print(rows) 56 | assertEquals(3, rows.size) 57 | } 58 | 59 | @Test 60 | fun testGenerator_InlineDiff() { 61 | val first = "anything \n \nother" 62 | val second = "anything\n\nother" 63 | val generator = DiffRowGenerator( 64 | showInlineDiffs = true, 65 | columnWidth = Int.MAX_VALUE // do not wrap 66 | ) 67 | val rows: List = generator.generateDiffRows(first.lines(), second.lines()) 68 | print(rows) 69 | assertEquals(3, rows.size) 70 | assertTrue(rows[0].oldLine.indexOf(" 0) 71 | } 72 | 73 | @Test 74 | fun testGenerator_IgnoreWhitespaces() { 75 | val first = "anything \n \nother\nmore lines" 76 | val second = "anything\n\nother\nsome more lines" 77 | val generator = DiffRowGenerator( 78 | ignoreWhiteSpaces = true, 79 | columnWidth = Int.MAX_VALUE // do not wrap 80 | ) 81 | val rows: List = generator.generateDiffRows(first.lines(), second.lines()) 82 | print(rows) 83 | assertEquals(4, rows.size) 84 | assertEquals(rows[0].tag, DiffRow.Tag.EQUAL) 85 | assertEquals(rows[1].tag, DiffRow.Tag.EQUAL) 86 | assertEquals(rows[2].tag, DiffRow.Tag.EQUAL) 87 | assertEquals(rows[3].tag, DiffRow.Tag.CHANGE) 88 | } 89 | 90 | @Test 91 | fun testGeneratorWithWordWrap() { 92 | val first = "anything \n \nother" 93 | val second = "anything\n\nother" 94 | val generator = DiffRowGenerator( 95 | columnWidth = 5 96 | ) 97 | val rows: List = generator.generateDiffRows(first.lines(), second.lines()) 98 | print(rows) 99 | assertEquals(3, rows.size) 100 | assertEquals(DiffRow(DiffRow.Tag.CHANGE, "anyth
ing ", "anyth
ing"), rows[0]) 101 | assertEquals(DiffRow(DiffRow.Tag.CHANGE, " ", ""), rows[1]) 102 | assertEquals(DiffRow(DiffRow.Tag.EQUAL, "other", "other"), rows[2]) 103 | } 104 | 105 | @Test 106 | fun testGeneratorWithMerge() { 107 | val first = "anything \n \nother" 108 | val second = "anything\n\nother" 109 | val generator = DiffRowGenerator( 110 | showInlineDiffs = true, 111 | mergeOriginalRevised = true, 112 | ) 113 | val rows: List = generator.generateDiffRows(first.lines(), second.lines()) 114 | print(rows) 115 | assertEquals(3, rows.size) 116 | assertEquals(DiffRow(DiffRow.Tag.CHANGE, "anything ", "anything"), rows[0]) 117 | assertEquals(DiffRow(DiffRow.Tag.CHANGE, " ", ""), rows[1]) 118 | assertEquals(DiffRow(DiffRow.Tag.EQUAL, "other", "other"), rows[2]) 119 | } 120 | 121 | @Test 122 | fun testGeneratorWithMerge2() { 123 | val generator = DiffRowGenerator( 124 | showInlineDiffs = true, 125 | mergeOriginalRevised = true, 126 | ) 127 | val rows: List = generator.generateDiffRows(listOf("Test"), listOf("ester")) 128 | print(rows) 129 | assertEquals(1, rows.size) 130 | assertEquals(DiffRow(DiffRow.Tag.CHANGE, "Tester", "ester"), rows[0]) 131 | } 132 | 133 | @Test 134 | fun testGeneratorWithMerge3() { 135 | val first = "test\nanything \n \nother" 136 | val second = "anything\n\nother\ntest\ntest2" 137 | val generator = DiffRowGenerator( 138 | showInlineDiffs = true, 139 | mergeOriginalRevised = true, 140 | ) 141 | val rows = generator.generateDiffRows(first.lines(), second.lines()) 142 | println(rows) 143 | assertEquals(6, rows.size) 144 | assertEquals(DiffRow(DiffRow.Tag.CHANGE, "test", "anything"), rows[0]) 145 | assertEquals(DiffRow(DiffRow.Tag.CHANGE, "anything ", ""), rows[1]) 146 | assertEquals(DiffRow(DiffRow.Tag.DELETE, " ", ""), rows[2]) 147 | assertEquals(DiffRow(DiffRow.Tag.EQUAL, "other", "other"), rows[3]) 148 | assertEquals(DiffRow(DiffRow.Tag.INSERT, "test", "test"), rows[4]) 149 | assertEquals(DiffRow(DiffRow.Tag.INSERT, "test2", "test2"), rows[5]) 150 | } 151 | 152 | @Test 153 | fun testGeneratorWithMergeByWord4() { 154 | val generator = DiffRowGenerator( 155 | showInlineDiffs = true, 156 | mergeOriginalRevised = true, 157 | inlineDiffByWord = true, 158 | ) 159 | val rows: List = generator.generateDiffRows(listOf("Test"), listOf("ester")) 160 | print(rows) 161 | assertEquals(1, rows.size) 162 | assertEquals(DiffRow(DiffRow.Tag.CHANGE, "Testester", "ester"), rows[0]) 163 | } 164 | 165 | @Test 166 | fun testGeneratorWithMergeByWord5() { 167 | val generator = DiffRowGenerator( 168 | showInlineDiffs = true, 169 | mergeOriginalRevised = true, 170 | inlineDiffByWord = true, 171 | columnWidth = 80 172 | ) 173 | val rows: List = generator.generateDiffRows(listOf("Test feature"), listOf("ester feature best")) 174 | print(rows) 175 | assertEquals(1, rows.size) 176 | assertEquals( 177 | DiffRow(DiffRow.Tag.CHANGE, "Testester
feature best", "ester feature best"), 178 | rows[0] 179 | ) 180 | } 181 | 182 | @Test 183 | fun testSplitString() { 184 | val splitter = WordDiffSplitter() 185 | val list = splitter.split("test,test2") 186 | assertEquals(3, list.size) 187 | assertEquals("[test, ,, test2]", list.toString()) 188 | } 189 | 190 | @Test 191 | fun testSplitString2() { 192 | val splitter = WordDiffSplitter() 193 | val list = splitter.split("test , test2") 194 | println(list) 195 | assertEquals(5, list.size) 196 | assertEquals("[test, , ,, , test2]", list.toString()) 197 | } 198 | 199 | @Test 200 | fun testSplitString3() { 201 | val splitter = WordDiffSplitter() 202 | val list = splitter.split("test,test2,") 203 | println(list) 204 | assertEquals(4, list.size) 205 | assertEquals("[test, ,, test2, ,]", list.toString()) 206 | } 207 | 208 | @Test 209 | fun testGeneratorExample1() { 210 | val generator = DiffRowGenerator( 211 | showInlineDiffs = true, 212 | mergeOriginalRevised = true, 213 | inlineDiffByWord = true, 214 | oldTag = MarkdownTagGenerator("~"), 215 | newTag = MarkdownTagGenerator("**"), 216 | ) 217 | val rows: List = generator.generateDiffRows( 218 | listOf("This is a test senctence."), 219 | listOf("This is a test for diffutils.") 220 | ) 221 | println(rows[0].oldLine) 222 | assertEquals(1, rows.size) 223 | assertEquals("This is a test ~senctence~**for diffutils**.", rows[0].oldLine) 224 | } 225 | 226 | @Test 227 | fun testGeneratorExample2() { 228 | val generator = DiffRowGenerator( 229 | showInlineDiffs = true, 230 | inlineDiffByWord = true, 231 | oldTag = MarkdownTagGenerator("~"), 232 | newTag = MarkdownTagGenerator("**"), 233 | ) 234 | val rows: List = generator.generateDiffRows( 235 | listOf("This is a test senctence.", "This is the second line.", "And here is the finish."), 236 | listOf("This is a test for diffutils.", "This is the second line.") 237 | ) 238 | println("|original|new|") 239 | println("|--------|---|") 240 | for (row in rows) { 241 | println("|" + row.oldLine + "|" + row.newLine + "|") 242 | } 243 | assertEquals(3, rows.size) 244 | assertEquals("This is a test ~senctence~.", rows[0].oldLine) 245 | assertEquals("This is a test **for diffutils**.", rows[0].newLine) 246 | } 247 | 248 | @Test 249 | fun testGeneratorUnchanged() { 250 | val first = "anything \n \nother" 251 | val second = "anything\n\nother" 252 | val generator = DiffRowGenerator( 253 | columnWidth = 5, 254 | reportLinesUnchanged = true, 255 | ) 256 | val rows: List = generator.generateDiffRows(first.lines(), second.lines()) 257 | print(rows) 258 | assertEquals(3, rows.size) 259 | assertEquals(DiffRow(DiffRow.Tag.CHANGE, "anything ", "anything"), rows[0]) 260 | assertEquals(DiffRow(DiffRow.Tag.CHANGE, " ", ""), rows[1]) 261 | assertEquals(DiffRow(DiffRow.Tag.EQUAL, "other", "other"), rows[2]) 262 | } 263 | 264 | @Test 265 | fun testGeneratorIssue14() { 266 | val splitter = WordDiffSplitter(Regex(",")) 267 | val generator = DiffRowGenerator( 268 | showInlineDiffs = true, 269 | mergeOriginalRevised = true, 270 | inlineDiffSplitter = splitter, 271 | oldTag = MarkdownTagGenerator("~"), 272 | newTag = MarkdownTagGenerator("**"), 273 | ) 274 | val rows: List = generator.generateDiffRows( 275 | listOf("J. G. Feldstein, Chair"), 276 | listOf("T. P. Pastor, Chair") 277 | ) 278 | println(rows[0].oldLine) 279 | assertEquals(1, rows.size) 280 | assertEquals("~J. G. Feldstein~**T. P. Pastor**, Chair", rows[0].oldLine) 281 | } 282 | 283 | @Test 284 | fun testGeneratorIssue15() { 285 | val generator = DiffRowGenerator( 286 | showInlineDiffs = true, //show the ~ ~ and ** ** symbols on each difference 287 | inlineDiffByWord = true, //show the ~ ~ and ** ** around each different word instead of each letter 288 | //reportLinesUnchanged = true) //experiment 289 | oldTag = MarkdownTagGenerator("~"), 290 | newTag = MarkdownTagGenerator("**"), 291 | ) 292 | val listOne: List = """ 293 | TABLE_NAME, COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, NULLABLE, 294 | ACTIONS_C17005, ID, NUMBER, 22, 19, N, 295 | ACTIONS_C17005, ISSUEID, NUMBER, 22, 19, Y, 296 | ACTIONS_C17005, MODIFIED, NUMBER, 22, 10, Y, 297 | ACTIONS_C17005, TABLE, VARCHAR2, 1020, null, Y, 298 | ACTIONS_C17005, S_NAME, CLOB, 4000, null, Y, 299 | ACTIONS_C17008, ID, NUMBER, 22, 19, N, 300 | ACTIONS_C17008, ISSUEID, NUMBER, 22, 19, Y, 301 | ACTIONS_C17008, MODIFIED, NUMBER, 22, 10, Y, 302 | """.trimIndent().lines() 303 | val listTwo: List = """ 304 | TABLE_NAME, COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, NULLABLE, 305 | ACTIONS_C16913, ID, NUMBER, 22, 19, N, 306 | ACTIONS_C16913, ISSUEID, NUMBER, 22, 19, Y, 307 | ACTIONS_C16913, MODIFIED, NUMBER, 22, 10, Y, 308 | ACTIONS_C16913, VRS, NUMBER, 22, 1, Y, 309 | ACTIONS_C16913, ZTABS, VARCHAR2, 255, null, Y, 310 | ACTIONS_C16913, ZTABS_S, VARCHAR2, 255, null, Y, 311 | ACTIONS_C16913, TASK, VARCHAR2, 255, null, Y, 312 | ACTIONS_C16913, HOURS_SPENT, VARCHAR2, 255, null, Y, 313 | """.trimIndent().lines() 314 | val rows = generator.generateDiffRows(listOne, listTwo) 315 | assertEquals(9, rows.size) 316 | for (row in rows) { 317 | println("|" + row.oldLine + "| " + row.newLine + " |") 318 | if (!row.oldLine.startsWith("TABLE_NAME")) { 319 | assertTrue(row.newLine.startsWith("**ACTIONS_C16913**")) 320 | assertTrue(row.oldLine.startsWith("~ACTIONS_C1700")) 321 | } 322 | } 323 | } 324 | 325 | @Test 326 | fun testGeneratorIssue22() { 327 | val generator = DiffRowGenerator( 328 | showInlineDiffs = true, 329 | inlineDiffByWord = true, 330 | oldTag = MarkdownTagGenerator("~"), 331 | newTag = MarkdownTagGenerator("**"), 332 | ) 333 | val aa = "This is a test senctence." 334 | val bb = "This is a test for diffutils.\nThis is the second line." 335 | val rows: List = generator.generateDiffRows( 336 | listOf(*aa.split("\n").toTypedArray()), 337 | listOf(*bb.split("\n").toTypedArray()) 338 | ) 339 | rows.zip(listOf( 340 | DiffRow(DiffRow.Tag.CHANGE, "This is a test ~senctence~.", "This is a test **for diffutils**."), 341 | DiffRow(DiffRow.Tag.INSERT, "", "**This is the second line.**") 342 | )) 343 | .forEach { 344 | assertEquals(it.first, it.second) 345 | } 346 | println("|original|new|") 347 | println("|--------|---|") 348 | for (row in rows) { 349 | println("|" + row.oldLine + "|" + row.newLine + "|") 350 | } 351 | } 352 | 353 | @Test 354 | fun testGeneratorIssue22_2() { 355 | val generator = DiffRowGenerator( 356 | showInlineDiffs = true, 357 | inlineDiffByWord = true, 358 | oldTag = MarkdownTagGenerator("~"), 359 | newTag = MarkdownTagGenerator("**"), 360 | ) 361 | val aa = "This is a test for diffutils.\nThis is the second line." 362 | val bb = "This is a test senctence." 363 | val rows: List = generator.generateDiffRows( 364 | listOf(*aa.split("\n").toTypedArray()), 365 | listOf(*bb.split("\n").toTypedArray()) 366 | ) 367 | rows.zip(listOf( 368 | DiffRow(DiffRow.Tag.CHANGE, "This is a test ~for diffutils~.", "This is a test **senctence**."), 369 | DiffRow(DiffRow.Tag.DELETE, "~This is the second line.~", "") 370 | )) 371 | .forEach { 372 | assertEquals(it.first, it.second) 373 | } 374 | } 375 | 376 | @Test 377 | fun testGeneratorIssue22_3() { 378 | val generator = DiffRowGenerator( 379 | showInlineDiffs = true, 380 | inlineDiffByWord = true, 381 | oldTag = MarkdownTagGenerator("~"), 382 | newTag = MarkdownTagGenerator("**"), 383 | ) 384 | val aa = "This is a test senctence." 385 | val bb = "This is a test for diffutils.\nThis is the second line.\nAnd one more." 386 | val rows: List = generator.generateDiffRows( 387 | listOf(*aa.split("\n").toTypedArray()), 388 | listOf(*bb.split("\n").toTypedArray()) 389 | ) 390 | rows.zip(listOf( 391 | DiffRow(DiffRow.Tag.CHANGE, "This is a test ~senctence~.", "This is a test **for diffutils**."), 392 | DiffRow(DiffRow.Tag.INSERT, "", "**This is the second line.**"), 393 | DiffRow(DiffRow.Tag.INSERT, "", "**And one more.**"), 394 | )) 395 | .forEach { 396 | assertEquals(it.first, it.second) 397 | } 398 | } 399 | 400 | @Test 401 | fun testGeneratorIssue41DefaultNormalizer() { 402 | val generator = DiffRowGenerator( 403 | ) 404 | val rows: List = generator.generateDiffRows(listOf("<"), listOf("<")) 405 | assertEquals(DiffRow(DiffRow.Tag.EQUAL, "<", "<"), rows.single()) 406 | } 407 | 408 | @Test 409 | fun testGeneratorIssue41UserNormalizer() { 410 | val generator = DiffRowGenerator( 411 | lineNormalizer = { str -> str.replace("\t", " ") } 412 | ) 413 | var rows: List = generator.generateDiffRows(listOf("<"), listOf("<")) 414 | assertEquals(DiffRow(DiffRow.Tag.EQUAL, "<", "<"), rows.single()) 415 | rows = generator.generateDiffRows(listOf("\t<"), listOf("<")) 416 | assertEquals(DiffRow(DiffRow.Tag.CHANGE, " <", "<"), rows.single()) 417 | } 418 | 419 | @Test 420 | fun testGenerationIssue44reportLinesUnchangedProblem() { 421 | val generator = DiffRowGenerator( 422 | showInlineDiffs = true, 423 | reportLinesUnchanged = true, 424 | oldTag = MarkdownTagGenerator("~~"), 425 | newTag = MarkdownTagGenerator("**"), 426 | ) 427 | val rows: List = generator.generateDiffRows(listOf("
To do
"), listOf("
Done
")) 428 | assertEquals(DiffRow(DiffRow.Tag.CHANGE, "
~~T~~o~~ do~~
", "
**D**o**ne**
"), rows.single()) 429 | } 430 | 431 | @Test 432 | fun testIgnoreWhitespaceIssue66() { 433 | val generator = DiffRowGenerator( 434 | showInlineDiffs = true, 435 | inlineDiffByWord = true, 436 | ignoreWhiteSpaces = true, 437 | mergeOriginalRevised = true, 438 | oldTag = MarkdownTagGenerator("~"), //introduce markdown style for strikethrough, 439 | newTag = MarkdownTagGenerator("**") //introduce markdown style for bold, 440 | ) 441 | 442 | //compute the differences for two test texts. 443 | //CHECKSTYLE:OFF 444 | val rows: List = generator.generateDiffRows( 445 | listOf("This\tis\ta\ttest."), 446 | listOf("This is a test") 447 | ) 448 | //CHECKSTYLE:ON 449 | assertEquals("This is a test~.~", rows[0].oldLine) 450 | } 451 | 452 | @Test 453 | fun testIgnoreWhitespaceIssue66_2() { 454 | val generator = DiffRowGenerator( 455 | showInlineDiffs = true, 456 | inlineDiffByWord = true, 457 | ignoreWhiteSpaces = true, 458 | mergeOriginalRevised = true, 459 | oldTag = MarkdownTagGenerator("~"), //introduce markdown style for strikethrough, 460 | newTag = MarkdownTagGenerator("**") //introduce markdown style for bold, 461 | ) 462 | 463 | //compute the differences for two test texts. 464 | val rows: List = generator.generateDiffRows( 465 | listOf("This is a test."), 466 | listOf("This is a test") 467 | ) 468 | assertEquals("This is a test~.~", rows[0].oldLine) 469 | } 470 | 471 | @Test 472 | fun testIgnoreWhitespaceIssue64() { 473 | val generator = DiffRowGenerator( 474 | showInlineDiffs = true, 475 | inlineDiffByWord = true, 476 | ignoreWhiteSpaces = true, 477 | mergeOriginalRevised = true, 478 | oldTag = MarkdownTagGenerator("~"), //introduce markdown style for strikethrough, 479 | newTag = MarkdownTagGenerator("**") //introduce markdown style for bold, 480 | ) 481 | 482 | //compute the differences for two test texts. 483 | val rows: List = generator.generateDiffRows( 484 | listOf(*"test\n\ntestline".split("\n").toTypedArray()), 485 | listOf(*"A new text line\n\nanother one".split("\n").toTypedArray()) 486 | ) 487 | rows.map { it.oldLine } 488 | .zip( 489 | listOf( 490 | "~test~**A new text line**", 491 | "", 492 | "~testline~**another one**" 493 | ) 494 | ) 495 | .all { 496 | it.first == it.second 497 | } 498 | } 499 | 500 | @Test 501 | fun testReplaceDiffsIssue63() { 502 | val generator = DiffRowGenerator( 503 | showInlineDiffs = true, 504 | inlineDiffByWord = true, 505 | mergeOriginalRevised = true, 506 | oldTag = MarkdownTagGenerator("~"), //introduce markdown style for strikethrough, 507 | newTag = MarkdownTagGenerator("**"), //introduce markdown style for bold, 508 | processDiffs = { str -> str.replace(" ", "/") }, 509 | ) 510 | 511 | //compute the differences for two test texts. 512 | val rows: List = generator.generateDiffRows( 513 | listOf("This is a test."), 514 | listOf("This is a test") 515 | ) 516 | assertEquals("This~//~**/**is~//~**/**a~//~**/**test~.~", rows[0].oldLine) 517 | } 518 | 519 | @Test 520 | fun testProblemTooManyDiffRowsIssue65() { 521 | val generator = DiffRowGenerator( 522 | showInlineDiffs = true, 523 | reportLinesUnchanged = true, 524 | oldTag = MarkdownTagGenerator("~"), 525 | newTag = MarkdownTagGenerator("**"), 526 | mergeOriginalRevised = true, 527 | inlineDiffByWord = false, 528 | replaceOriginalLinefeedInChangesWithSpaces = true 529 | ) 530 | val diffRows: List = generator.generateDiffRows( 531 | listOf("Ich möchte nicht mit einem Bot sprechen.", "Ich soll das schon wieder wiederholen?"), 532 | listOf("Ich möchte nicht mehr mit dir sprechen. Leite mich weiter.", "Kannst du mich zum Kundendienst weiterleiten?") 533 | ) 534 | print(diffRows) 535 | assertEquals(2, diffRows.size) 536 | } 537 | 538 | @Test 539 | fun testProblemTooManyDiffRowsIssue65_NoMerge() { 540 | val generator = DiffRowGenerator( 541 | showInlineDiffs = true, 542 | reportLinesUnchanged = true, 543 | oldTag = MarkdownTagGenerator("~"), 544 | newTag = MarkdownTagGenerator("**"), 545 | mergeOriginalRevised = false, 546 | inlineDiffByWord = false, 547 | ) 548 | val diffRows: List = generator.generateDiffRows( 549 | listOf("Ich möchte nicht mit einem Bot sprechen.", "Ich soll das schon wieder wiederholen?"), 550 | listOf("Ich möchte nicht mehr mit dir sprechen. Leite mich weiter.", "Kannst du mich zum Kundendienst weiterleiten?") 551 | ) 552 | println(diffRows) 553 | assertEquals(2, diffRows.size) 554 | } 555 | 556 | @Test 557 | fun testProblemTooManyDiffRowsIssue65_DiffByWord() { 558 | val generator = DiffRowGenerator( 559 | showInlineDiffs = true, 560 | reportLinesUnchanged = true, 561 | oldTag = MarkdownTagGenerator("~"), 562 | newTag = MarkdownTagGenerator("**"), 563 | mergeOriginalRevised = true, 564 | inlineDiffByWord = true, 565 | ) 566 | val diffRows: List = generator.generateDiffRows( 567 | listOf("Ich möchte nicht mit einem Bot sprechen.", "Ich soll das schon wieder wiederholen?"), 568 | listOf("Ich möchte nicht mehr mit dir sprechen. Leite mich weiter.", "Kannst du mich zum Kundendienst weiterleiten?") 569 | ) 570 | println(diffRows) 571 | assertEquals(2, diffRows.size) 572 | } 573 | 574 | @Test 575 | fun testProblemTooManyDiffRowsIssue65_NoInlineDiff() { 576 | val generator = DiffRowGenerator( 577 | showInlineDiffs = false, 578 | reportLinesUnchanged = true, 579 | oldTag = MarkdownTagGenerator("~"), 580 | newTag = MarkdownTagGenerator("**"), 581 | mergeOriginalRevised = true, 582 | inlineDiffByWord = false, 583 | ) 584 | val diffRows: List = generator.generateDiffRows( 585 | listOf("Ich möchte nicht mit einem Bot sprechen.", "Ich soll das schon wieder wiederholen?"), 586 | listOf("Ich möchte nicht mehr mit dir sprechen. Leite mich weiter.", "Kannst du mich zum Kundendienst weiterleiten?") 587 | ) 588 | println(diffRows) 589 | assertEquals(2, diffRows.size) 590 | } 591 | 592 | @Test 593 | fun testLinefeedInStandardTagsWithLineWidthIssue81() { 594 | val original: List = listOf( 595 | *"""American bobtail jaguar. American bobtail bombay but turkish angora and tomcat. 596 | Russian blue leopard. Lion. Tabby scottish fold for russian blue, so savannah yet lynx. Tomcat singapura, cheetah. 597 | Bengal tiger panther but singapura but bombay munchkin for cougar.""".split("\n").toTypedArray() 598 | ) 599 | val revised: List = listOf( 600 | *"""bobtail jaguar. American bobtail turkish angora and tomcat. 601 | Russian blue leopard. Lion. Tabby scottish folded for russian blue, so savannah yettie? lynx. Tomcat singapura, cheetah. 602 | Bengal tiger panther but singapura but bombay munchkin for cougar. And more.""".split("\n").toTypedArray() 603 | ) 604 | val generator = DiffRowGenerator( 605 | showInlineDiffs = true, 606 | ignoreWhiteSpaces = true, 607 | columnWidth = 100, 608 | ) 609 | val deltas = generator.generateDiffRows(original, revised) 610 | println(deltas) 611 | } 612 | 613 | @Test 614 | fun testIssue86WrongInlineDiff() { 615 | val original: String = """ 616 | MessageTime,MessageType,Instrument,InstrumentState,TradePrice,TradeVolume,TradeCond,TradeId,AskPrice1,AskVol1,BidPrice1,BidVol1,AskPrice2,AskVol2,BidPrice2,BidVol2,AskPrice3,AskVol3,BidPrice3,BidVol3,AskPrice4,AskVol4,BidPrice4,BidVol4,AskPrice5,AskVol5,BidPrice5,BidVol5 617 | 2020-04-04T08:00:00.000Z,S,HHD_MAY20,Open,,,,,,,,,,,,,,,,,,,,,,,, 618 | 2020-04-04T08:00:00.000Z,S,FHK_C23.5_MAY20,Open,,,,,,,,,,,,,,,,,,,,,,,, 619 | 2020-04-04T13:49:11.522Z,Q,HHD_MAY20,,,,,,2.6,10,2.6,10,,,,,,,,,,,,,,,, 620 | 2020-04-04T13:49:18.210Z,T,HHD_MAY20,,2.6,1,Screen,0,,,,,,,,,,,,,,,,,,,, 621 | 2020-04-04T17:00:00.000Z,S,HHD_MAY20,Close,,,,,,,,,,,,,,,,,,,,,,,, 622 | 2020-04-04T17:00:00.000Z,S,FHK_C23.5_MAY20,Close,,,,,,,,,,,,,,,,,,,,,,,, 623 | """.trimIndent() 624 | val revised: String = """ 625 | MessageTime,MessageType,Instrument,InstrumentState,TradePrice,TradeVolume,TradeCond,TradeId,AskPrice1,AskVol1,BidPrice1,BidVol1,AskPrice2,AskVol2,BidPrice2,BidVol2,AskPrice3,AskVol3,BidPrice3,BidVol3,AskPrice4,AskVol4,BidPrice4,BidVol4,AskPrice5,AskVol5,BidPrice5,BidVol5 626 | 2020-04-02T08:00:00.000Z,S,HHD_MAY20,Open,,,,,,,,,,,,,,,,,,,,,,,, 627 | 2020-04-02T08:00:00.000Z,S,FHK_C23.5_MAY20,Open,,,,,,,,,,,,,,,,,,,,,,,, 628 | 2020-04-04T13:49:11.522Z,Q,HHD_MAY20,,,,,,2.6,10,2.6,10,,,,,,,,,,,,,,,, 629 | 2020-04xs-04T17dw:00:00.000Z,Sdwdw,HHD_MAY20dwdw,Closdwde,,,,,,,,,,,,,,,,,,,,,,,, 630 | 2020-04-04T13:49:18.210Z,T,HHD_MAY20,,2.6,2,Screen,0,,,,,,,,,,,,,,,,,,,, 631 | 2020-04-04T17:00:00.000Z,S,HHD_MAY20,Close,,,,,,,,,,,,,,,,,,,,,,,, 632 | 2020-04-04T17:00:00.000Z,S,FHK_C23.5_MAY20,Close,,,,,,,,,,,,,,,,,,,,,,,, 633 | """.trimIndent() 634 | val generator = DiffRowGenerator( 635 | showInlineDiffs = true, 636 | mergeOriginalRevised = true, 637 | inlineDiffByWord = true, 638 | oldTag = MarkdownTagGenerator("~"), 639 | newTag = MarkdownTagGenerator("**"), 640 | ) 641 | val rows: List = generator.generateDiffRows( 642 | listOf(*original.split("\n").toTypedArray()), 643 | listOf(*revised.split("\n").toTypedArray()) 644 | ) 645 | rows 646 | .filter { it.tag !== DiffRow.Tag.EQUAL } 647 | .forEach(::println) 648 | } 649 | 650 | @Test 651 | fun testCorrectChangeIssue114() { 652 | val original: List = listOf("A", "B", "C", "D", "E") 653 | val revised: List = listOf("a", "C", "", "E") 654 | val generator = DiffRowGenerator( 655 | showInlineDiffs = false, 656 | inlineDiffByWord = true, 657 | oldTag = MarkdownTagGenerator("~"), 658 | newTag = MarkdownTagGenerator("**"), 659 | ) 660 | val rows = generator.generateDiffRows(original, revised) 661 | for (diff in rows) { 662 | println(diff) 663 | } 664 | assertTrue(rows.map { it.tag.name } 665 | .zip(listOf("CHANGE", "DELETE", "EQUAL", "CHANGE", "EQUAL")) 666 | .all { it.first == it.second } 667 | ) 668 | } 669 | 670 | @Test 671 | fun testCorrectChangeIssue114_2() { 672 | val original: List = listOf("A", "B", "C", "D", "E") 673 | val revised: List = listOf("a", "C", "", "E") 674 | val generator = DiffRowGenerator( 675 | showInlineDiffs = true, 676 | inlineDiffByWord = true, 677 | oldTag = MarkdownTagGenerator("~"), 678 | newTag = MarkdownTagGenerator("**"), 679 | ) 680 | val rows = generator.generateDiffRows(original, revised) 681 | for (diff in rows) { 682 | println(diff) 683 | } 684 | assertTrue(rows.map { it.tag.name } 685 | .zip(listOf("CHANGE", "DELETE", "EQUAL", "CHANGE", "EQUAL")) 686 | .all { it.first == it.second } 687 | ) 688 | assertEquals(DiffRow(DiffRow.Tag.DELETE, "~B~", ""), rows[1]) 689 | } 690 | 691 | @Test 692 | fun testIssue119WrongContextLength() { 693 | val original: String = 694 | """ 695 | const world: string = 'world', 696 | p: number | undefined = 42; 697 | 698 | console.log(`Hello, ${'$'}world}!`); 699 | """.trimIndent() 700 | val revised: String = 701 | """ 702 | const world: string = 'world'; 703 | const p: number | undefined = 42; 704 | 705 | console.log(`Hello, ${'$'}world}!`); 706 | """.trimIndent() 707 | val generator = DiffRowGenerator( 708 | showInlineDiffs = true, 709 | mergeOriginalRevised = true, 710 | inlineDiffByWord = true, 711 | oldTag = MarkdownTagGenerator("~"), 712 | newTag = MarkdownTagGenerator("**"), 713 | ) 714 | val rows: List = generator.generateDiffRows( 715 | original.split("\n"), 716 | revised.split("\n") 717 | ) 718 | rows.filter { it.tag != DiffRow.Tag.EQUAL } 719 | .forEach { println(it) } 720 | } 721 | 722 | @Test 723 | fun testIssue129WithDeltaDecompression() { 724 | val lines1 = listOf( 725 | "apple1", 726 | "apple2", 727 | "apple3", 728 | "A man named Frankenstein abc to Switzerland for cookies!", 729 | "banana1", 730 | "banana2", 731 | "banana3", 732 | ) 733 | 734 | val lines2 = listOf( 735 | "apple1", 736 | "apple2", 737 | "apple3", 738 | "A man named Frankenstein", 739 | "xyz", 740 | "to Switzerland for cookies!", 741 | "banana1", 742 | "banana2", 743 | "banana3", 744 | ) 745 | 746 | val generator = DiffRowGenerator( 747 | showInlineDiffs = true, 748 | oldTag = CustomOldTagGenerator(), 749 | newTag = CustomNewTagGenerator(), 750 | ) 751 | 752 | val diffRows = generator.generateDiffRows(lines1, lines2) 753 | val txt = diffRows.joinToString(separator = " ") { row -> row.tag.toString() } 754 | assertEquals(txt, "EQUAL EQUAL EQUAL CHANGE INSERT INSERT EQUAL EQUAL EQUAL") 755 | } 756 | 757 | @Test 758 | fun testIssue129SkipDeltaDecompression() { 759 | val lines1 = listOf( 760 | "apple1", 761 | "apple2", 762 | "apple3", 763 | "A man named Frankenstein abc to Switzerland for cookies!", 764 | "banana1", 765 | "banana2", 766 | "banana3", 767 | ) 768 | 769 | val lines2 = listOf( 770 | "apple1", 771 | "apple2", 772 | "apple3", 773 | "A man named Frankenstein", 774 | "xyz", 775 | "to Switzerland for cookies!", 776 | "banana1", 777 | "banana2", 778 | "banana3", 779 | ) 780 | 781 | val generator = DiffRowGenerator( 782 | showInlineDiffs = true, 783 | decompressDeltas = false, 784 | oldTag = CustomOldTagGenerator(), 785 | newTag = CustomNewTagGenerator(), 786 | ) 787 | 788 | val diffRows = generator.generateDiffRows(lines1, lines2) 789 | val txt = diffRows.joinToString(separator = " ") { row -> row.tag.toString() } 790 | assertEquals(txt, "EQUAL EQUAL EQUAL CHANGE CHANGE CHANGE EQUAL EQUAL EQUAL") 791 | } 792 | 793 | private class MarkdownTagGenerator(val str: String) : DiffTagGenerator { 794 | override fun generateOpen(tag: DiffRow.Tag): String = str 795 | override fun generateClose(tag: DiffRow.Tag): String = str 796 | } 797 | 798 | private class CustomOldTagGenerator : DiffTagGenerator { 799 | override fun generateOpen(tag: DiffRow.Tag): String = "==old$tag==>" 800 | override fun generateClose(tag: DiffRow.Tag): String = "<==old==" 801 | } 802 | 803 | private class CustomNewTagGenerator : DiffTagGenerator { 804 | override fun generateOpen(tag: DiffRow.Tag): String = "==new$tag==>" 805 | override fun generateClose(tag: DiffRow.Tag): String = "<==new==" 806 | } 807 | } 808 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/github/petertrr/diffutils/text/StringUtilsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * Copyright 2017 java-diff-utils. 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 | * This file has been modified by Peter Trifanov when porting from Java to Kotlin. 18 | */ 19 | package io.github.petertrr.diffutils.text 20 | 21 | import io.github.petertrr.diffutils.htmlEntities 22 | import io.github.petertrr.diffutils.normalize 23 | import io.github.petertrr.diffutils.wrapText 24 | import kotlin.test.Test 25 | import kotlin.test.assertEquals 26 | import kotlin.test.assertFailsWith 27 | 28 | class StringUtilsTest { 29 | /** 30 | * Test of htmlEntities method, of class StringUtils. 31 | */ 32 | @Test 33 | fun testHtmlEntities() { 34 | assertEquals("<test>", "".htmlEntities()) 35 | } 36 | 37 | /** 38 | * Test of normalize method, of class StringUtils. 39 | */ 40 | @Test 41 | fun testNormalize_String() { 42 | assertEquals(" test", "\ttest".normalize()) 43 | } 44 | 45 | /** 46 | * Test of wrapText method, of class 47 | */ 48 | @Test 49 | fun testWrapText_String_int() { 50 | assertEquals("te
st", "test".wrapText(2)) 51 | assertEquals("tes
t", "test".wrapText(3)) 52 | assertEquals("test", "test".wrapText(10)) 53 | assertEquals(".\uD800\uDC01
.", ".\uD800\uDC01.".wrapText(2)) 54 | assertEquals("..
\uD800\uDC01", "..\uD800\uDC01".wrapText(3)) 55 | } 56 | 57 | @Test 58 | fun testWrapText_String_int_zero() { 59 | assertFailsWith { "test".wrapText(-1) } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/github/petertrr/diffutils/utils/DeltaUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Peter Trifanov. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.github.petertrr.diffutils.utils 17 | 18 | import io.github.petertrr.diffutils.patch.ChangeDelta 19 | import io.github.petertrr.diffutils.patch.Chunk 20 | import io.github.petertrr.diffutils.patch.DeleteDelta 21 | import io.github.petertrr.diffutils.patch.InsertDelta 22 | 23 | fun deleteDeltaOf(sourcePosition: Int, sourceLines: List, 24 | targetPosition: Int = sourcePosition) = DeleteDelta( 25 | Chunk(sourcePosition, sourceLines), 26 | Chunk(targetPosition, emptyList()) 27 | ) 28 | 29 | fun changeDeltaOf(sourcePosition: Int, sourceLines: List, 30 | targetPosition: Int, targetLines: List) = ChangeDelta( 31 | Chunk(sourcePosition, sourceLines), 32 | Chunk(targetPosition, targetLines) 33 | ) 34 | 35 | fun insertDeltaOf(sourcePosition: Int, targetPosition: Int, targetLines: List) = InsertDelta( 36 | Chunk(sourcePosition, emptyList()), 37 | Chunk(targetPosition, targetLines) 38 | ) 39 | --------------------------------------------------------------------------------