Skip to content

atacan/swift-jsdiff

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JSDiff

A Swift wrapper around jsdiff using JavaScriptCore. Provides text diffing, patch creation, and patch application capabilities.

Requirements

JSDiff (Core Library)

  • macOS 11.0+ / iOS 12.0+ / tvOS 12.0+ / visionOS 1.0+
  • Swift 5.5+ (for actor-based concurrency)

JSDiffUI (SwiftUI Views)

  • macOS 12.0+ / iOS 15.0+ / tvOS 15.0+ / watchOS 8.0+ / visionOS 1.0+
  • Swift 5.5+

Installation

Swift Package Manager

Add the following to your Package.swift dependencies:

dependencies: [
    .package(url: "https://github.com/your-repo/JSDiff.git", from: "1.0.0")
]

Or add it directly in Xcode:

  1. File → Add Package Dependencies
  2. Enter the repository URL
  3. Select the version and add to your target

Importing

// Core diffing functionality
import JSDiff

// SwiftUI visualization views (optional)
import JSDiffUI

Usage

Initialization

import JSDiff

// JSDiff is an actor, so initialize it in an async context
let diff = JSDiff()

Character-Level Diff

let changes = await diff?.diffChars("abc", "adc")

for change in changes ?? [] {
    if change.isAdded {
        print("Added: \(change.value)")
    } else if change.isRemoved {
        print("Removed: \(change.value)")
    } else {
        print("Unchanged: \(change.value)")
    }
}

Word-Level Diff

let changes = await diff?.diffWords("hello world", "hello there")

Line-Level Diff

let oldText = """
line1
line2
line3
"""

let newText = """
line1
line2 modified
line3
"""

let changes = await diff?.diffLines(oldText, newText)

Options

Each diff method accepts an options object:

// Character diff with case insensitivity
let options = DiffCharsOptions(ignoreCase: true)
let changes = await diff?.diffChars("ABC", "abc", options: options)

// Line diff with whitespace ignoring
let lineOptions = DiffLinesOptions(ignoreWhitespace: true, newlineIsToken: true)
let changes = await diff?.diffLines(oldText, newText, options: lineOptions)

JSON Diff

Diff any Encodable types:

struct User: Codable {
    let name: String
    let age: Int
}

let oldUser = User(name: "Alice", age: 30)
let newUser = User(name: "Alice", age: 31)

let changes = try await diff?.diffJson(oldUser, newUser)

Or diff JSON strings directly:

let changes = await diff?.diffJson(#"{"name": "Alice"}"#, #"{"name": "Bob"}"#)

Array Diff

let oldArray = ["a", "b", "c"]
let newArray = ["a", "x", "c"]

let changes = await diff?.diffArrays(oldArray, newArray)

for change in changes ?? [] {
    print("Value: \(change.value)")  // Array of strings
    print("Count: \(change.count)")
}

Creating Patches

let patch = await diff?.createPatch(
    fileName: "example.txt",
    old: "old content\nline2",
    new: "new content\nline2"
)

print(patch)
// Output:
// --- example.txt
// +++ example.txt
// @@ -1,2 +1,2 @@
// -old content
// +new content
//  line2

Create patches for two different files:

let patch = await diff?.createTwoFilesPatch(
    oldFileName: "original.txt",
    newFileName: "modified.txt",
    old: oldContent,
    new: newContent
)

Applying Patches

let original = "old content\nline2"
let patch = await diff?.createPatch(fileName: "file.txt", old: original, new: "new content\nline2")

let result = await diff?.applyPatch(original, patch: patch!)
print(result)  // "new content\nline2"

Structured Patches

For programmatic access to patch data:

let structured = await diff?.structuredPatch(
    oldFileName: "old.txt",
    newFileName: "new.txt",
    old: oldContent,
    new: newContent
)

print(structured?.hunks.first?.oldStart)  // Starting line in old file
print(structured?.hunks.first?.lines)      // Changed lines

Parsing Patches

let patchString = """
--- old.txt
+++ new.txt
@@ -1,3 +1,3 @@
 line1
-line2
+line2 modified
 line3
"""

let patches = await diff?.parsePatch(patchString)

Reversing Patches

let reversed = await diff?.reversePatch(patchString)

Converting Change Output

Convert changes to XML format:

let changes = await diff?.diffChars("abc", "adc")
let xml = await diff?.convertChangesToXML(changes ?? [])
// Output: <del>a</del><ins>a</ins><span>b</span>...

Convert to diff-match-patch format:

let dmp = await diff?.convertChangesToDMP(changes ?? [])
for change in dmp ?? [] {
    print("Operation: \(change.operation), Value: \(change.value)")
    // operation: 0 = unchanged, 1 = added, -1 = removed
}

SwiftUI Visualization (JSDiffUI)

The JSDiffUI module provides ready-to-use SwiftUI views for visualizing diff output. It supports three display styles: inline, side-by-side, and unified.

Basic Usage

import JSDiff
import JSDiffUI

// Compute the diff
let diff = JSDiff()
let changes = await diff?.diffLines(oldText, newText)

// Display inline style
DiffView(changes: changes ?? [], displayStyle: .inline)

// Or side-by-side comparison
DiffView(changes: changes ?? [], displayStyle: .sideBySide)

// Or unified diff format
DiffView(changes: changes ?? [], displayStyle: .unified)

Display Styles

Style Description
.inline Single text view with changes highlighted in-place
.sideBySide Two-column view showing original and new versions
.unified Traditional patch format with +/- prefixes
Screenshot 2026-03-07 at 19 16 50 Screenshot 2026-03-07 at 19 16 40 Screenshot 2026-03-07 at 19 17 00

Styling Configuration

The DiffStyle struct provides extensive customization:

// Use a preset style
DiffView(changes: changes, style: .dark)
DiffView(changes: changes, style: .minimal)
DiffView(changes: changes, style: .colorful)

// Custom configuration
let customStyle = DiffStyle(
    addedBackground: .green.opacity(0.25),
    addedText: .primary,
    removedBackground: .red.opacity(0.25),
    removedText: .primary,
    font: .system(.body, design: .monospaced),
    showLineNumbers: true,
    addedPrefix: "+",
    removedPrefix: "-"
)

DiffView(changes: changes, style: customStyle)

Modifier Methods

Chain modifiers for convenient configuration:

DiffView(changes: changes, displayStyle: .sideBySide)
    .diffStyle(.colorful)
    .showLineNumbers(false)
    .font(.system(.caption, design: .monospaced))

Available Style Presets

Preset Description
.default Standard colors with line numbers
.dark Optimized for dark mode
.minimal Subtle highlighting, no line numbers
.colorful Vibrant green/red highlights

DiffStyle Configuration Options

Property Type Default Description
addedBackground Color Green 20% Background for added inline text
addedText Color .primary Text color for added content
removedBackground Color Red 20% Background for removed inline text
removedText Color .primary Text color for removed content
addedLineBackground Color Green 15% Background for added lines
removedLineBackground Color Red 15% Background for removed lines
font Font Monospaced body Font for diff content
showLineNumbers Bool true Show line numbers column
lineSpacing CGFloat 2 Spacing between lines
addedPrefix String "+" Prefix for added lines (unified style)
removedPrefix String "-" Prefix for removed lines (unified style)
minLineHeight CGFloat 22 Minimum height per line

DiffStyle Builder Methods

let style = DiffStyle.default
    .withShowLineNumbers(false)
    .withFont(.system(.caption, design: .monospaced))
    .withLineSpacing(4)
    .withAddedColors(background: .green.opacity(0.3), text: .green)
    .withRemovedColors(background: .red.opacity(0.3), text: .red)
    .withPrefixes(added: "", removed: "")

Individual Views

For more control, use individual view components:

// Inline view with character-level highlighting
InlineDiffView(changes: changes, style: .default)

// Side-by-side comparison
SideBySideDiffView(changes: changes, style: .default)

// Unified diff view
UnifiedDiffView(changes: changes, style: .default)

Patch Views

Display structured patches with file headers:

// Single patch
let patch = await diff?.structuredPatch(
    oldFileName: "original.txt",
    newFileName: "modified.txt",
    old: oldContent,
    new: newContent
)

if let patch {
    PatchView(patch: patch)
}

// Multiple patches
let patches = await diff?.parsePatch(patchString)
if let patches {
    PatchesView(patches: patches)
}

Style Picker Component

Include a style picker for user customization:

struct DiffPreviewView: View {
    @State private var displayStyle: DiffDisplayStyle = .inline
    let changes: [Change]
    
    var body: some View {
        VStack {
            DiffStylePicker(displayStyle: $displayStyle)
            DiffView(changes: changes, displayStyle: displayStyle)
        }
    }
}

Thread Safety

JSDiff is implemented as a Swift actor, providing compile-time thread safety. All methods are isolated to the actor's context.

// Safe to use from multiple concurrent tasks
Task {
    let changes1 = await diff?.diffChars("a", "b")
}

Task {
    let changes2 = await diff?.diffWords("hello", "world")
}

Change Object

public struct Change: Sendable, Codable, Equatable {
    public let value: String      // The text content
    public let added: Bool        // true if inserted
    public let removed: Bool      // true if deleted
    public let count: Int         // Number of tokens (chars/words/lines)
    
    public var isUnchanged: Bool  // true if neither added nor removed
    public var isAdded: Bool      // convenience property
    public var isRemoved: Bool    // convenience property
}

Available Diff Methods

Method Token Type Use Case
diffChars Characters Fine-grained text comparison
diffWords Words (ignoring whitespace) Document editing
diffWordsWithSpace Words + whitespace Precise whitespace tracking
diffLines Lines File version comparison
diffSentences Sentences Paragraph-level changes
diffCss CSS tokens Stylesheet comparison
diffJson JSON lines API responses, config files
diffArrays Custom arrays Tokenized comparison

License

This package wraps jsdiff which is licensed under BSD-3-Clause. See Sources/JSDiff/Resources/LICENSE for details.

The Swift wrapper code is available under the MIT license.

About

A Swift wrapper for the jsdiff JavaScript library using JavaScriptCore

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages