Skip to content

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.

Default Rendered example
1
A quiet morning.
1
A quiet morning.
2 −
The door is closed.
2 +
The door is open.
3 −
Wait outside.
3 +
Come inside.
4 +
Welcome.
Usage Example
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.

Unequal replacement blocks Rendered example
10
func greet() {
20
func greet() {
11 −
    print("hello")
21 +
    name := "world"
12 −
    return
22 +
    print("hello, " + name)
23 +
    return
13
24
14
}
25
}
Unequal replacement blocks
@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.

Verbatim text and long lines Rendered example
1 −
	  <div data-version='old'>old</div>  
1 +
	  <img src=x onerror=alert('new')>  
2
  Olá, 世界 👋  
2
  Olá, 世界 👋  
3 −
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 · 
3 +
revised text · revised text · revised text · revised text · revised text · revised text · revised text · revised text · revised text · revised text · revised text · revised text · 
Verbatim text and long lines
@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.

Empty inputs Rendered example

No lines to compare.

Empty inputs
@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.

Identical inputs Rendered example
1
Nothing changed.
1
Nothing changed.
Identical inputs
@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.

Bounded scrolling Rendered example
1
Event 01: ready
1
Event 01: ready
2
Event 02: ready
2
Event 02: ready
3
Event 03: ready
3
Event 03: ready
4
Event 04: ready
4
Event 04: ready
5
Event 05: ready
5
Event 05: ready
6
Event 06: ready
6
Event 06: ready
7
Event 07: ready
7
Event 07: ready
8
Event 08: ready
8
Event 08: ready
9
Event 09: ready
9
Event 09: ready
10
Event 10: ready
10
Event 10: ready
11
Event 11: ready
11
Event 11: ready
12
Event 12: ready
12
Event 12: ready
13
Event 13: ready
13
Event 13: ready
14
Event 14: ready
14
Event 14: ready
15
Event 15: ready
15
Event 15: ready
16
Event 16: ready
16
Event 16: ready
17
Event 17: ready
17
Event 17: ready
18
Event 18: ready
18
Event 18: ready
19
Event 19: ready
19
Event 19: ready
20
Event 20: ready
20
Event 20: ready
21
Event 21: ready
21
Event 21: ready
22
Event 22: ready
22
Event 22: ready
23
Event 23: ready
23
Event 23: ready
24
Event 24: ready
24
Event 24: ready
25
Event 25: ready
25
Event 25: ready
26
Event 26: ready
26
Event 26: ready
27
Event 27: ready
27
Event 27: ready
28
Event 28: ready
28
Event 28: ready
29
Event 29: ready
29
Event 29: ready
30
Event 30: ready
30
Event 30: ready
Bounded scrolling
rows := 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.0

The 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
Open v0.4.0 API