Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion docs/sdd/sdd-012-soubory.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,22 @@ 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, a blok
jiného typu jsou fialové s čárkovaným rámečkem. Blok, kterému se změnil typ nebo formát, nese za
textem štítek, který změny pojmenuje („nadpis 2 → odstavec; nově tučně“, „adresa odkazu A → B“);
změnu formátu pojmenuje i tooltip nad textem. 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 bloky“): 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í.
Expand Down
201 changes: 201 additions & 0 deletions src/App/EvilCase.App/Components/MarkdownCompare.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
@using Microsoft.JSInterop
@implements IAsyncDisposable
@inject ITextCompares Compares
@inject ILogger<MarkdownCompare> 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. *@
<div class="@(this.Unified ? "ec-preview-view ec-text-compare-view ec-text-compare-view-unified" : "ec-preview-view ec-text-compare-view")">
<div class="ec-preview-toolbar ec-text-compare-toolbar" role="toolbar" aria-label="Ovládání porovnání">
<div class="ec-segmented ec-segmented-compact ec-text-compare-layout" role="group" aria-label="Rozložení">
<button type="button" class="ec-segment" aria-pressed="@Pressed(!this.Unified)" @onclick="() => this.UnifiedChanged.InvokeAsync(false)">Vedle sebe</button>
<button type="button" class="ec-segment" aria-pressed="@Pressed(this.Unified)" @onclick="() => this.UnifiedChanged.InvokeAsync(true)">V jednom sloupci</button>
</div>
<span class="ec-preview-toolbar-separator ec-text-compare-layout" aria-hidden="true"></span>
@this.Modes
<span class="ec-preview-toolbar-separator" aria-hidden="true"></span>
<div class="ec-text-compare-changes" role="group" aria-label="Změny">
<EcButton Variant="EcButtonVariant.Secondary" Size="EcButtonSize.Compact" Icon="@AppIcons.ArrowUp" AriaLabel="Předchozí změna"
Disabled="@(this.view.Changes == 0)" OnClick="() => this.Step(previous: true)"><span class="ec-preview-toolbar-label">Předchozí změna</span></EcButton>
<EcButton Variant="EcButtonVariant.Secondary" Size="EcButtonSize.Compact" Icon="@AppIcons.ArrowDown" AriaLabel="Další změna"
Disabled="@(this.view.Changes == 0)" OnClick="() => this.Step(previous: false)"><span class="ec-preview-toolbar-label">Další změna</span></EcButton>
@if (this.ready)
{
<span class="ec-text-compare-summary"><span class="ec-text-compare-summary-added">+@this.view.Added</span> <span class="ec-text-compare-summary-removed">−@this.view.Removed</span><span class="ec-text-compare-summary-unit">@(" " + BlocksWord(this.view.Removed))</span></span>
}
</div>
<ToolbarSearch @ref="this.search" Label="Hledat v dokumentu" Disabled="@(!this.ready)" Collapsible OnSearch="this.Search" />
</div>
<div class="ec-preview-stage ec-text-editor-stage ec-text-compare-stage">
<div class="ec-text-compare-card">
@this.Versions
<div @ref="this.host" class="ec-markdown-compare"></div>
<div class="ec-markdown-compare-legend" role="note" aria-label="Legenda">
<span class="ec-markdown-compare-legend-item"><ins class="ec-markdown-compare-inserted">text</ins>vloženo</span>
<span class="ec-markdown-compare-legend-item"><del class="ec-markdown-compare-deleted">text</del>smazáno</span>
<span class="ec-markdown-compare-legend-item"><span class="ec-markdown-compare-format">text</span>změna formátu</span>
</div>
</div>
</div>
</div>

@code {
private readonly CancellationTokenSource cancel = new();

private ElementReference host;

private DotNetObjectReference<MarkdownCompare>? self;

private ToolbarSearch? search;

private bool ready;

private bool opened;

private CompareView view = new(Formatted: true, Formattable: true, Added: 0, Removed: 0, Changes: 0);

/// <summary>
/// The older text, the left one side by side.
/// </summary>
[Parameter, EditorRequired]
public required string Older { get; set; }

[Parameter, EditorRequired]
public required string Newer { get; set; }

/// <summary>
/// The names of the older, the newer and the one-column text for assistive technology. Fixed for the compare's life.
/// </summary>
[Parameter, EditorRequired]
public required IReadOnlyList<string> Labels { get; set; }

/// <summary>
/// Whether the texts show in one column; a narrow screen always shows them so.
/// </summary>
[Parameter]
public bool Unified { get; set; }

[Parameter]
public EventCallback<bool> UnifiedChanged { get; set; }

/// <summary>
/// The switch between the formatted texts and their source, after the layout.
/// </summary>
[Parameter]
public RenderFragment? Modes { get; set; }

/// <summary>
/// What stands above the texts, such as the choice of the versions.
/// </summary>
[Parameter]
public RenderFragment? Versions { get; set; }

/// <summary>
/// Raised when the texts formatted do not differ.
/// </summary>
[Parameter]
public EventCallback OnSame { get; set; }

/// <summary>
/// Raised when the compare's script did not start.
/// </summary>
[Parameter]
public EventCallback OnUnavailable { get; set; }

[JSInvokable]
public void NotifyMatches(int current, int total)
{
this.search?.MatchesChanged(current, total, pending: false);
}

/// <summary>
/// Ctrl+F or Cmd+F in a text.
/// </summary>
[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ů",
};
}
}
15 changes: 13 additions & 2 deletions src/App/EvilCase.App/Components/TextCompare.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
@inject ITextCompares Compares
@inject ILogger<TextCompare> 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. *@
<div class="@(this.Unified ? "ec-preview-view ec-text-compare-view ec-text-compare-view-unified" : "ec-preview-view ec-text-compare-view")">
<div class="ec-preview-toolbar ec-text-compare-toolbar" role="toolbar" aria-label="Ovládání porovnání">
<div class="ec-segmented ec-segmented-compact ec-text-compare-layout" role="group" aria-label="Rozložení">
<button type="button" class="ec-segment" aria-pressed="@Pressed(!this.Unified)" @onclick="() => this.UnifiedChanged.InvokeAsync(false)">Vedle sebe</button>
<button type="button" class="ec-segment" aria-pressed="@Pressed(this.Unified)" @onclick="() => this.UnifiedChanged.InvokeAsync(true)">V jednom sloupci</button>
</div>
@if (this.Modes is not null)
{
<span class="ec-preview-toolbar-separator ec-text-compare-layout" aria-hidden="true"></span>
@this.Modes
}
@if (this.Formats)
{
<span class="ec-preview-toolbar-separator ec-text-compare-layout" aria-hidden="true"></span>
Expand Down Expand Up @@ -106,6 +111,12 @@
[Parameter]
public EventCallback<bool> UnifiedChanged { get; set; }

/// <summary>
/// A switch between views of the texts, after the layout.
/// </summary>
[Parameter]
public RenderFragment? Modes { get; set; }

/// <summary>
/// What stands above the texts, such as the choice of the versions.
/// </summary>
Expand Down
46 changes: 40 additions & 6 deletions src/App/EvilCase.App/Components/VersionCompare.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
@implements IDisposable
@inject ILogger<VersionCompare> Logger

@* The compare of two versions of a text, code or Markdown file, the older on the left. A side over 1 MB or not in UTF-8
says why it does not compare; versions equal but for their line breaks say only that they are equal. *@
@* The compare of two versions of a text, code or Markdown file, the older on the left; Markdown formatted or as its
source. A side over 1 MB or not in UTF-8 says why it does not compare; versions equal but for their line breaks say
only that they are equal. *@
@if (this.Versions is null || this.Item(this.Older) is not { } older || this.Item(this.Newer) is not { } newer)
{
<div class="ec-preview-stage ec-text-compare-stage">
Expand All @@ -31,10 +32,21 @@ else if (this.Texts(older, newer) is ({ } olderText, { } newerText) && !this.una
_ => (CodeLanguage?)null,
};

<TextCompare @key="(this.Older, this.Newer, this.round)" Older="@olderText" Newer="@newerText" Language="language" Formats="preview == FilePreviewKind.Code"
Labels="@([$"Verze {this.Older} souboru {this.File.FileName}", $"Verze {this.Newer} souboru {this.File.FileName}", $"Rozdíly mezi verzí {this.Older} a verzí {this.Newer}"])"
Unified="this.unified" UnifiedChanged="unified => this.unified = unified" Versions="this.Pickers(older, newer)"
OnUnavailable="() => this.unavailable = true" />
IReadOnlyList<string> labels = [$"Verze {this.Older} souboru {this.File.FileName}", $"Verze {this.Newer} souboru {this.File.FileName}", $"Rozdíly mezi verzí {this.Older} a verzí {this.Newer}"];

if (preview == FilePreviewKind.Markdown && !this.ShowsSource(older, newer))
{
<MarkdownCompare @key="(this.Older, this.Newer, this.round)" Older="@olderText" Newer="@newerText" Labels="labels"
Unified="this.unified" UnifiedChanged="unified => this.unified = unified" Modes="this.MarkdownModes(source: false)"
Versions="this.Pickers(older, newer)" OnSame="() => this.SameFormatted(older, newer)" OnUnavailable="() => this.unavailable = true" />
}
else
{
<TextCompare @key="(this.Older, this.Newer, this.round)" Older="@olderText" Newer="@newerText" Language="language" Formats="preview == FilePreviewKind.Code"
Labels="labels" Unified="this.unified" UnifiedChanged="unified => this.unified = unified"
Modes="@(preview == FilePreviewKind.Markdown ? this.MarkdownModes(source: true) : null)" Versions="this.Pickers(older, newer)"
OnUnavailable="() => this.unavailable = true" />
}
}
else
{
Expand Down Expand Up @@ -81,6 +93,10 @@ else
// The versions whose last read failed.
private readonly HashSet<int> failed = [];

// The pairs of versions, by number and upload, whose Markdown texts formatted do not differ; they compare their source
// until a choice says otherwise.
private readonly HashSet<(int, DateTime, int, DateTime)> sameFormatted = [];

private CancellationTokenSource? loadToken;

private (int, int)? loadedPair;
Expand All @@ -89,6 +105,9 @@ else

private bool unified;

// Whether a Markdown file compares its source; null until chosen.
private bool? source;

// The compare's script did not start; a retry opens it anew.
private bool unavailable;

Expand Down Expand Up @@ -225,6 +244,21 @@ else
this.round++;
}

private bool ShowsSource(FileVersionItem older, FileVersionItem newer)
{
return this.source ?? this.sameFormatted.Contains((older.Number, older.Created, newer.Number, newer.Created));
}

private void SameFormatted(FileVersionItem older, FileVersionItem newer)
{
this.sameFormatted.Add((older.Number, older.Created, newer.Number, newer.Created));
}

private RenderFragment MarkdownModes(bool source)
{
return @<MarkdownModeSwitch Editing="false" Source="source" OnSwitch="shown => this.source = shown" />;
}

private RenderFragment Pickers(FileVersionItem older, FileVersionItem newer)
{
return @<div class="ec-text-compare-versions" role="group" aria-label="Porovnávané verze">
Expand Down
12 changes: 10 additions & 2 deletions src/App/EvilCase.App/Files/ITextCompares.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
namespace EvilBrains.EvilCase.App.Files;

/// <summary>
/// The read-only compares of two texts, run by the text editor's library, which the browser loads when the first one
/// opens.
/// The read-only compares of two texts, run by the text editor's library and for a formatted Markdown text by the
/// Markdown library besides, which the browser loads when the first one opens.
/// </summary>
internal interface ITextCompares
{
Expand All @@ -20,6 +20,14 @@ internal interface ITextCompares
/// </summary>
public Task<CompareView> OpenCompare(ElementReference host, string older, string newer, CodeLanguage? language, bool format, IReadOnlyList<string> labels, DotNetObjectReference<TextCompare> owner);

/// <summary>
/// Opens the compare of the Markdown texts <paramref name="older"/> and <paramref name="newer"/> formatted in
/// <paramref name="host"/>; its changes count blocks. <paramref name="labels"/> names the older, the newer and the
/// one-column text; <paramref name="owner"/> hears of the search matches and of Ctrl+F. A library that does not load
/// throws a <c>JSException</c>.
/// </summary>
public Task<CompareView> OpenMarkdownCompare(ElementReference host, string older, string newer, IReadOnlyList<string> labels, DotNetObjectReference<MarkdownCompare> owner);

/// <summary>
/// Compares the formatted or the original texts; an open search runs again. Answers the changes of the texts shown.
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/App/EvilCase.App/Files/TextCompares.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public async Task<CompareView> OpenCompare(ElementReference host, string older,
return await compares.InvokeAsync<CompareView>("openCompare", host, older, newer, language is { } value ? FilePreviews.LanguageValue(value) : null, format, labels, owner);
}

public async Task<CompareView> OpenMarkdownCompare(ElementReference host, string older, string newer, IReadOnlyList<string> labels, DotNetObjectReference<MarkdownCompare> owner)
{
var compares = await this.Module();

// Never cancelled: a compare the browser made must reach the caller to be closed.
return await compares.InvokeAsync<CompareView>("openMarkdownCompare", host, older, newer, labels, owner);
}

public async Task<CompareView> ShowCompareFormatted(ElementReference host, bool formatted)
{
var compares = await this.Module();
Expand Down
Loading
Loading