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}>"); + 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); + } }