Word-level diff between two versions of a text field.
Built on List.myers_difference/2 over whitespace-preserving tokens, so
no diff dependency is needed and rejoining the fragments reproduces the
inputs exactly — the view relies on that to render "before" and "after"
from one pass.
Both functions are pure. summary/2 needs an exact changed-fragment
count, which means it runs the same List.myers_difference/2 pass as
words/2 — through a shared private helper, not by calling words/2
itself (see that function's doc for why the distinction matters) — it
is not a cheap approximation, just a smaller return value.
Myers is O(N*D), so the cost tracks how DIFFERENT the two texts are, not
how long they are. Measured on a 1.7 KB body_html:
small edit 0.55 ms
half the text 2.33 ms
wholly rewritten 12.0 msA caller listing many rows must bound how many it renders at once: 529 rows of the last kind is 6.3 seconds inside the LiveView process. Page the rows and call this only for the page being shown.
Both functions emit :telemetry on every call
([:phoenix_kit_ecommerce, :shopify, :text_diff, :words | :summary],
empty measurements/metadata) — cheap (a no-op when nothing's attached)
and the only way a caller enforcing "only for the page being shown" or
"only for the row that's expanded" can prove it from outside this
module, since correct output looks identical whether or not those
guarantees held.
Summary
Functions
Small-payload shape of the change: how many changed regions, and how much longer or shorter the text became. Getting an exact count still requires running the full diff (see the module doc) — this is smaller to return and to render, not cheaper to compute.
Returns the diff as ordered fragments. nil is treated as an empty string.
Types
@type fragment() :: {:eq | :del | :ins, String.t()}
Functions
@spec summary(String.t() | nil, String.t() | nil) :: %{ fragments: non_neg_integer(), length_delta: integer() }
Small-payload shape of the change: how many changed regions, and how much longer or shorter the text became. Getting an exact count still requires running the full diff (see the module doc) — this is smaller to return and to render, not cheaper to compute.
Returns the diff as ordered fragments. nil is treated as an empty string.