-module(gliff).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/gliff.gleam").
-export([to_unified/3, to_unified_with/4, to_ansi/3, to_ansi_inline/1, from_unified/1, apply_patch/2, apply_patch_fuzzy/3, similarity/1, cleanup_semantic/1, cleanup_semantic_lossless/1, inline_highlight/1, merge3/3, default_config/0, diff/2, diff_chars/2, diff_myers/2, diff_patience/2, diff_words/2, diff_with/3, diff_chars_with/3]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" gliff — A text diffing library for Gleam.\n"
"\n"
" Provides Myers and Patience diff algorithms, unified diff formatting,\n"
" patch application, semantic cleanup, and inline highlighting.\n"
).
-file("src/gliff.gleam", 54).
?DOC(
" Format a list of edits as a unified diff string.\n"
"\n"
" The output matches the format produced by `diff -u`, with 3 lines of\n"
" context around each change. `old_name` and `new_name` appear in the\n"
" `---` and `+++` header lines.\n"
).
-spec to_unified(list(gliff@types:edit()), binary(), binary()) -> binary().
to_unified(Edits, Old_name, New_name) ->
gliff@internal@unified:to_unified(Edits, Old_name, New_name).
-file("src/gliff.gleam", 66).
?DOC(
" Format edits as a unified diff with a custom number of context lines.\n"
"\n"
" Same as `to_unified` but allows specifying how many unchanged lines\n"
" surround each change. Equivalent to `diff -U<n>`.\n"
).
-spec to_unified_with(list(gliff@types:edit()), binary(), binary(), integer()) -> binary().
to_unified_with(Edits, Old_name, New_name, Context) ->
gliff@internal@unified:to_unified_with(Edits, Old_name, New_name, Context).
-file("src/gliff.gleam", 79).
?DOC(
" Render edits as a colored diff string using ANSI escape codes.\n"
"\n"
" Deletions appear in red, insertions in green, and hunk headers in cyan.\n"
" Output follows the same structure as unified diff format.\n"
).
-spec to_ansi(list(gliff@types:edit()), binary(), binary()) -> binary().
to_ansi(Edits, Old_name, New_name) ->
gliff@internal@ansi:to_ansi(Edits, Old_name, New_name).
-file("src/gliff.gleam", 91).
?DOC(
" Render edits with ANSI colors and inline character highlighting.\n"
"\n"
" Changed characters within modified lines are rendered in bold,\n"
" making it easy to see exactly what changed at a glance.\n"
).
-spec to_ansi_inline(list(gliff@types:edit())) -> binary().
to_ansi_inline(Edits) ->
gliff@internal@ansi:to_ansi_inline(Edits).
-file("src/gliff.gleam", 99).
?DOC(
" Parse a unified diff string into a list of hunks.\n"
"\n"
" Accepts the standard format produced by `diff -u` or `to_unified`.\n"
" Returns an error if the input is malformed.\n"
).
-spec from_unified(binary()) -> {ok, list(gliff@types:hunk())} |
{error, binary()}.
from_unified(Input) ->
gliff@internal@unified:from_unified(Input).
-file("src/gliff.gleam", 110).
?DOC(
" Apply edit operations to a text string to produce the target text.\n"
"\n"
" The fundamental property is:\n"
" `apply_patch(old, diff(old, new)) == Ok(new)`\n"
"\n"
" Returns an error if the text doesn't match the expected content\n"
" described by the Equal and Delete operations.\n"
).
-spec apply_patch(binary(), list(gliff@types:edit())) -> {ok, binary()} |
{error, binary()}.
apply_patch(Text, Edits) ->
gliff@internal@patch:apply_patch(Text, Edits).
-file("src/gliff.gleam", 120).
?DOC(
" Apply edit operations with fuzzy matching for context/delete lines.\n"
"\n"
" When Equal or Delete lines don't match exactly, accepts lines that\n"
" are similar enough based on character-level comparison.\n"
" Tolerance ranges from 0.0 (exact match, same as `apply_patch`) to\n"
" 1.0 (accept any line). A value of 0.6 works well for most cases.\n"
).
-spec apply_patch_fuzzy(binary(), list(gliff@types:edit()), float()) -> {ok,
binary()} |
{error, binary()}.
apply_patch_fuzzy(Text, Edits, Tolerance) ->
gliff@internal@fuzzy_patch:apply_patch_fuzzy(Text, Edits, Tolerance).
-file("src/gliff.gleam", 132).
?DOC(
" Compute the similarity ratio between two texts from their diff result.\n"
"\n"
" Returns a value between 0.0 (completely different) and 1.0 (identical).\n"
" Calculated as `2 * matching_elements / total_elements`.\n"
).
-spec similarity(list(gliff@types:edit())) -> float().
similarity(Edits) ->
gliff@internal@similarity:ratio(Edits).
-file("src/gliff.gleam", 170).
?DOC(
" Eliminate trivial equalities and merge adjacent edits.\n"
"\n"
" Removes small Equal sections that are surrounded by larger changes,\n"
" absorbing them into the Delete and Insert operations. This produces\n"
" fewer, larger edit blocks that are easier to read.\n"
).
-spec cleanup_semantic(list(gliff@types:edit())) -> list(gliff@types:edit()).
cleanup_semantic(Edits) ->
gliff@internal@cleanup:semantic(Edits).
-file("src/gliff.gleam", 179).
?DOC(
" Shift edit boundaries to natural positions without changing semantics.\n"
"\n"
" Moves edit boundaries to align with word boundaries, sentence endings,\n"
" or blank lines based on a scoring system. The resulting diff applies\n"
" identically but reads more naturally.\n"
).
-spec cleanup_semantic_lossless(list(gliff@types:edit())) -> list(gliff@types:edit()).
cleanup_semantic_lossless(Edits) ->
gliff@internal@cleanup:semantic_lossless(Edits).
-file("src/gliff.gleam", 188).
?DOC(
" Enrich line-level edits with character-level change highlighting.\n"
"\n"
" For adjacent Delete/Insert pairs, computes a character-level sub-diff\n"
" and returns spans marking which characters actually changed. Equal\n"
" edits pass through as InlineEqual.\n"
).
-spec inline_highlight(list(gliff@types:edit())) -> list(gliff@types:inline_edit()).
inline_highlight(Edits) ->
gliff@internal@inline:highlight(Edits).
-file("src/gliff.gleam", 201).
?DOC(
" Perform a 3-way merge between two diverged versions of a base text.\n"
"\n"
" Computes diff(base, ours) and diff(base, theirs), then combines\n"
" the changes. When both sides modify the same region differently,\n"
" a conflict is reported with Git-style conflict markers.\n"
"\n"
" Returns `MergeOk` if the merge is clean, or `MergeConflict` with\n"
" the merged text (including `<<<<<<<`/`=======`/`>>>>>>>` markers)\n"
" and a list of conflicts.\n"
).
-spec merge3(binary(), binary(), binary()) -> gliff@types:merge_result().
merge3(Base, Ours, Theirs) ->
gliff@internal@merge:merge3(Base, Ours, Theirs).
-file("src/gliff.gleam", 209).
?DOC(
" Create a default diff configuration.\n"
"\n"
" Returns `DiffConfig(algorithm: Myers, cleanup: NoCleanup, max_iterations: 0)`\n"
" where `max_iterations: 0` means unlimited computation.\n"
).
-spec default_config() -> gliff@types:diff_config().
default_config() ->
{diff_config, myers, no_cleanup, 0}.
-file("src/gliff.gleam", 264).
-spec apply_cleanup(list(gliff@types:edit()), gliff@types:cleanup()) -> list(gliff@types:edit()).
apply_cleanup(Edits, Mode) ->
case Mode of
no_cleanup ->
Edits;
semantic_cleanup ->
gliff@internal@cleanup:semantic(Edits);
semantic_lossless_cleanup ->
gliff@internal@cleanup:semantic_lossless(Edits)
end.
-file("src/gliff.gleam", 272).
-spec split_lines(binary()) -> list(binary()).
split_lines(Text) ->
case Text of
<<""/utf8>> ->
[];
_ ->
gleam@string:split(Text, <<"\n"/utf8>>)
end.
-file("src/gliff.gleam", 302).
-spec collect_equals(list(gliff@types:raw_edit()), list(binary())) -> {list(binary()),
list(gliff@types:raw_edit())}.
collect_equals(Raw, Acc) ->
case Raw of
[{raw_equal, V} | Rest] ->
collect_equals(Rest, [V | Acc]);
_ ->
{Acc, Raw}
end.
-file("src/gliff.gleam", 312).
-spec collect_inserts(list(gliff@types:raw_edit()), list(binary())) -> {list(binary()),
list(gliff@types:raw_edit())}.
collect_inserts(Raw, Acc) ->
case Raw of
[{raw_insert, V} | Rest] ->
collect_inserts(Rest, [V | Acc]);
_ ->
{Acc, Raw}
end.
-file("src/gliff.gleam", 322).
-spec collect_deletes(list(gliff@types:raw_edit()), list(binary())) -> {list(binary()),
list(gliff@types:raw_edit())}.
collect_deletes(Raw, Acc) ->
case Raw of
[{raw_delete, V} | Rest] ->
collect_deletes(Rest, [V | Acc]);
_ ->
{Acc, Raw}
end.
-file("src/gliff.gleam", 284).
-spec group_edits_loop(list(gliff@types:raw_edit()), list(gliff@types:edit())) -> list(gliff@types:edit()).
group_edits_loop(Raw, Acc) ->
case Raw of
[] ->
Acc;
[{raw_equal, V} | Rest] ->
{Equals, Remaining} = collect_equals(Rest, [V]),
group_edits_loop(Remaining, [{equal, lists:reverse(Equals)} | Acc]);
[{raw_insert, V@1} | Rest@1] ->
{Inserts, Remaining@1} = collect_inserts(Rest@1, [V@1]),
group_edits_loop(
Remaining@1,
[{insert, lists:reverse(Inserts)} | Acc]
);
[{raw_delete, V@2} | Rest@2] ->
{Deletes, Remaining@2} = collect_deletes(Rest@2, [V@2]),
group_edits_loop(
Remaining@2,
[{delete, lists:reverse(Deletes)} | Acc]
)
end.
-file("src/gliff.gleam", 279).
-spec group_edits(list(gliff@types:raw_edit())) -> list(gliff@types:edit()).
group_edits(Raw) ->
_pipe = group_edits_loop(Raw, []),
lists:reverse(_pipe).
-file("src/gliff.gleam", 31).
?DOC(
" Compute a line-level diff between two strings using the Myers algorithm.\n"
"\n"
" Returns a list of edit operations (Equal, Insert, Delete) where each\n"
" operation contains one or more contiguous lines.\n"
).
-spec diff(binary(), binary()) -> list(gliff@types:edit()).
diff(Old, New) ->
Old_lines = split_lines(Old),
New_lines = split_lines(New),
Raw_edits = gliff@internal@myers:diff(Old_lines, New_lines),
group_edits(Raw_edits).
-file("src/gliff.gleam", 42).
?DOC(
" Compute a character-level diff between two strings using the Myers algorithm.\n"
"\n"
" Each element in the returned edit list represents one or more contiguous\n"
" grapheme clusters that share the same edit type.\n"
).
-spec diff_chars(binary(), binary()) -> list(gliff@types:edit()).
diff_chars(Old, New) ->
Old_chars = gleam@string:to_graphemes(Old),
New_chars = gleam@string:to_graphemes(New),
Raw_edits = gliff@internal@myers:diff(Old_chars, New_chars),
group_edits(Raw_edits).
-file("src/gliff.gleam", 137).
?DOC(" Compute a line-level diff using the Myers algorithm (explicit alias for `diff`).\n").
-spec diff_myers(binary(), binary()) -> list(gliff@types:edit()).
diff_myers(Old, New) ->
diff(Old, New).
-file("src/gliff.gleam", 146).
?DOC(
" Compute a line-level diff using the Patience algorithm.\n"
"\n"
" Patience diff anchors on lines that appear exactly once in both texts,\n"
" then recursively diffs the gaps. This often produces more readable\n"
" output for code changes where blocks are reordered.\n"
).
-spec diff_patience(binary(), binary()) -> list(gliff@types:edit()).
diff_patience(Old, New) ->
Old_lines = split_lines(Old),
New_lines = split_lines(New),
Raw_edits = gliff@internal@patience:diff(Old_lines, New_lines),
group_edits(Raw_edits).
-file("src/gliff.gleam", 158).
?DOC(
" Compute a word-level diff between two strings.\n"
"\n"
" Tokenizes input by whitespace boundaries (preserving whitespace as\n"
" separate tokens), then diffs the token sequences. Each edit contains\n"
" one or more word/whitespace tokens.\n"
).
-spec diff_words(binary(), binary()) -> list(gliff@types:edit()).
diff_words(Old, New) ->
Old_tokens = gliff@internal@tokenize:words(Old),
New_tokens = gliff@internal@tokenize:words(New),
Raw_edits = gliff@internal@myers:diff(Old_tokens, New_tokens),
group_edits(Raw_edits).
-file("src/gliff.gleam", 219).
?DOC(
" Compute a line-level diff with full configuration.\n"
"\n"
" Allows selecting the algorithm, cleanup mode, and iteration budget.\n"
" Returns `Complete` if the diff finished normally, or `Truncated` if\n"
" the iteration budget was exceeded (in which case a crude but correct\n"
" fallback diff is returned).\n"
).
-spec diff_with(binary(), binary(), gliff@types:diff_config()) -> gliff@types:diff_result().
diff_with(Old, New, Config) ->
Old_tokens = split_lines(Old),
New_tokens = split_lines(New),
B = gliff@internal@budget:from_max(erlang:element(4, Config)),
{Raw_edits, Complete} = case erlang:element(2, Config) of
myers ->
gliff@internal@myers:diff_with_budget(Old_tokens, New_tokens, B);
patience ->
{gliff@internal@patience:diff(Old_tokens, New_tokens), true}
end,
Edits = group_edits(Raw_edits),
Edits_cleaned = apply_cleanup(Edits, erlang:element(3, Config)),
case Complete of
true ->
{complete, Edits_cleaned};
false ->
{truncated, Edits_cleaned}
end.
-file("src/gliff.gleam", 241).
?DOC(
" Compute a character-level diff with full configuration.\n"
"\n"
" Same as `diff_with` but operates on grapheme clusters instead of lines.\n"
).
-spec diff_chars_with(binary(), binary(), gliff@types:diff_config()) -> gliff@types:diff_result().
diff_chars_with(Old, New, Config) ->
Old_chars = gleam@string:to_graphemes(Old),
New_chars = gleam@string:to_graphemes(New),
B = gliff@internal@budget:from_max(erlang:element(4, Config)),
{Raw_edits, Complete} = case erlang:element(2, Config) of
myers ->
gliff@internal@myers:diff_with_budget(Old_chars, New_chars, B);
patience ->
{gliff@internal@patience:diff(Old_chars, New_chars), true}
end,
Edits = group_edits(Raw_edits),
Edits_cleaned = apply_cleanup(Edits, erlang:element(3, Config)),
case Complete of
true ->
{complete, Edits_cleaned};
false ->
{truncated, Edits_cleaned}
end.