|
1 | | -# BlazorTextDiff 🔍 |
| 1 | +# BlazorTextDiff |
2 | 2 |
|
3 | | -A modern Blazor component for displaying side-by-side text differences with syntax highlighting and advanced comparison features. Built on top of the powerful [DiffPlex](https://github.com/mmanela/diffplex) library. |
| 3 | +A Blazor component for displaying side-by-side text differences with character-level highlighting. Built on [DiffPlex](https://github.com/mmanela/diffplex). |
4 | 4 |
|
5 | | -## 🚀 Features |
| 5 | +## Features |
6 | 6 |
|
7 | | -- **Side-by-side comparison** with clear visual indicators |
8 | | -- **Syntax highlighting** for better readability |
9 | | -- **Ignore case and whitespace** options |
10 | | -- **Async diff processing** for large texts |
11 | | -- **Customizable headers** with diff statistics |
12 | | -- **Responsive design** that works on all devices |
13 | | -- **Easy integration** with existing Blazor applications |
| 7 | +- Side-by-side diff with line numbers |
| 8 | +- Character-level highlighting within changed lines |
| 9 | +- Word-level soft highlight with character-level strong highlight for partial changes |
| 10 | +- Adjacent character highlights merge into smooth pill shapes |
| 11 | +- Collapse/expand unchanged sections |
| 12 | +- Ignore case and whitespace options |
| 13 | +- Custom header with diff statistics |
| 14 | +- Custom CSS class and attribute support |
| 15 | +- Fully themeable via CSS custom properties |
| 16 | +- Dark mode via `prefers-color-scheme` |
| 17 | +- Responsive and accessible |
14 | 18 |
|
15 | | -## 📊 Status |
| 19 | +## Status |
16 | 20 |
|
17 | 21 | [](https://github.com/lzinga/BlazorTextDiff/actions/workflows/publish-packages.yml) |
18 | 22 | [](https://github.com/lzinga/BlazorTextDiff/actions/workflows/deploy-pages.yml) |
19 | 23 | [](https://www.nuget.org/packages/BlazorTextDiff/) |
20 | | -[](https://www.nuget.org/packages/BlazorTextDiff/) |
21 | 24 |
|
22 | | -## 🎮 Live Demo |
| 25 | +## Live Demo |
23 | 26 |
|
24 | | -Try the interactive demo: [https://lzinga.github.io/BlazorTextDiff/](https://lzinga.github.io/BlazorTextDiff/) |
| 27 | +[https://lzinga.github.io/BlazorTextDiff/](https://lzinga.github.io/BlazorTextDiff/) |
25 | 28 |
|
26 | | -## 📸 Screenshots |
| 29 | +## Installation |
27 | 30 |
|
28 | | - |
29 | | -*Basic text comparison showing additions, deletions, and modifications* |
| 31 | +```bash |
| 32 | +dotnet add package BlazorTextDiff |
| 33 | +``` |
30 | 34 |
|
31 | | - |
32 | | -*Async processing for large text comparisons* |
| 35 | +## Setup |
33 | 36 |
|
34 | | -## 📦 Installation |
| 37 | +Add the stylesheet to your `index.html` or `_Host.cshtml`: |
35 | 38 |
|
36 | | -Install the NuGet package: |
| 39 | +```html |
| 40 | +<link href="_content/BlazorTextDiff/css/BlazorDiff.css" rel="stylesheet" /> |
| 41 | +``` |
37 | 42 |
|
38 | | -```bash |
39 | | -dotnet add package BlazorTextDiff |
| 43 | +No JavaScript or service registration is required. |
| 44 | + |
| 45 | +## Usage |
| 46 | + |
| 47 | +### Basic |
| 48 | + |
| 49 | +```razor |
| 50 | +<TextDiff OldText="@oldText" NewText="@newText" /> |
40 | 51 | ``` |
41 | 52 |
|
42 | | -You'll also need the DiffPlex library: |
| 53 | +### With Options |
43 | 54 |
|
44 | | -```bash |
45 | | -dotnet add package DiffPlex |
| 55 | +```razor |
| 56 | +<TextDiff OldText="@oldText" |
| 57 | + NewText="@newText" |
| 58 | + CollapseContent="true" |
| 59 | + IgnoreCase="true" |
| 60 | + IgnoreWhiteSpace="false" |
| 61 | + Class="my-diff"> |
| 62 | + <Header> |
| 63 | + <div style="padding: 10px 12px;"> |
| 64 | + <span class="diff-stats-badge warning">@context.LineModificationCount modified</span> |
| 65 | + <span class="diff-stats-badge danger">@context.LineDeletionCount deleted</span> |
| 66 | + <span class="diff-stats-badge success">@context.LineAdditionCount added</span> |
| 67 | + </div> |
| 68 | + </Header> |
| 69 | +</TextDiff> |
46 | 70 | ``` |
47 | 71 |
|
48 | | -## ⚙️ Setup |
| 72 | +## Parameters |
| 73 | + |
| 74 | +| Parameter | Type | Default | Description | |
| 75 | +|---|---|---|---| |
| 76 | +| `OldText` | `string?` | `null` | Original text (left pane) | |
| 77 | +| `NewText` | `string?` | `null` | Modified text (right pane) | |
| 78 | +| `CollapseContent` | `bool` | `false` | Collapse unchanged sections | |
| 79 | +| `MaxHeight` | `int` | `300` | Max height (px) when collapsed | |
| 80 | +| `IgnoreCase` | `bool` | `false` | Ignore case differences | |
| 81 | +| `IgnoreWhiteSpace` | `bool` | `false` | Ignore whitespace differences | |
| 82 | +| `Header` | `RenderFragment<DiffStats>?` | `null` | Custom header template | |
| 83 | +| `Class` | `string?` | `null` | Additional CSS class(es) | |
| 84 | + |
| 85 | +Unmatched HTML attributes (`style`, `id`, `data-*`, etc.) are passed through to the root element. |
| 86 | + |
| 87 | +## How Character Highlighting Works |
| 88 | + |
| 89 | +The component uses three levels of visual hierarchy: |
| 90 | + |
| 91 | +1. **Line-level** — the entire row gets a colored background (`inserted-line`, `deleted-line`, `modified-line`) |
| 92 | +2. **Word-level** — when a word is partially changed, it gets a soft background highlight (`inserted-word`, `deleted-word`, `modified-word`) |
| 93 | +3. **Character-level** — the specific changed characters get a strong highlight (`inserted-character`, `deleted-character`, `modified-character`) |
| 94 | + |
| 95 | +For example, `Programing` → `Programming`: |
| 96 | +- The whole word wraps in `<span class="inserted-word">` (soft green background) |
| 97 | +- Only the added `m` wraps in `<span class="inserted-character">` (strong green highlight) |
49 | 98 |
|
50 | | -### 1. Configure Services |
| 99 | +When a word is entirely changed (e.g. `cat` → `dog`), it skips the word wrapper and uses the character-level class directly. |
51 | 100 |
|
52 | | -Add the required services to your `Program.cs`: |
| 101 | +Adjacent character highlights automatically merge into a single pill shape — rounded corners only appear on the first and last character in a run. |
53 | 102 |
|
54 | | -```csharp |
55 | | -// Program.cs |
56 | | -public static async Task Main(string[] args) |
57 | | -{ |
58 | | - var builder = WebAssemblyHostBuilder.CreateDefault(args); |
59 | | - |
60 | | - // Register BlazorTextDiff dependencies |
61 | | - builder.Services.AddScoped<ISideBySideDiffBuilder, SideBySideDiffBuilder>(); |
62 | | - builder.Services.AddScoped<IDiffer, Differ>(); |
| 103 | +## Customization |
63 | 104 |
|
64 | | - builder.RootComponents.Add<App>("app"); |
| 105 | +All visual styling is controlled via CSS custom properties. Override them in your own stylesheet to retheme the component. |
65 | 106 |
|
66 | | - await builder.Build().RunAsync(); |
| 107 | +### Diff Colors |
| 108 | + |
| 109 | +```css |
| 110 | +:root { |
| 111 | + /* Line-level backgrounds */ |
| 112 | + --diff-addition-bg: #e6ffed; |
| 113 | + --diff-deletion-bg: #ffeef0; |
| 114 | + --diff-modification-bg: #fff8c5; |
| 115 | + |
| 116 | + /* Line-level left border accents */ |
| 117 | + --diff-addition-border: #2ea043; |
| 118 | + --diff-deletion-border: #f85149; |
| 119 | + --diff-modification-border: #fb8500; |
| 120 | + |
| 121 | + /* Character-level strong highlights */ |
| 122 | + --diff-addition-highlight: #7ce89b; |
| 123 | + --diff-deletion-highlight: #f9a8b0; |
| 124 | + --diff-modification-highlight: #ffc833; |
67 | 125 | } |
68 | 126 | ``` |
69 | 127 |
|
70 | | -### 2. Include Styles and Scripts |
| 128 | +The word-level soft background reuses `--diff-*-bg` (same as the line), and the character-level strong highlight uses `--diff-*-highlight`. |
71 | 129 |
|
72 | | -Add to your `index.html` or `_Host.cshtml`: |
| 130 | +### Highlight Shape |
73 | 131 |
|
74 | | -```html |
75 | | -<!-- Required CSS --> |
76 | | -<link href="_content/BlazorTextDiff/css/BlazorDiff.css" rel="stylesheet" /> |
77 | | - |
78 | | -<!-- Required JavaScript --> |
79 | | -<script src="_content/BlazorTextDiff/js/BlazorTextDiff.js"></script> |
| 132 | +```css |
| 133 | +:root { |
| 134 | + /* Character highlight pill shape */ |
| 135 | + --diff-char-radius: 3px; /* border-radius for each character span */ |
| 136 | + --diff-char-padding: 1px 2px; /* padding inside each character span */ |
| 137 | + |
| 138 | + /* Word highlight shape */ |
| 139 | + --diff-word-radius: 3px; /* border-radius for the word wrapper */ |
| 140 | + --diff-word-padding: 1px 0; /* padding inside the word wrapper */ |
| 141 | +} |
80 | 142 | ``` |
81 | 143 |
|
82 | | -## 🎯 Usage |
| 144 | +Examples: |
83 | 145 |
|
84 | | -### Basic Comparison |
| 146 | +```css |
| 147 | +/* Sharp rectangles instead of rounded pills */ |
| 148 | +:root { --diff-char-radius: 0; --diff-word-radius: 0; } |
85 | 149 |
|
86 | | -```html |
87 | | -<TextDiff OldText="@oldText" NewText="@newText" /> |
| 150 | +/* Larger, more prominent pills */ |
| 151 | +:root { --diff-char-radius: 6px; --diff-char-padding: 2px 4px; } |
| 152 | + |
| 153 | +/* Underline style (no background, border-bottom instead) */ |
| 154 | +.my-diff .inserted-character { background: none; border-bottom: 2px solid #2ea043; } |
| 155 | +.my-diff .deleted-character { background: none; border-bottom: 2px solid #f85149; text-decoration: line-through; } |
88 | 156 | ``` |
89 | 157 |
|
90 | | -### Advanced Features |
| 158 | +### Scoped Overrides |
91 | 159 |
|
92 | | -```html |
93 | | -<TextDiff |
94 | | - OldText="@oldText" |
95 | | - NewText="@newText" |
96 | | - CollapseContent="true" |
97 | | - ShowWhiteSpace="true" |
98 | | - IgnoreCase="true" |
99 | | - IgnoreWhiteSpace="false"> |
100 | | - |
101 | | - <Header> |
102 | | - <div class="diff-stats"> |
103 | | - <span class="badge bg-success">+@context.LineAdditionCount</span> |
104 | | - <span class="badge bg-warning">~@context.LineModificationCount</span> |
105 | | - <span class="badge bg-danger">-@context.LineDeletionCount</span> |
106 | | - </div> |
107 | | - </Header> |
108 | | -</TextDiff> |
| 160 | +Use the `Class` parameter to scope styles to a specific instance: |
| 161 | + |
| 162 | +```css |
| 163 | +.my-diff .modified-line { background-color: #ffe0b2; } |
| 164 | +.my-diff .deleted-character { background-color: #e53935; color: #fff; } |
109 | 165 | ``` |
110 | 166 |
|
111 | | -### Async Processing |
| 167 | +### CSS Classes Reference |
112 | 168 |
|
113 | | -For large texts, use async processing: |
| 169 | +**Layout:** |
| 170 | +`diff-container`, `diff-pane-left`, `diff-pane-right`, `diff-header`, `diff-panes`, `diff-expand-notice` |
114 | 171 |
|
115 | | -```csharp |
116 | | -@code { |
117 | | - private string oldText = ""; |
118 | | - private string newText = ""; |
119 | | - private bool isProcessing = false; |
| 172 | +**Line-level (on `<td>`):** |
| 173 | +`inserted-line`, `deleted-line`, `modified-line`, `unchanged-line` |
120 | 174 |
|
121 | | - private async Task ProcessLargeDiff() |
122 | | - { |
123 | | - isProcessing = true; |
124 | | - // Your async logic here |
125 | | - await Task.Delay(100); // Simulate processing |
126 | | - isProcessing = false; |
127 | | - } |
128 | | -} |
129 | | -``` |
| 175 | +**Word-level (on `<span>` wrapping a partially changed word):** |
| 176 | +`inserted-word`, `deleted-word`, `modified-word` |
130 | 177 |
|
131 | | -## 🔧 Component Parameters |
| 178 | +**Character-level (on `<span>` wrapping specific changed characters):** |
| 179 | +`inserted-character`, `deleted-character`, `modified-character` |
132 | 180 |
|
133 | | -| Parameter | Type | Default | Description | |
134 | | -|-----------|------|---------|-------------| |
135 | | -| `OldText` | `string` | `""` | The original text (left side) | |
136 | | -| `NewText` | `string` | `""` | The modified text (right side) | |
137 | | -| `CollapseContent` | `bool` | `false` | Collapse large diff sections | |
138 | | -| `ShowWhiteSpace` | `bool` | `false` | Visualize spaces and tabs | |
139 | | -| `IgnoreCase` | `bool` | `false` | Ignore case differences | |
140 | | -| `IgnoreWhiteSpace` | `bool` | `false` | Ignore whitespace differences | |
| 181 | +**Stats badges:** |
| 182 | +`diff-stats-badge`, `primary`, `success`, `danger`, `warning`, `info` |
141 | 183 |
|
142 | | -## 🎨 Customization |
| 184 | +### All CSS Custom Properties |
143 | 185 |
|
144 | | -The component uses CSS classes that you can override: |
| 186 | +| Property | Default | Description | |
| 187 | +|---|---|---| |
| 188 | +| `--diff-bg-primary` | `#ffffff` | Main background | |
| 189 | +| `--diff-bg-secondary` | `#f6f8fa` | Header/footer background | |
| 190 | +| `--diff-bg-tertiary` | `#f1f3f4` | Hover background | |
| 191 | +| `--diff-border-primary` | `#e1e4e8` | Border color | |
| 192 | +| `--diff-text-primary` | `#24292e` | Main text color | |
| 193 | +| `--diff-text-muted` | `#656d76` | Line number color | |
| 194 | +| `--diff-text-accent` | `#0969da` | Accent/focus color | |
| 195 | +| `--diff-addition-bg` | `#e6ffed` | Added line & word background | |
| 196 | +| `--diff-addition-border` | `#2ea043` | Added line left border | |
| 197 | +| `--diff-addition-highlight` | `#7ce89b` | Added character highlight | |
| 198 | +| `--diff-deletion-bg` | `#ffeef0` | Deleted line & word background | |
| 199 | +| `--diff-deletion-border` | `#f85149` | Deleted line left border | |
| 200 | +| `--diff-deletion-highlight` | `#f9a8b0` | Deleted character highlight | |
| 201 | +| `--diff-modification-bg` | `#fff8c5` | Modified line & word background | |
| 202 | +| `--diff-modification-border` | `#fb8500` | Modified line left border | |
| 203 | +| `--diff-modification-highlight` | `#ffc833` | Modified character highlight | |
| 204 | +| `--diff-char-radius` | `3px` | Character highlight border-radius | |
| 205 | +| `--diff-char-padding` | `1px 2px` | Character highlight padding | |
| 206 | +| `--diff-word-radius` | `3px` | Word highlight border-radius | |
| 207 | +| `--diff-word-padding` | `1px 0` | Word highlight padding | |
| 208 | +| `--diff-shadow` | `0 1px 3px ...` | Container shadow | |
| 209 | +| `--diff-shadow-hover` | `0 2px 6px ...` | Container shadow on hover | |
145 | 210 |
|
146 | | -```css |
147 | | -.diff-container { /* Main container */ } |
148 | | -.diff-line-added { /* Added lines */ } |
149 | | -.diff-line-deleted { /* Deleted lines */ } |
150 | | -.diff-line-modified { /* Modified lines */ } |
151 | | -.diff-line-unchanged { /* Unchanged lines */ } |
152 | | -``` |
| 211 | +Dark mode overrides are built in via `@media (prefers-color-scheme: dark)`. |
| 212 | + |
| 213 | +## AI-Assisted Development |
153 | 214 |
|
154 | | -## 📄 License |
| 215 | +This project uses AI as a development tool to help improve and maintain the library. AI assists with tasks such as code refactoring, writing tests, updating documentation, and implementing new features. All changes are generally reviewed by a human before being merged, but due to limited contributor availability and a lack of pull requests, AI is used to keep the project moving forward and ensure it stays up to date. |
155 | 216 |
|
156 | | -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. |
| 217 | +If you spot anything that looks off or have suggestions, contributions and issues are always welcome. |
157 | 218 |
|
158 | | -## 🙏 Acknowledgments |
| 219 | +## License |
159 | 220 |
|
160 | | -- [DiffPlex](https://github.com/mmanela/diffplex) - The core diffing library |
161 | | -- [Blazor](https://blazor.net/) - The web framework that makes this possible |
| 221 | +MIT — see [LICENSE](LICENSE). |
162 | 222 |
|
163 | | ---- |
| 223 | +## Acknowledgments |
164 | 224 |
|
165 | | -<div align="center"> |
166 | | - Made with ❤️ for the Blazor community |
167 | | -</div> |
| 225 | +- [DiffPlex](https://github.com/mmanela/diffplex) — core diffing engine |
| 226 | +- [Blazor](https://blazor.net/) — web framework |
0 commit comments