Diff
Compare any text: prose, source code, logs, or configuration. RowsFromText uses sergi/go-diff to calculate complete line changes and source numbers from two strings. You can also supply precomputed rows for your own comparison policy. Goshtoso presents complete escaped lines. Below 640px each pair stacks Original then Revised, with labels on every populated side. Long lines scroll within a keyboard-accessible region bounded to 32rem by default.
A quiet morning.A quiet morning.The door is closed.The door is open.Wait outside.Come inside.Welcome.before := "A quiet morning.\nThe door is closed.\nWait outside.\n"
after := "A quiet morning.\nThe door is open.\nCome inside.\nWelcome.\n"
@diff.Diff(diff.Config{
ID: "text-review", BeforeLabel: "Original", AfterLabel: "Revised",
Rows: diff.RowsFromText(before, after),
})Built-in text: the development API provides expressions.Diff for application defaults, request preferences, and individual component overrides. Read the expression guide.
Unequal replacement blocks
Pair complete lines in source order. Use nil for missing sides, including unmatched lines in a replacement block. RowsFromText pairs these blocks automatically. A non-nil line with empty Text is a real blank line. Number is an optional independent source line number, never calculated by the renderer.
func greet() {func greet() { print("hello") name := "world" return print("hello, " + name) return}}@diff.Diff(diff.Config{
BeforeLabel: "Original", AfterLabel: "Revised",
Rows: []diff.Row{
{Before: &diff.Line{Text: "func greet() {", Number: 10}, After: &diff.Line{Text: "func greet() {", Number: 20}},
{Operation: diff.OperationReplace, Before: &diff.Line{Text: " print(\"hello\")", Number: 11}, After: &diff.Line{Text: " name := \"world\"", Number: 21}},
{Operation: diff.OperationReplace, Before: &diff.Line{Text: " return", Number: 12}, After: &diff.Line{Text: " print(\"hello, \" + name)", Number: 22}},
{Operation: diff.OperationReplace, After: &diff.Line{Text: " return", Number: 23}},
{Before: &diff.Line{Text: "", Number: 13}, After: &diff.Line{Text: "", Number: 24}},
{Before: &diff.Line{Text: "}", Number: 14}, After: &diff.Line{Text: "}", Number: 25}},
},
})Verbatim text and long lines
Spaces, tabs, Unicode, and HTML-looking source remain text. There is no lexer or client initialization. Long lines share horizontal scrolling without overlapping the opposite source or overflowing the page.
<div data-version='old'>old</div> <img src=x onerror=alert('new')> Olá, 世界 👋 Olá, 世界 👋 original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · original text · revised text · revised text · revised text · revised text · revised text · revised text · revised text · revised text · revised text · revised text · revised text · revised text · @diff.Diff(diff.Config{
BeforeLabel: "Original", AfterLabel: "Revised",
Rows: []diff.Row{
{Operation: diff.OperationReplace, Before: &diff.Line{Text: "\t <div data-version='old'>old</div> ", Number: 1}, After: &diff.Line{Text: "\t <img src=x onerror=alert('new')> ", Number: 1}},
{Before: &diff.Line{Text: " Olá, 世界 👋 ", Number: 2}, After: &diff.Line{Text: " Olá, 世界 👋 ", Number: 2}},
{Operation: diff.OperationReplace, Before: &diff.Line{Text: strings.Repeat("original text · ", 24), Number: 3}, After: &diff.Line{Text: strings.Repeat("revised text · ", 12), Number: 3}},
},
})Empty inputs
Empty or all-padding rows show the empty state. EmptyText and the built-in expressions can supply application-owned wording.
No lines to compare.
@diff.Diff(diff.Config{
BeforeLabel: "Original", AfterLabel: "Revised",
Rows: nil,
})Identical inputs
Supply unchanged rows for matching text. Both sources keep their labels and optional numbers; the viewer performs no equality check.
Nothing changed.Nothing changed.@diff.Diff(diff.Config{
BeforeLabel: "Original", AfterLabel: "Revised",
Rows: []diff.Row{
{Before: &diff.Line{Text: "Nothing changed.", Number: 1}, After: &diff.Line{Text: "Nothing changed.", Number: 1}},
},
})Bounded scrolling
Set MaxHeight to a CSS length to bound long comparisons. Focus the region and use the arrow keys to scroll. An ID provides an ordinary HTMX fragment target: render diff.Diff again in your handler and replace the region with outerHTML; no component-specific endpoint or JavaScript is needed.
Event 01: readyEvent 01: readyEvent 02: readyEvent 02: readyEvent 03: readyEvent 03: readyEvent 04: readyEvent 04: readyEvent 05: readyEvent 05: readyEvent 06: readyEvent 06: readyEvent 07: readyEvent 07: readyEvent 08: readyEvent 08: readyEvent 09: readyEvent 09: readyEvent 10: readyEvent 10: readyEvent 11: readyEvent 11: readyEvent 12: readyEvent 12: readyEvent 13: readyEvent 13: readyEvent 14: readyEvent 14: readyEvent 15: readyEvent 15: readyEvent 16: readyEvent 16: readyEvent 17: readyEvent 17: readyEvent 18: readyEvent 18: readyEvent 19: readyEvent 19: readyEvent 20: readyEvent 20: readyEvent 21: readyEvent 21: readyEvent 22: readyEvent 22: readyEvent 23: readyEvent 23: readyEvent 24: readyEvent 24: readyEvent 25: readyEvent 25: readyEvent 26: readyEvent 26: readyEvent 27: readyEvent 27: readyEvent 28: readyEvent 28: readyEvent 29: readyEvent 29: readyEvent 30: readyEvent 30: readyrows := make([]diff.Row, 30)
for index := range rows {
text := fmt.Sprintf("Event %02d: ready", index + 1)
rows[index] = diff.Row{
Before: &diff.Line{Text: text, Number: index + 1},
After: &diff.Line{Text: text, Number: index + 1},
}
}
@diff.Diff(diff.Config{
ID: "long-comparison", BeforeLabel: "Original", AfterLabel: "Revised",
Rows: rows, MaxHeight: "180px",
})Go API
v0.4.0The examples above cover behavior and composition. pkg.go.dev is the canonical reference for exported types, functions, methods, and Go documentation.
github.com/araihu/goshtoso/components/diff