Skip to content
Closed
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
44 changes: 44 additions & 0 deletions Source/Layout/Markdown4D.Editor.Model.pas
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ TMarkdownEditorModel = class
function CollapsedIndexOf(const HeaderOffset: Integer): Integer;
function TryRegionAtHeader(const HeaderLine: Integer; out Region: TFoldRegion): Boolean;
procedure WrapOrToggle(const Marker: string);
function MarkersSurroundSelection(const Start, Len: Integer; const Marker: string): Boolean;
procedure InsertLink;
procedure WrapCodeBlock;
procedure ToggleHeading(const Level: Integer);
Expand Down Expand Up @@ -1313,12 +1314,55 @@ procedure TMarkdownEditorModel.WrapOrToggle(const Marker: string);
Exit;
end;

// Wrapping leaves the selection on the word itself, inside the markers it just
// added, so pressing the same command again has to recognise them from there.
// Without this a second press wraps the wrapped text and doubles the markers.
if MarkersSurroundSelection(Start, Len, Marker) then
begin
ApplyReplace(Start - MarkerLen, Len + 2 * MarkerLen, Selected, False);
FAnchor := Start - MarkerLen;
FCaret := FAnchor + System.Length(Selected);
Exit;
end;

const Wrapped = Marker + Selected + Marker;
ApplyReplace(Start, Len, Wrapped, False);
FAnchor := Start + MarkerLen;
FCaret := Start + MarkerLen + System.Length(Selected);
end;

function TMarkdownEditorModel.MarkersSurroundSelection(const Start, Len: Integer;
const Marker: string): Boolean;
begin
Result := False;

const MarkerLen = System.Length(Marker);
const LeadingStart = Start - MarkerLen + 1;
const TrailingStart = Start + Len + 1;

if (LeadingStart < 1) or (TrailingStart + MarkerLen - 1 > System.Length(FText)) then
Exit;

if Copy(FText, LeadingStart, MarkerLen) <> Marker then
Exit;
if Copy(FText, TrailingStart, MarkerLen) <> Marker then
Exit;

// A marker that continues into more of the same character belongs to a longer
// run, and so to a different construct: the single asterisks either side of a
// word inside '**bold**' are the bold, not an italic to be taken away.
const MarkerChar = Marker[1];

if (LeadingStart > 1) and (FText[LeadingStart - 1] = MarkerChar) then
Exit;

const AfterTrailing = TrailingStart + MarkerLen;
if (AfterTrailing <= System.Length(FText)) and (FText[AfterTrailing] = MarkerChar) then
Exit;

Result := True;
end;

procedure TMarkdownEditorModel.InsertLink;
begin
const Start = SelectionStart;
Expand Down
46 changes: 46 additions & 0 deletions Tests/Markdown4D.Editor.Model.Tests.pas
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ TMarkdownEditorModelTests = class
[TestCase('On bold selection unwraps', '**Hello** world,9,Hello world')]
procedure ExecuteBold_Various_TogglesBoldMarkers(const Text: string; const SelectionEnd: Integer; const Expected: string);

[Test]
procedure ExecuteBold_Twice_LeavesTheWordUnchanged;

[Test]
procedure ExecuteBold_MarkersJustOutsideSelection_RemovesThem;

[Test]
procedure ExecuteItalic_OnABoldWord_AddsItalicRatherThanEatingTheBold;

[Test]
procedure ExecuteLink_InsertsPlaceholderWithCaretInUrl;

Expand Down Expand Up @@ -764,6 +773,43 @@ procedure TMarkdownEditorModelTests.ExecuteBold_Various_TogglesBoldMarkers(const
Assert.AreEqual(Expected, FModel.Text);
end;

procedure TMarkdownEditorModelTests.ExecuteBold_Twice_LeavesTheWordUnchanged;
begin
// What a user actually does: select a word, press Bold, press Bold again. The
// first press leaves the selection on the word itself, inside the markers it
// just added, so the second press has to recognise them from there.
FModel.LoadText('Unidentified');
FModel.SetSelection(0, 12);

FModel.ExecuteCommand(TEditorCommand.Bold);
Assert.AreEqual('**Unidentified**', FModel.Text, 'after the first press');

FModel.ExecuteCommand(TEditorCommand.Bold);
Assert.AreEqual('Unidentified', FModel.Text, 'after the second press');
end;

procedure TMarkdownEditorModelTests.ExecuteBold_MarkersJustOutsideSelection_RemovesThem;
begin
FModel.LoadText('a **word** b');
FModel.SetSelection(4, 4);

FModel.ExecuteCommand(TEditorCommand.Bold);

Assert.AreEqual('a word b', FModel.Text);
end;

procedure TMarkdownEditorModelTests.ExecuteItalic_OnABoldWord_AddsItalicRatherThanEatingTheBold;
begin
// The single asterisks around the selection belong to a bold run, not to an
// italic one, so italic has to add its own rather than strip half the bold.
FModel.LoadText('**word**');
FModel.SetSelection(2, 4);

FModel.ExecuteCommand(TEditorCommand.Italic);

Assert.AreEqual('***word***', FModel.Text);
end;

procedure TMarkdownEditorModelTests.ExecuteLink_InsertsPlaceholderWithCaretInUrl;
begin
FModel.LoadText('Hello');
Expand Down