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
9 changes: 9 additions & 0 deletions src/SharpFM.Model/ClipTypes/IClipTypeStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,13 @@ public interface IClipTypeStrategy
/// "new clip" flows in the host and by plugins.
/// </summary>
string DefaultXml(string clipName);

/// <summary>
/// Pull a clip-display-name hint out of <paramref name="xml"/> when the
/// wire format carries one (e.g. <c>&lt;Script name="…"&gt;</c> for
/// <c>Mac-XMSC</c>, <c>&lt;BaseTable name="…"&gt;</c> for <c>Mac-XMTB</c>).
/// Returns <c>null</c> for formats with no embedded name or when the
/// element is absent. Must not throw on malformed input.
/// </summary>
string? TryGetSourceName(string xml);
}
2 changes: 2 additions & 0 deletions src/SharpFM.Model/ClipTypes/LayoutClipStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ public ClipParseResult Parse(string xml)

public string DefaultXml(string clipName) =>
"<fmxmlsnippet type=\"FMObjectList\"></fmxmlsnippet>";

public string? TryGetSourceName(string xml) => null;
}
2 changes: 2 additions & 0 deletions src/SharpFM.Model/ClipTypes/OpaqueClipStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,6 @@ public ClipParseResult Parse(string xml)

public string DefaultXml(string clipName) =>
"<fmxmlsnippet type=\"FMObjectList\"></fmxmlsnippet>";

public string? TryGetSourceName(string xml) => null;
}
16 changes: 16 additions & 0 deletions src/SharpFM.Model/ClipTypes/ScriptClipStrategy.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using SharpFM.Model.Parsing;
using SharpFM.Model.Scripting;

Expand Down Expand Up @@ -63,4 +66,17 @@ public ClipParseResult Parse(string xml)

public string DefaultXml(string clipName) =>
"<fmxmlsnippet type=\"FMObjectList\"></fmxmlsnippet>";

public string? TryGetSourceName(string xml)
{
try
{
var name = XDocument.Parse(xml).Descendants("Script").FirstOrDefault()?.Attribute("name")?.Value;
return string.IsNullOrEmpty(name) ? null : name;
}
catch (XmlException)
{
return null;
}
}
}
16 changes: 16 additions & 0 deletions src/SharpFM.Model/ClipTypes/TableClipStrategy.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using SharpFM.Model.Parsing;
using SharpFM.Model.Schema;
using SharpFM.Model.Scripting;
Expand Down Expand Up @@ -66,4 +69,17 @@ public string DefaultXml(string clipName) =>
_wrapsBaseTable
? $"<fmxmlsnippet type=\"FMObjectList\"><BaseTable name=\"{XmlHelpers.XmlEscape(clipName)}\"></BaseTable></fmxmlsnippet>"
: "<fmxmlsnippet type=\"FMObjectList\"></fmxmlsnippet>";

public string? TryGetSourceName(string xml)
{
try
{
var name = XDocument.Parse(xml).Descendants("BaseTable").FirstOrDefault()?.Attribute("name")?.Value;
return string.IsNullOrEmpty(name) ? null : name;
}
catch (XmlException)
{
return null;
}
}
}
17 changes: 17 additions & 0 deletions src/SharpFM/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,19 @@ public async Task CopyAsClass()
}
}

// Pick a clip name that doesn't collide with anything already loaded.
// Returns the input when it's free, otherwise appends "(2)", "(3)", … until
// a free slot is found.
private string UniqueClipName(string desired)
{
if (FileMakerClips.All(c => c.Clip.Name != desired)) return desired;
for (var n = 2; ; n++)
{
var candidate = $"{desired} ({n})";
if (FileMakerClips.All(c => c.Clip.Name != candidate)) return candidate;
}
}

public async Task PasteFileMakerClipData()
{
try
Expand All @@ -355,6 +368,10 @@ public async Task PasteFileMakerClipData()
// don't add duplicates
if (FileMakerClips.Any(k => k.Clip.Xml == clip.Xml)) continue;

var sourceName = ClipTypeRegistry.For(format).TryGetSourceName(clip.Xml);
var desired = string.IsNullOrWhiteSpace(sourceName) ? "new-clip" : sourceName;
clip = clip.Rename(UniqueClipName(desired));

lastAdded = new ClipViewModel(clip);
FileMakerClips.Add(lastAdded);
count++;
Expand Down
9 changes: 9 additions & 0 deletions tests/SharpFM.Tests/ClipTypes/LayoutClipStrategyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ public void Identity_IsLayout()
Assert.Equal("Layout", LayoutClipStrategy.Instance.DisplayName);
}

[Fact]
public void TryGetSourceName_ReturnsNull()
{
var name = LayoutClipStrategy.Instance.TryGetSourceName(
"<fmxmlsnippet type=\"FMObjectList\"><Layout/></fmxmlsnippet>");

Assert.Null(name);
}

[Fact]
public void Parse_ValidLayoutSnippet_ReturnsSuccess()
{
Expand Down
8 changes: 8 additions & 0 deletions tests/SharpFM.Tests/ClipTypes/OpaqueClipStrategyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,12 @@ public void DefaultXml_ProducesParseableSnippet()
var result = OpaqueClipStrategy.Instance.Parse(seed);
Assert.IsType<ParseSuccess>(result);
}

[Fact]
public void TryGetSourceName_ReturnsNull()
{
var name = OpaqueClipStrategy.Instance.TryGetSourceName("<root name=\"Whatever\"/>");

Assert.Null(name);
}
}
35 changes: 35 additions & 0 deletions tests/SharpFM.Tests/ClipTypes/ScriptClipStrategyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,41 @@ public void StepsAndScript_HaveDistinctIdentities()
Assert.Equal("Script", ScriptClipStrategy.Script.DisplayName);
}

[Fact]
public void TryGetSourceName_ReturnsScriptNameAttribute()
{
var name = ScriptClipStrategy.Script.TryGetSourceName(
"<fmxmlsnippet type=\"FMObjectList\"><Script name=\"FooBar\"></Script></fmxmlsnippet>");

Assert.Equal("FooBar", name);
}

[Fact]
public void TryGetSourceName_StepsClipWithoutScriptWrapper_ReturnsNull()
{
var name = ScriptClipStrategy.Steps.TryGetSourceName(
"<fmxmlsnippet type=\"FMObjectList\"><Step enable=\"True\" id=\"141\" name=\"Set Variable\"/></fmxmlsnippet>");

Assert.Null(name);
}

[Fact]
public void TryGetSourceName_MalformedXml_ReturnsNull()
{
var name = ScriptClipStrategy.Script.TryGetSourceName("<oops>");

Assert.Null(name);
}

[Fact]
public void TryGetSourceName_PreservesPunctuationInName()
{
var name = ScriptClipStrategy.Script.TryGetSourceName(
"<fmxmlsnippet type=\"FMObjectList\"><Script name=\"My &quot;favorite&quot; script\"></Script></fmxmlsnippet>");

Assert.Equal("My \"favorite\" script", name);
}

[Fact]
public void Parse_EmptyScriptSnippet_ReturnsLosslessSuccess()
{
Expand Down
26 changes: 26 additions & 0 deletions tests/SharpFM.Tests/ClipTypes/TableClipStrategyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,32 @@ public void TableAndField_HaveDistinctIdentities()
Assert.Equal("Mac-XMFD", TableClipStrategy.Field.FormatId);
}

[Fact]
public void TryGetSourceName_ReturnsBaseTableNameAttribute()
{
var name = TableClipStrategy.Table.TryGetSourceName(
"<fmxmlsnippet type=\"FMObjectList\"><BaseTable name=\"Customers\" id=\"1\"></BaseTable></fmxmlsnippet>");

Assert.Equal("Customers", name);
}

[Fact]
public void TryGetSourceName_FieldClipWithoutBaseTable_ReturnsNull()
{
var name = TableClipStrategy.Field.TryGetSourceName(
"<fmxmlsnippet type=\"FMObjectList\"><Field id=\"1\" name=\"ID\"/></fmxmlsnippet>");

Assert.Null(name);
}

[Fact]
public void TryGetSourceName_MalformedXml_ReturnsNull()
{
var name = TableClipStrategy.Table.TryGetSourceName("<oops>");

Assert.Null(name);
}

[Fact]
public void Parse_ValidTable_ReturnsSuccess()
{
Expand Down
72 changes: 72 additions & 0 deletions tests/SharpFM.Tests/ViewModels/MainWindowViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using SharpFM.Dialogs;
using SharpFM.Model;
using SharpFM.Plugin;
using SharpFM.Plugin.UI;
using SharpFM.Services;
Expand Down Expand Up @@ -199,6 +200,77 @@ public async Task PasteFileMakerClipData_SelectsLastPastedClip()
Assert.Contains("Pasted 2 clip(s)", vm.StatusMessage);
}

[Fact]
public async Task PasteFileMakerClipData_ScriptWithMetadataName_UsesMetadataAsClipName()
{
var clipboard = new MockClipboardService();
clipboard.ClipboardData["Mac-XMSC"] = BuildClipBytes(
"<fmxmlsnippet><Script name=\"OrderTotal\"></Script></fmxmlsnippet>");
var vm = CreateVm(clipboard);
vm.FileMakerClips.Clear();

await vm.PasteFileMakerClipData();

Assert.Equal("OrderTotal", vm.SelectedClip!.Clip.Name);
}

[Fact]
public async Task PasteFileMakerClipData_TableWithBaseTableName_UsesItAsClipName()
{
var clipboard = new MockClipboardService();
clipboard.ClipboardData["Mac-XMTB"] = BuildClipBytes(
"<fmxmlsnippet><BaseTable name=\"Customers\"></BaseTable></fmxmlsnippet>");
var vm = CreateVm(clipboard);
vm.FileMakerClips.Clear();

await vm.PasteFileMakerClipData();

Assert.Equal("Customers", vm.SelectedClip!.Clip.Name);
}

[Fact]
public async Task PasteFileMakerClipData_NoMetadataName_FallsBackToNewClip()
{
var clipboard = new MockClipboardService();
clipboard.ClipboardData["Mac-XMSS"] = BuildClipBytes(
"<fmxmlsnippet type=\"FMObjectList\"><Step enable=\"True\" id=\"141\" name=\"Set Variable\"/></fmxmlsnippet>");
var vm = CreateVm(clipboard);
vm.FileMakerClips.Clear();

await vm.PasteFileMakerClipData();

Assert.Equal("new-clip", vm.SelectedClip!.Clip.Name);
}

[Fact]
public async Task PasteFileMakerClipData_CollidingName_AppendsNumericSuffix()
{
var clipboard = new MockClipboardService();
clipboard.ClipboardData["Mac-XMSC"] = BuildClipBytes(
"<fmxmlsnippet><Script name=\"OrderTotal\"></Script></fmxmlsnippet>");
var vm = CreateVm(clipboard);
vm.FileMakerClips.Clear();
vm.FileMakerClips.Add(new ClipViewModel(Clip.FromXml("OrderTotal", "Mac-XMSC", "<fmxmlsnippet/>")));

await vm.PasteFileMakerClipData();

Assert.Equal("OrderTotal (2)", vm.SelectedClip!.Clip.Name);
}

[Fact]
public async Task PasteFileMakerClipData_PreservesPunctuationInName()
{
var clipboard = new MockClipboardService();
clipboard.ClipboardData["Mac-XMSC"] = BuildClipBytes(
"<fmxmlsnippet><Script name=\"My &quot;favorite&quot; script\"></Script></fmxmlsnippet>");
var vm = CreateVm(clipboard);
vm.FileMakerClips.Clear();

await vm.PasteFileMakerClipData();

Assert.Equal("My \"favorite\" script", vm.SelectedClip!.Clip.Name);
}

private static byte[] BuildClipBytes(string xml)
{
var payload = Encoding.UTF8.GetBytes(xml);
Expand Down
Loading