From 0462e409f242c8ac9bd15fc985819bdcffdc901b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 20:53:27 +0000 Subject: [PATCH 1/4] Compare Markdown versions formatted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Markdown compare opens formatted in read-only Milkdown editors, side by side with facing blocks level or in one column with the removed content in place. Blocks align on the exact diff of equal blocks first, then by shared text; text changes come from presentableDiff of @codemirror/merge, which the CodeMirror bundle exports as diffTexts. Tables compare by cell, rows known by their first cell. A change of marks or block type is named in a tooltip or a chip. The Formátovaně / Zdrojový kód switch opens part 1's source compare, which versions equal formatted show on their own. Part of #671. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UovdgewekkrSiyay5dgc3v --- docs/sdd/sdd-012-soubory.md | 16 +- .../Components/MarkdownCompare.razor | 201 +++++++ .../EvilCase.App/Components/TextCompare.razor | 15 +- .../Components/VersionCompare.razor | 46 +- src/App/EvilCase.App/Files/ITextCompares.cs | 12 +- src/App/EvilCase.App/Files/TextCompares.cs | 8 + src/App/EvilCase.App/build-libs.mjs | 525 ++++++++++++++---- .../wwwroot/css/ec-components.css | 166 ++++++ .../EvilCase.App/wwwroot/css/ec-tokens.css | 17 + .../EvilCase.App/wwwroot/js/markdown-diff.js | 350 ++++++++++++ .../wwwroot/js/markdown-editor.js | 2 +- .../EvilCase.App/wwwroot/js/text-compare.js | 168 ++++-- .../FilePreviewModalCompareRenderTests.cs | 36 ++ .../Frontend/MarkdownDiffLibraryTests.cs | 243 ++++++++ .../EvilCase.Tests/Frontend/ServedLibrary.cs | 27 +- .../Frontend/StubTextCompares.cs | 18 + 16 files changed, 1654 insertions(+), 196 deletions(-) create mode 100644 src/App/EvilCase.App/Components/MarkdownCompare.razor create mode 100644 src/App/EvilCase.App/wwwroot/js/markdown-diff.js create mode 100644 src/Tests/EvilCase.Tests/Frontend/MarkdownDiffLibraryTests.cs diff --git a/docs/sdd/sdd-012-soubory.md b/docs/sdd/sdd-012-soubory.md index 4da7ac0a..4fd08bba 100644 --- a/docs/sdd/sdd-012-soubory.md +++ b/docs/sdd/sdd-012-soubory.md @@ -327,7 +327,21 @@ Esc a Zrušit ho zahodí. Jméno jiného souboru odmítne hláškou „Soubor to (na mobilu Formát), Předchozí změna a Další změna, které přecházejí dokola, souhrn „+12 −5 řádků“ a hledání. Kód se porovnává naformátovaný; když některou z verzí naformátovat nejde, porovná se původní text, přepínač je neaktivní a lišta to řekne. Verze, které se naformátované - neliší, se porovnají v původním textu. Markdown se porovnává ve zdrojovém kódu. + neliší, se porovnají v původním textu. +- Markdown se porovnává formátovaně. Přepínač Formátovaně / Zdrojový kód (na mobilu Formát / + Zdroj) za přepínačem sloupců přejde na porovnání zdrojového kódu, které je stejné jako u textu; + volba platí, dokud se porovnání nezavře. Verze, které se formátovaně neliší, se porovnají ve + zdrojovém kódu. +- Formátované porovnání ukáže obě verze jako náhled Markdownu, které nejdou měnit. Vložený text je + zelený a podtržený, smazaný červený a přeškrtnutý. Text, kterému se změnil jen formát, je + fialový s čárkovaným rámečkem a tooltip změnu pojmenuje („nově tučně“, „adresa odkazu A → B“); + blok jiného typu nese štítek se změnou („nadpis 2 → odstavec“). Přidaný blok je celý zelený, + odebraný celý červený. Tabulka se porovná po buňkách; řádky se poznají podle první buňky, když + se první buňky v každé z tabulek liší. Pod porovnáním stojí legenda barev. +- Vedle sebe stojí protější bloky na stejné výšce. V jednom sloupci stojí odebraný obsah tam, kde + byl, i odebrané řádky tabulky a položky seznamu. Souhrn počítá bloky („+3 −2 bloků“): změněný + blok na obou stranách, řádek tabulky jako blok. Nezměněné úseky se nesbalují. Obrázky se + nenačítají a HTML se nevykoná. - Hledání v porovnání nerozlišuje velikost písmen ani diakritiku a ukáže pořadí výskytu z počtu; výskyt ve sbaleném úseku úsek rozbalí. V jednom sloupci hledá jen v novější verzi. Porovnání nemá zvětšení. diff --git a/src/App/EvilCase.App/Components/MarkdownCompare.razor b/src/App/EvilCase.App/Components/MarkdownCompare.razor new file mode 100644 index 00000000..3314758b --- /dev/null +++ b/src/App/EvilCase.App/Components/MarkdownCompare.razor @@ -0,0 +1,201 @@ +@using Microsoft.JSInterop +@implements IAsyncDisposable +@inject ITextCompares Compares +@inject ILogger Logger + +@* The read-only compare of two Markdown texts formatted: side by side or in one column, below a toolbar with the layout, + the modes, the steps between the changes, their summary in blocks and the search, above a legend of its marks. + Versions stands above the texts. *@ +
+ +
+
+ @this.Versions +
+
+ textvloženo + textsmazáno + textzměna formátu +
+
+
+
+ +@code { + private readonly CancellationTokenSource cancel = new(); + + private ElementReference host; + + private DotNetObjectReference? self; + + private ToolbarSearch? search; + + private bool ready; + + private bool opened; + + private CompareView view = new(Formatted: true, Formattable: true, Added: 0, Removed: 0, Changes: 0); + + /// + /// The older text, the left one side by side. + /// + [Parameter, EditorRequired] + public required string Older { get; set; } + + [Parameter, EditorRequired] + public required string Newer { get; set; } + + /// + /// The names of the older, the newer and the one-column text for assistive technology. Fixed for the compare's life. + /// + [Parameter, EditorRequired] + public required IReadOnlyList Labels { get; set; } + + /// + /// Whether the texts show in one column; a narrow screen always shows them so. + /// + [Parameter] + public bool Unified { get; set; } + + [Parameter] + public EventCallback UnifiedChanged { get; set; } + + /// + /// The switch between the formatted texts and their source, after the layout. + /// + [Parameter] + public RenderFragment? Modes { get; set; } + + /// + /// What stands above the texts, such as the choice of the versions. + /// + [Parameter] + public RenderFragment? Versions { get; set; } + + /// + /// Raised when the texts formatted do not differ. + /// + [Parameter] + public EventCallback OnSame { get; set; } + + /// + /// Raised when the compare's script did not start. + /// + [Parameter] + public EventCallback OnUnavailable { get; set; } + + [JSInvokable] + public void NotifyMatches(int current, int total) + { + this.search?.MatchesChanged(current, total, pending: false); + } + + /// + /// Ctrl+F or Cmd+F in a text. + /// + [JSInvokable] + public async Task NotifyFind() + { + if (this.search is not null) + await this.search.Focus(); + } + + public async ValueTask DisposeAsync() + { + await this.cancel.CancelAsync(); + + if (this.opened) + { + try + { + await this.Compares.CloseCompare(this.host); + } + catch (JSException exception) + { + this.Logger.LogInformation(exception, "The compare was already gone"); + } + } + + this.self?.Dispose(); + this.cancel.Dispose(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + this.self = DotNetObjectReference.Create(this); + this.opened = true; + + try + { + var shown = await this.Compares.OpenMarkdownCompare(this.host, this.Older, this.Newer, this.Labels, this.self); + + if (this.cancel.IsCancellationRequested) + { + await this.Compares.CloseCompare(this.host); + + return; + } + + (this.view, this.ready) = (shown, true); + this.StateHasChanged(); + + if (shown.Changes == 0) + await this.OnSame.InvokeAsync(); + } + catch (JSException exception) + { + this.Logger.LogWarning(exception, "The Markdown compare did not start"); + + if (!this.cancel.IsCancellationRequested) + await this.OnUnavailable.InvokeAsync(); + } + } + + private async Task Step(bool previous) + { + await this.Compares.StepCompareChange(this.host, previous); + } + + private async Task Search(PreviewSearch search) + { + await this.Compares.FindInCompare(this.host, search.Query, search.Find); + } + + private static string Pressed(bool value) + { + return value ? "true" : "false"; + } + + // 1 blok, 2 až 4 bloky, 0 a 5 a více bloků. + private static string BlocksWord(int count) + { + return count switch + { + 1 => "blok", + >= 2 and <= 4 => "bloky", + _ => "bloků", + }; + } +} diff --git a/src/App/EvilCase.App/Components/TextCompare.razor b/src/App/EvilCase.App/Components/TextCompare.razor index 35a3cca3..1e4dd8a2 100644 --- a/src/App/EvilCase.App/Components/TextCompare.razor +++ b/src/App/EvilCase.App/Components/TextCompare.razor @@ -3,14 +3,19 @@ @inject ITextCompares Compares @inject ILogger Logger -@* The read-only compare of two texts: side by side or in one column, below a toolbar with the layout, the formatting, - the steps between the changes, their summary and the search. Versions stands above the texts. *@ +@* The read-only compare of two texts: side by side or in one column, below a toolbar with the layout, the modes or the + formatting, the steps between the changes, their summary and the search. Versions stands above the texts. *@