Character by character diff script written in Python producing HTML output
Project description
JLDiff
Character by character diff script written in Python producing HTML output — with optional word-level diffing and match coalescence.
This uses the Longest common subsequence algorithm to do a true character-by-character comparison — no line-by-line preprocessing. The result is HTML with red and green coloring showing exactly what changed.
A --word_level mode tokenizes input into words, whitespace, and punctuation, then diffs at the token level for cleaner, faster results on natural language text.
A --min_match N option merges short coincidental matches (fewer than N characters) into surrounding changes, producing cleaner diffs with fewer small fragments.
Note: Larger files take exponentially longer to process due to the nature of the algorithm. Word-level mode significantly reduces processing time by operating on tokens instead of individual characters.
Installation
From PyPI
pip install JLDiff
From GitHub (development version)
# With uv
uv pip install git+https://github.com/JEdward7777/JLDiff.git
# With pip
pip install git+https://github.com/JEdward7777/JLDiff.git
Run as a one-off CLI without installing
# From PyPI
uvx jldiff file1.txt file2.txt out.html
# From GitHub
uv run --from git+https://github.com/JEdward7777/JLDiff.git jldiff file1.txt file2.txt out.html
Usage
Command line
jldiff file1.txt file2.txt out.html [--same_size] [--word_level] [--min_match N]
| Flag | Description |
|---|---|
--same_size |
Keep diff text the same size as surrounding text (by default, changed text is rendered larger for visibility). |
--word_level |
Diff at the token level instead of character level. Tokens are groups of alphabetical characters, whitespace, or other characters. Produces cleaner whole-word diffs and runs significantly faster on large files. |
--min_match N |
Minimum match length to keep. Match groups shorter than N characters are merged into surrounding changes, reducing noise from small coincidental matches. Accepts --min_match N or --min_match=N. Works with both character-level and word-level diffs. |
As a library
Character-level diff (default)
from JLDiff import compute_diff, printDiffs
result = compute_diff("hello world", "hallo world", talk=False)
The compute_diff function takes two strings and returns a list of diff nodes. Each node has a .state (STATE_MATCH, STATE_PASSING_1ST, or STATE_PASSING_2ND) and a .content character. Set talk=False to suppress progress output to stdout.
Use printDiffs(result, output_file) to write the diff as HTML spans to a file-like object. printDiffs accepts both character-level and token-level diff output — it automatically flattens token-level nodes to characters via flatten_diff.
Word-level diff
from JLDiff import compute_diff_by_words, printDiffs
# Token-level diff — each node's .content is a whole token (e.g. "hello", " ", "world")
result = compute_diff_by_words("The quick brown fox", "The slow brown cat", talk=False)
# printDiffs handles flattening automatically
with open("out.html", "w") as f:
printDiffs(result, f)
compute_diff_by_words returns the unflattened token-level diff, so you can inspect which tokens changed:
for node in result:
if node.state != STATE_MATCH and node.content:
print(f"Changed token: {node.content!r}")
Custom character classes
By default, tokenization uses three character classes: alphabetical (str.isalpha), whitespace (str.isspace), and everything else. You can provide a custom classifier function:
from JLDiff import compute_diff_by_words, tokenize
# Classifier that treats digits as their own class
def my_classifier(ch):
if ch.isalpha():
return 'alpha'
elif ch.isdigit():
return 'digit'
elif ch.isspace():
return 'space'
else:
return 'other'
result = compute_diff_by_words(text1, text2, talk=False, classifier=my_classifier)
# Or use tokenize directly for full control
tokens = tokenize("hello123 world", classifier=my_classifier)
# ['hello', '123', ' ', 'world']
Match coalescence
When a diff contains many small coincidental matches that fragment the output, use coalesce_diff to merge them into surrounding changes:
from JLDiff import compute_diff, coalesce_diff, printDiffs
result = compute_diff("The quick brown fox", "The slow red fox", talk=False)
# Merge match groups shorter than 3 characters into surrounding changes
result = coalesce_diff(result, min_match=3)
with open("out.html", "w") as f:
printDiffs(result, f)
coalesce_diff works on output from both compute_diff and compute_diff_by_words. The min_match threshold always counts characters, even when operating on token-level diffs.
The algorithm:
- Groups consecutive same-state nodes into single multi-character nodes
- Oblates (discards) any match group shorter than
min_matchcharacters, splitting it into a deletion + insertion - Re-consolidates: between each pair of surviving matches, all deletions are collected into one node and all insertions into one node
Building blocks
For maximum control, use the individual functions:
from JLDiff import tokenize, compute_diff, coalesce_diff, flatten_diff, printDiffs
# 1. Tokenize
tokens1 = tokenize(text1)
tokens2 = tokenize(text2)
# 2. Diff on tokens (compute_diff works on any sequence of comparable elements)
token_diff = compute_diff(tokens1, tokens2, talk=False)
# 3. Optionally coalesce short matches
token_diff = coalesce_diff(token_diff, min_match=3)
# 4. Inspect token-level results directly
for node in token_diff:
print(node.state, repr(node.content))
# 5. Or flatten to character-level for HTML output
char_nodes = list(flatten_diff(token_diff))
flatten_diff is a generator that expands multi-character content nodes into single-character nodes. It's a transparent passthrough for character-level diffs (zero overhead).
coalesce_diff returns a new list of nodes — it does not modify the input. When min_match is 1 or less, it returns a copy of the input unchanged.
License
BSD 2-Clause — see LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jldiff-1.1.0.tar.gz.
File metadata
- Download URL: jldiff-1.1.0.tar.gz
- Upload date:
- Size: 11.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f8f49b5d2579cb9c546e6b3e0efd583906841b41d500d72840459c34927f06b
|
|
| MD5 |
871599f20b8861b08fc2ba03c3671b8d
|
|
| BLAKE2b-256 |
98a51264950cda8f9b8cd3b08d9b04ed66e9f8ffe9b7db762419763b5440fa47
|
Provenance
The following attestation bundles were made for jldiff-1.1.0.tar.gz:
Publisher:
publish.yml on JEdward7777/JLDiff
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jldiff-1.1.0.tar.gz -
Subject digest:
5f8f49b5d2579cb9c546e6b3e0efd583906841b41d500d72840459c34927f06b - Sigstore transparency entry: 1391478574
- Sigstore integration time:
-
Permalink:
JEdward7777/JLDiff@32142b5e53f3091d600832255e37eeab49e5f422 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/JEdward7777
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@32142b5e53f3091d600832255e37eeab49e5f422 -
Trigger Event:
release
-
Statement type:
File details
Details for the file jldiff-1.1.0-py3-none-any.whl.
File metadata
- Download URL: jldiff-1.1.0-py3-none-any.whl
- Upload date:
- Size: 11.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b60c5d7d586e320129aba3e5314c750fe715db86bf9c37a75b64d70f8259064
|
|
| MD5 |
456d91d07b1973303cf4187dc69795d6
|
|
| BLAKE2b-256 |
6c81f828791fb9f9769e338e093726a66b179985429b5f62cf0a7b33d6146f22
|
Provenance
The following attestation bundles were made for jldiff-1.1.0-py3-none-any.whl:
Publisher:
publish.yml on JEdward7777/JLDiff
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jldiff-1.1.0-py3-none-any.whl -
Subject digest:
2b60c5d7d586e320129aba3e5314c750fe715db86bf9c37a75b64d70f8259064 - Sigstore transparency entry: 1391478579
- Sigstore integration time:
-
Permalink:
JEdward7777/JLDiff@32142b5e53f3091d600832255e37eeab49e5f422 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/JEdward7777
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@32142b5e53f3091d600832255e37eeab49e5f422 -
Trigger Event:
release
-
Statement type: