diff --git a/src/SharpFM.Model/ClipTypes/IClipTypeStrategy.cs b/src/SharpFM.Model/ClipTypes/IClipTypeStrategy.cs
index 3d3bd41..b3077ab 100644
--- a/src/SharpFM.Model/ClipTypes/IClipTypeStrategy.cs
+++ b/src/SharpFM.Model/ClipTypes/IClipTypeStrategy.cs
@@ -26,7 +26,9 @@ public interface IClipTypeStrategy
///
/// Produce a starter XML body for a fresh clip with the given name. Used by
- /// "new clip" flows in the host and by plugins.
+ /// "new clip" flows in the host and by plugins. Build with
+ /// rather than string concatenation so
+ /// XML metacharacters in are escaped by the framework.
///
string DefaultXml(string clipName);
diff --git a/src/SharpFM.Model/ClipTypes/TableClipStrategy.cs b/src/SharpFM.Model/ClipTypes/TableClipStrategy.cs
index 6d4f103..cb9d9a7 100644
--- a/src/SharpFM.Model/ClipTypes/TableClipStrategy.cs
+++ b/src/SharpFM.Model/ClipTypes/TableClipStrategy.cs
@@ -5,7 +5,6 @@
using System.Xml.Linq;
using SharpFM.Model.Parsing;
using SharpFM.Model.Schema;
-using SharpFM.Model.Scripting;
namespace SharpFM.Model.ClipTypes;
@@ -65,10 +64,15 @@ public ClipParseResult Parse(string xml)
return new ParseSuccess(new TableClipModel(table), report);
}
- public string DefaultXml(string clipName) =>
- _wrapsBaseTable
- ? $""
- : "";
+ public string DefaultXml(string clipName)
+ {
+ var snippet = new XElement("fmxmlsnippet", new XAttribute("type", "FMObjectList"));
+ if (_wrapsBaseTable)
+ {
+ snippet.Add(new XElement("BaseTable", new XAttribute("name", clipName)));
+ }
+ return snippet.ToString(SaveOptions.DisableFormatting);
+ }
public string? TryGetSourceName(string xml)
{
diff --git a/src/SharpFM.Model/Schema/FmField.cs b/src/SharpFM.Model/Schema/FmField.cs
index 27de225..aa8fd9d 100644
--- a/src/SharpFM.Model/Schema/FmField.cs
+++ b/src/SharpFM.Model/Schema/FmField.cs
@@ -189,7 +189,7 @@ public XElement ToXml()
// Calculation
if (!string.IsNullOrEmpty(Calculation))
{
- var calcEl = XElement.Parse($"");
+ var calcEl = new XElement("Calculation", new XCData(Calculation));
if (AlwaysEvaluate)
calcEl.Add(new XAttribute("alwaysEvaluate", "True"));
if (!string.IsNullOrEmpty(CalculationContext))
@@ -222,10 +222,10 @@ public XElement ToXml()
new XAttribute("nextSerialNumber", AutoEnterValue ?? "1")));
break;
case AutoEnterType.ConstantData:
- autoEl.Add(XElement.Parse($""));
+ autoEl.Add(new XElement("ConstantData", new XCData(AutoEnterValue ?? "")));
break;
case AutoEnterType.Calculation:
- autoEl.Add(XElement.Parse($""));
+ autoEl.Add(new XElement("Calculation", new XCData(AutoEnterValue ?? "")));
break;
}
@@ -249,16 +249,16 @@ public XElement ToXml()
{
var rangeEl = new XElement("Range");
if (RangeMin != null)
- rangeEl.Add(XElement.Parse($""));
+ rangeEl.Add(new XElement("MinimumValue", new XCData(RangeMin)));
if (RangeMax != null)
- rangeEl.Add(XElement.Parse($""));
+ rangeEl.Add(new XElement("MaximumValue", new XCData(RangeMax)));
valEl.Add(rangeEl);
}
if (ValidationCalculation != null)
- valEl.Add(XElement.Parse($""));
+ valEl.Add(new XElement("StrictValidation", new XCData(ValidationCalculation)));
if (ErrorMessage != null)
- valEl.Add(XElement.Parse($""));
+ valEl.Add(new XElement("ErrorMessage", new XCData(ErrorMessage)));
el.Add(valEl);
}
diff --git a/src/SharpFM.Model/Scripting/Values/Calculation.cs b/src/SharpFM.Model/Scripting/Values/Calculation.cs
index 1befcf1..8ca0200 100644
--- a/src/SharpFM.Model/Scripting/Values/Calculation.cs
+++ b/src/SharpFM.Model/Scripting/Values/Calculation.cs
@@ -16,7 +16,7 @@ public sealed record Calculation(string Text)
/// common case in FileMaker script XML.
///
public XElement ToXml(string elementName = "Calculation") =>
- XElement.Parse($"<{elementName}>{elementName}>");
+ new(elementName, new XCData(Text));
///
/// Parse the text body of the element (CDATA is transparent to
diff --git a/src/SharpFM.Model/Scripting/XmlHelpers.cs b/src/SharpFM.Model/Scripting/XmlHelpers.cs
index eb1c675..57d4d55 100644
--- a/src/SharpFM.Model/Scripting/XmlHelpers.cs
+++ b/src/SharpFM.Model/Scripting/XmlHelpers.cs
@@ -6,14 +6,6 @@ namespace SharpFM.Model.Scripting;
public static class XmlHelpers
{
- public static string XmlEscape(string s)
- {
- return s.Replace("&", "&")
- .Replace("<", "<")
- .Replace(">", ">")
- .Replace("\"", """);
- }
-
public static string Unquote(string s)
{
if (s.Length >= 2 && s[0] == '"' && s[^1] == '"')
diff --git a/tests/SharpFM.Tests/ClipTypes/TableClipStrategyTests.cs b/tests/SharpFM.Tests/ClipTypes/TableClipStrategyTests.cs
index d299d54..521f2b0 100644
--- a/tests/SharpFM.Tests/ClipTypes/TableClipStrategyTests.cs
+++ b/tests/SharpFM.Tests/ClipTypes/TableClipStrategyTests.cs
@@ -100,6 +100,7 @@ public void DefaultXml_ProducesParseableSnippet()
[InlineData("My \"favorite\" stuff")]
[InlineData("A & B")]
[InlineData("")]
+ [InlineData("O'Brien")]
public void Table_DefaultXml_EscapesPunctuationInName(string clipName)
{
var seed = TableClipStrategy.Table.DefaultXml(clipName);
diff --git a/tests/SharpFM.Tests/Scripting/Values/CalculationTests.cs b/tests/SharpFM.Tests/Scripting/Values/CalculationTests.cs
index b3c6fef..d6fcf9b 100644
--- a/tests/SharpFM.Tests/Scripting/Values/CalculationTests.cs
+++ b/tests/SharpFM.Tests/Scripting/Values/CalculationTests.cs
@@ -55,4 +55,17 @@ public void RoundTrip_PreservesComplexExpression()
Assert.Equal(expr, roundTripped.Value);
}
+
+ [Theory]
+ [InlineData("a < b & c > d")]
+ [InlineData("Quote(\"hello\")")]
+ [InlineData("If ( name = \"O'Brien\" ; 1 ; 0 )")]
+ public void RoundTrip_PreservesXmlMetacharacters(string expr)
+ {
+ var emitted = new Calculation(expr).ToXml();
+ var serialized = emitted.ToString();
+ var reparsed = Calculation.FromXml(XElement.Parse(serialized));
+
+ Assert.Equal(expr, reparsed.Text);
+ }
}