diff --git a/src/commonMain/kotlin/io/github/petertrr/diffutils/DiffUtils.kt b/src/commonMain/kotlin/io/github/petertrr/diffutils/DiffUtils.kt index b76548c..6eb1337 100644 --- a/src/commonMain/kotlin/io/github/petertrr/diffutils/DiffUtils.kt +++ b/src/commonMain/kotlin/io/github/petertrr/diffutils/DiffUtils.kt @@ -22,11 +22,9 @@ package io.github.petertrr.diffutils import io.github.petertrr.diffutils.algorithm.DiffAlgorithm import io.github.petertrr.diffutils.algorithm.DiffAlgorithmListener -import io.github.petertrr.diffutils.algorithm.DiffEqualizer import io.github.petertrr.diffutils.algorithm.NoopAlgorithmListener import io.github.petertrr.diffutils.algorithm.myers.MyersDiff import io.github.petertrr.diffutils.patch.Patch -import io.github.petertrr.diffutils.patch.PatchFailedException import io.github.petertrr.diffutils.text.DiffRowGenerator import kotlin.jvm.JvmName import kotlin.jvm.JvmOverloads @@ -35,16 +33,16 @@ import kotlin.jvm.JvmOverloads private val lineBreak = Regex("\r\n|\r|\n") /** - * Computes the difference between the source and target text. + * Computes the difference between two strings. * * By default, uses the Myers algorithm. * - * @param sourceText The original text - * @param targetText The target text + * @param sourceText A string representing the original text + * @param targetText A string representing the revised text * @param algorithm The diff algorithm to use * @param progress The diff algorithm progress listener * @param includeEqualParts Whether to include equal data parts into the patch. `false` by default. - * @return The patch describing the difference between the original and target text + * @return The patch describing the difference between the original and revised strings */ @JvmOverloads public fun diff( @@ -62,37 +60,17 @@ public fun diff( includeEqualParts = includeEqualParts, ) -/** - * Computes the difference between the source and target list of elements using the Myers algorithm. - * - * @param source The original elements - * @param target The target elements - * @param equalizer The equalizer to replace the default compare algorithm [Any.equals]. - * If `null`, the default equalizer of the default algorithm is used. - * @return The patch describing the difference between the source and target sequences - */ -public fun diff( - source: List, - target: List, - equalizer: DiffEqualizer, -): Patch = - diff( - source = source, - target = target, - algorithm = MyersDiff(equalizer), - ) - /** * Computes the difference between the original and target list of elements. * - * By default, uses the Meyers algorithm. + * By default, uses the Myers algorithm. * - * @param source The original elements - * @param target The target elements + * @param source A list representing the original sequence of elements + * @param target A list representing the revised sequence of elements * @param algorithm The diff algorithm to use * @param progress The diff algorithm progress listener - * @param includeEqualParts Whether to include equal data parts into the patch. `false` by default. - * @return The patch describing the difference between the original and target sequences + * @param includeEqualParts Whether to include equal parts in the resulting patch. `false` by default. + * @return The patch describing the difference between the original and revised sequences */ @JvmOverloads public fun diff( @@ -114,6 +92,10 @@ public fun diff( * * This one uses the "trick" to make out of texts lists of characters, * like [DiffRowGenerator] does and merges those changes at the end together again. + * + * @param original A string representing the original text + * @param revised A string representing the revised text + * @return The patch describing the difference between the original and revised text */ public fun diffInline(original: String, revised: String): Patch { val origChars = original.toCharArray() @@ -142,22 +124,21 @@ public fun diffInline(original: String, revised: String): Patch { } /** - * Patch the original text with the given patch. + * Applies the given patch to the original list and returns the revised list. * - * @param original The original text + * @param original A list representing the original sequence of elements * @param patch The patch to apply - * @return The revised text - * @throws PatchFailedException If the patch cannot be applied + * @return A list representing the revised sequence of elements */ public fun patch(original: List, patch: Patch): List = patch.applyTo(original) /** - * Unpatch the revised text for a given patch + * Applies the given patch to the revised list and returns the original list. * - * @param revised The revised text - * @param patch The given patch - * @return The original text + * @param revised A list representing the revised sequence of elements + * @param patch The patch to apply + * @return A list representing the original sequence of elements */ public fun unpatch(revised: List, patch: Patch): List = patch.restore(revised) diff --git a/src/commonMain/kotlin/io/github/petertrr/diffutils/text/StringUtils.kt b/src/commonMain/kotlin/io/github/petertrr/diffutils/StringUtils.kt similarity index 68% rename from src/commonMain/kotlin/io/github/petertrr/diffutils/text/StringUtils.kt rename to src/commonMain/kotlin/io/github/petertrr/diffutils/StringUtils.kt index acb70d6..0625360 100644 --- a/src/commonMain/kotlin/io/github/petertrr/diffutils/text/StringUtils.kt +++ b/src/commonMain/kotlin/io/github/petertrr/diffutils/StringUtils.kt @@ -18,7 +18,7 @@ */ @file:JvmName("StringUtils") -package io.github.petertrr.diffutils.text +package io.github.petertrr.diffutils import kotlin.jvm.JvmName @@ -26,49 +26,49 @@ import kotlin.jvm.JvmName * Replaces all opening and closing tags (`<` and `>`) * with their escaped sequences (`<` and `>`). */ -internal fun htmlEntities(str: String): String = - str.replace("<", "<").replace(">", ">") +internal fun String.htmlEntities(): String = + replace("<", "<").replace(">", ">") /** * Normalizes a string by escaping some HTML meta characters * and replacing tabs with 4 spaces each. */ -internal fun normalize(str: String): String = - htmlEntities(str).replace("\t", " ") +internal fun String.normalize(): String = + htmlEntities().replace("\t", " ") /** - * Wrap the text with the given column width + * Wrap the text with the given column width. */ -internal fun wrapText(line: String, columnWidth: Int): String { +internal fun String.wrapText(columnWidth: Int): String { require(columnWidth >= 0) { "Column width must be greater than or equal to 0" } if (columnWidth == 0) { - return line + return this } - val length = line.length - val delimiter = "
".length + val length = length + val delimiterLength = "
".length var widthIndex = columnWidth - val b = StringBuilder(line) + val sb = StringBuilder(this) var count = 0 while (length > widthIndex) { - var breakPoint = widthIndex + delimiter * count + var breakPoint = widthIndex + delimiterLength * count - if (b[breakPoint - 1].isHighSurrogate() && b[breakPoint].isLowSurrogate()) { + if (sb[breakPoint - 1].isHighSurrogate() && sb[breakPoint].isLowSurrogate()) { // Shift a breakpoint that would split a supplemental code-point. breakPoint += 1 - if (breakPoint == b.length) { + if (breakPoint == sb.length) { // Break before instead of after if this is the last code-point. breakPoint -= 2 } } - b.insert(breakPoint, "
") + sb.insert(breakPoint, "
") widthIndex += columnWidth count++ } - return b.toString() + return sb.toString() } diff --git a/src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/myers/MyersDiff.kt b/src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/myers/MyersDiff.kt index 8eae8c1..dc1d56d 100644 --- a/src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/myers/MyersDiff.kt +++ b/src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/myers/MyersDiff.kt @@ -58,7 +58,7 @@ public class MyersDiff(private val equalizer: DiffEqualizer = EqualsDiffEq val max = origSize + revSize + 1 val size = 1 + 2 * max val middle = size / 2 - val diagonal: Array = arrayOfNulls(size) + val diagonal = arrayOfNulls(size) diagonal[middle + 1] = PathNode(0, -1, snake = true, bootstrap = true, prev = null) for (d in 0..( progress: DiffAlgorithmListener, ) { progress.diffStep((end1 - start1) / 2 + (end2 - start2) / 2, -1) - val middle = getMiddleSnake(data, start1, end1, start2, end2) if (middle == null || @@ -65,28 +64,29 @@ public class MyersDiffWithLinearSpace( ++i ++j } else { + // index is less than 0 here if data.script is empty + val index = data.script.size - 1 + // TODO: compress these commands if (end1 - start1 > end2 - start2) { - if (data.script.isEmpty() || - data.script[data.script.size - 1].endOriginal != i || - data.script[data.script.size - 1].deltaType != DeltaType.DELETE + if (index < 0 || + data.script[index].endOriginal != i || + data.script[index].deltaType != DeltaType.DELETE ) { data.script.add(Change(DeltaType.DELETE, i, i + 1, j, j)) } else { - data.script[data.script.size - 1] = - data.script[data.script.size - 1].copy(endOriginal = i + 1) + data.script[index] = data.script[index].copy(endOriginal = i + 1) } ++i } else { - if (data.script.isEmpty() || - data.script[data.script.size - 1].endRevised != j || - data.script[data.script.size - 1].deltaType != DeltaType.INSERT + if (index < 0 || + data.script[index].endRevised != j || + data.script[index].deltaType != DeltaType.INSERT ) { data.script.add(Change(DeltaType.INSERT, i, i, j, j + 1)) } else { - data.script[data.script.size - 1] = - data.script[data.script.size - 1].copy(endRevised = j + 1) + data.script[index] = data.script[index].copy(endRevised = j + 1) } ++j diff --git a/src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/myers/PathNode.kt b/src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/myers/PathNode.kt index b6aa1bb..eca1ef2 100644 --- a/src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/myers/PathNode.kt +++ b/src/commonMain/kotlin/io/github/petertrr/diffutils/algorithm/myers/PathNode.kt @@ -55,11 +55,7 @@ internal class PathNode( return null } - return if (!snake && prev != null) { - prev.previousSnake() - } else { - this - } + return if (!snake && prev != null) prev.previousSnake() else this } override fun toString(): String { diff --git a/src/commonMain/kotlin/io/github/petertrr/diffutils/text/DiffRowGenerator.kt b/src/commonMain/kotlin/io/github/petertrr/diffutils/text/DiffRowGenerator.kt index 9ecf650..a7d1b33 100644 --- a/src/commonMain/kotlin/io/github/petertrr/diffutils/text/DiffRowGenerator.kt +++ b/src/commonMain/kotlin/io/github/petertrr/diffutils/text/DiffRowGenerator.kt @@ -18,9 +18,10 @@ */ package io.github.petertrr.diffutils.text -import io.github.petertrr.diffutils.algorithm.DiffEqualizer +import io.github.petertrr.diffutils.algorithm.DiffAlgorithm import io.github.petertrr.diffutils.algorithm.EqualsDiffEqualizer import io.github.petertrr.diffutils.algorithm.IgnoreWsStringDiffEqualizer +import io.github.petertrr.diffutils.algorithm.myers.MyersDiff import io.github.petertrr.diffutils.diff import io.github.petertrr.diffutils.patch.ChangeDelta import io.github.petertrr.diffutils.patch.Chunk @@ -29,6 +30,7 @@ import io.github.petertrr.diffutils.patch.Delta import io.github.petertrr.diffutils.patch.DeltaType import io.github.petertrr.diffutils.patch.InsertDelta import io.github.petertrr.diffutils.patch.Patch +import io.github.petertrr.diffutils.wrapText import kotlin.math.max import kotlin.math.min @@ -41,7 +43,7 @@ import kotlin.math.min * @param columnWidth Set the column width of generated lines of original and revised texts. * Making it < 0 doesn't make any sense. * @param ignoreWhiteSpaces Ignore white spaces in generating diff rows or not - * @param equalizer Provide an equalizer for diff processing + * @param algorithm The diffing algorithm to use. By default [MyersDiff]. * @param inlineDiffByWord Per default each character is separately processed. * Setting this parameter to `true` introduces processing by word, which does * not deliver in word changes. Therefore, the whole word will be tagged as changed: @@ -57,8 +59,7 @@ import kotlin.math.min * Default: `false` * @param newTag Generator for New-Text-Tags * @param oldTag Generator for Old-Text-Tags - * @param reportLinesUnchanged Give the original old and new text lines to [DiffRow] - * without any additional processing and without any tags to highlight the change. + * @param reportLinesUnchanged Report all lines without markup on the old or new text. * Default: `false` * @param lineNormalizer By default, [DiffRowGenerator] preprocesses lines for HTML output. * Tabs and special HTML characters like "<" are replaced with its encoded value. @@ -80,12 +81,12 @@ import kotlin.math.min public class DiffRowGenerator( private val columnWidth: Int = 80, private val ignoreWhiteSpaces: Boolean = false, - private var equalizer: DiffEqualizer = if (ignoreWhiteSpaces) IgnoreWsStringDiffEqualizer() else EqualsDiffEqualizer(), + private val algorithm: DiffAlgorithm = defaultAlgorithm(ignoreWhiteSpaces), inlineDiffByWord: Boolean = false, - private val inlineDiffSplitter: DiffSplitter = if (inlineDiffByWord) WordDiffSplitter() else CharDiffSplitter(), + private val inlineDiffSplitter: DiffSplitter = defaultSplitter(inlineDiffByWord), private val mergeOriginalRevised: Boolean = false, - private val newTag: DiffTagGenerator = NewDiffTagGenerator(), - private val oldTag: DiffTagGenerator = OldDiffTagGenerator(), + private val newTag: DiffTagGenerator = HtmlDiffTagGenerator("editNewInline"), + private val oldTag: DiffTagGenerator = HtmlDiffTagGenerator("editOldInline"), private val reportLinesUnchanged: Boolean = false, private val lineNormalizer: DiffLineNormalizer = HtmlLineNormalizer(), private val processDiffs: DiffLineProcessor? = null, @@ -102,7 +103,7 @@ public class DiffRowGenerator( * @return The [DiffRow]s between original and revised texts */ public fun generateDiffRows(original: List, revised: List): List = - generateDiffRows(original, diff(original, revised, equalizer)) + generateDiffRows(original, diff(original, revised, algorithm)) /** * Generates the [DiffRow]s describing the difference between original and @@ -138,8 +139,8 @@ public class DiffRowGenerator( return diffRows } - internal fun normalizeLines(list: List): List = - if (reportLinesUnchanged) list else list.map { lineNormalizer.normalize(it) } + private fun normalizeLines(list: List): List = + if (reportLinesUnchanged) list else list.map(lineNormalizer::normalize) /** * Transforms one patch delta into a [DiffRow] object. @@ -262,7 +263,7 @@ public class DiffRowGenerator( } private fun buildDiffRowWithoutNormalizing(type: DiffRow.Tag, oldLine: String, newLine: String): DiffRow = - DiffRow(type, wrapText(oldLine, columnWidth), wrapText(newLine, columnWidth)) + DiffRow(type, oldLine.wrapText(columnWidth), newLine.wrapText(columnWidth)) /** * Add the inline diffs for given delta @@ -278,7 +279,7 @@ public class DiffRowGenerator( val origList = inlineDiffSplitter.split(joinedOrig) val revList = inlineDiffSplitter.split(joinedRev) - val diff = diff(origList, revList, equalizer) + val diff = diff(origList, revList, algorithm) val inlineDeltas = diff.deltas.reversed() for (inlineDelta in inlineDeltas) { @@ -364,8 +365,8 @@ public class DiffRowGenerator( } } - val origResult = StringBuilder() - val revResult = StringBuilder() + val origResult = StringBuilder(origList.size) + val revResult = StringBuilder(revList.size) for (character in origList) { origResult.append(character) @@ -379,9 +380,11 @@ public class DiffRowGenerator( // trailing empty string by default val original = origResult.split("\n").dropLastWhile(String::isEmpty) val revised = revResult.split("\n").dropLastWhile(String::isEmpty) - val diffRows = ArrayList() - for (j in 0..(size) + + for (j in 0.. { + val equalizer = if (ignoreWhiteSpaces) IgnoreWsStringDiffEqualizer() else EqualsDiffEqualizer() + return MyersDiff(equalizer) +} + +private fun defaultSplitter(inlineDiffByWord: Boolean): DiffSplitter = + if (inlineDiffByWord) WordDiffSplitter() else CharDiffSplitter() diff --git a/src/commonMain/kotlin/io/github/petertrr/diffutils/text/NewDiffTagGenerator.kt b/src/commonMain/kotlin/io/github/petertrr/diffutils/text/HtmlDiffTagGenerator.kt similarity index 86% rename from src/commonMain/kotlin/io/github/petertrr/diffutils/text/NewDiffTagGenerator.kt rename to src/commonMain/kotlin/io/github/petertrr/diffutils/text/HtmlDiffTagGenerator.kt index 6d3af7a..bfd6772 100644 --- a/src/commonMain/kotlin/io/github/petertrr/diffutils/text/NewDiffTagGenerator.kt +++ b/src/commonMain/kotlin/io/github/petertrr/diffutils/text/HtmlDiffTagGenerator.kt @@ -15,9 +15,9 @@ */ package io.github.petertrr.diffutils.text -internal class NewDiffTagGenerator : DiffTagGenerator { +internal class HtmlDiffTagGenerator(private val className: String) : DiffTagGenerator { override fun generateOpen(tag: DiffRow.Tag): String = - "" + "" override fun generateClose(tag: DiffRow.Tag): String = "" diff --git a/src/commonMain/kotlin/io/github/petertrr/diffutils/text/HtmlLineNormalizer.kt b/src/commonMain/kotlin/io/github/petertrr/diffutils/text/HtmlLineNormalizer.kt index 359484a..59cb8dc 100644 --- a/src/commonMain/kotlin/io/github/petertrr/diffutils/text/HtmlLineNormalizer.kt +++ b/src/commonMain/kotlin/io/github/petertrr/diffutils/text/HtmlLineNormalizer.kt @@ -15,9 +15,9 @@ */ package io.github.petertrr.diffutils.text -import io.github.petertrr.diffutils.text.normalize as normalizeFn +import io.github.petertrr.diffutils.normalize internal class HtmlLineNormalizer : DiffLineNormalizer { override fun normalize(line: String): String = - normalizeFn(line) + line.normalize() } diff --git a/src/commonMain/kotlin/io/github/petertrr/diffutils/text/OldDiffTagGenerator.kt b/src/commonMain/kotlin/io/github/petertrr/diffutils/text/OldDiffTagGenerator.kt deleted file mode 100644 index df6b071..0000000 --- a/src/commonMain/kotlin/io/github/petertrr/diffutils/text/OldDiffTagGenerator.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2024 Peter Trifanov. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.github.petertrr.diffutils.text - -internal class OldDiffTagGenerator : DiffTagGenerator { - override fun generateOpen(tag: DiffRow.Tag): String = - "" - - override fun generateClose(tag: DiffRow.Tag): String = - "" -} diff --git a/src/commonMain/kotlin/io/github/petertrr/diffutils/text/WordDiffSplitter.kt b/src/commonMain/kotlin/io/github/petertrr/diffutils/text/WordDiffSplitter.kt index d8850b1..bfa249b 100644 --- a/src/commonMain/kotlin/io/github/petertrr/diffutils/text/WordDiffSplitter.kt +++ b/src/commonMain/kotlin/io/github/petertrr/diffutils/text/WordDiffSplitter.kt @@ -15,10 +15,13 @@ */ package io.github.petertrr.diffutils.text +// As a "global" variable to avoid re-compiling the regex each time +private val defaultPattern = Regex("\\s+|[,.\\[\\](){}/\\\\*+\\-#]") + /** * Splitting lines by word to achieve word by word diff checking. */ -internal class WordDiffSplitter(private val pattern: Regex = Regex("\\s+|[,.\\[\\](){}/\\\\*+\\-#]")) : DiffSplitter { +internal class WordDiffSplitter(private val pattern: Regex = defaultPattern) : DiffSplitter { override fun split(line: String): MutableList { val matchResults = pattern.findAll(line) val list = ArrayList() diff --git a/src/commonTest/kotlin/io/github/petertrr/diffutils/text/DiffRowGeneratorTest.kt b/src/commonTest/kotlin/io/github/petertrr/diffutils/text/DiffRowGeneratorTest.kt index 361df0f..5d7eb28 100644 --- a/src/commonTest/kotlin/io/github/petertrr/diffutils/text/DiffRowGeneratorTest.kt +++ b/src/commonTest/kotlin/io/github/petertrr/diffutils/text/DiffRowGeneratorTest.kt @@ -40,8 +40,8 @@ class DiffRowGeneratorTest { */ @Test fun testNormalize_List() { - val generator = DiffRowGenerator() - assertEquals(listOf(" test"), generator.normalizeLines(listOf("\ttest"))) + val normalizer = HtmlLineNormalizer() + assertEquals(" test", normalizer.normalize("\ttest")) } @Test diff --git a/src/commonTest/kotlin/io/github/petertrr/diffutils/text/StringUtilsTest.kt b/src/commonTest/kotlin/io/github/petertrr/diffutils/text/StringUtilsTest.kt index 824bcf5..1c3c6ab 100644 --- a/src/commonTest/kotlin/io/github/petertrr/diffutils/text/StringUtilsTest.kt +++ b/src/commonTest/kotlin/io/github/petertrr/diffutils/text/StringUtilsTest.kt @@ -18,6 +18,9 @@ */ package io.github.petertrr.diffutils.text +import io.github.petertrr.diffutils.htmlEntities +import io.github.petertrr.diffutils.normalize +import io.github.petertrr.diffutils.wrapText import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -27,8 +30,8 @@ class StringUtilsTest { * Test of htmlEntities method, of class StringUtils. */ @Test - fun testHtmlEntites() { - assertEquals("<test>", htmlEntities("")) + fun testHtmlEntities() { + assertEquals("<test>", "".htmlEntities()) } /** @@ -36,7 +39,7 @@ class StringUtilsTest { */ @Test fun testNormalize_String() { - assertEquals(" test", normalize("\ttest")) + assertEquals(" test", "\ttest".normalize()) } /** @@ -44,15 +47,15 @@ class StringUtilsTest { */ @Test fun testWrapText_String_int() { - assertEquals("te
st", wrapText("test", 2)) - assertEquals("tes
t", wrapText("test", 3)) - assertEquals("test", wrapText("test", 10)) - assertEquals(".\uD800\uDC01
.", wrapText(".\uD800\uDC01.", 2)) - assertEquals("..
\uD800\uDC01", wrapText("..\uD800\uDC01", 3)) + assertEquals("te
st", "test".wrapText(2)) + assertEquals("tes
t", "test".wrapText(3)) + assertEquals("test", "test".wrapText(10)) + assertEquals(".\uD800\uDC01
.", ".\uD800\uDC01.".wrapText(2)) + assertEquals("..
\uD800\uDC01", "..\uD800\uDC01".wrapText(3)) } @Test fun testWrapText_String_int_zero() { - assertFailsWith { wrapText("test", -1) } + assertFailsWith { "test".wrapText(-1) } } }