From de374056a74ead9ce18aef30111b834cc398f9b1 Mon Sep 17 00:00:00 2001 From: Jason Naylor Date: Wed, 30 Oct 2024 13:44:59 -0700 Subject: [PATCH] Add an option for WriteNode to keep attribute namespaces * Adds an optional parameter to WriteNode to specify what attribute namespaces should be retained * Supports a FlexBridge fix https://jira.sil.org/browse/LT-21388 where xml:space was being saved as 'space' which on a further write by a normal xml serializer would then drop spaces +semver:minor --- CHANGELOG.md | 1 + SIL.Core.Tests/Xml/XmlUtilsTests.cs | 62 ++++++++++++++++++++++++++++- SIL.Core/Xml/XmlUtils.cs | 38 ++++++++++++++---- 3 files changed, 92 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48ee9b792..3fe200a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added +- [SIL.Core] Added optional parameter, preserveNamespaces, to XmlUtils.WriteNode - [SIL.Core] Added optional parameter, includeSystemLibraries, to AcknowledgementsProvider.CollectAcknowledgements - [SIL.Windows.Forms] Added ability to select which SIL logo(s) to use in SILAboutBox. - [SIL.Windows.Forms] Added public enum Widgets.SilLogoVariant diff --git a/SIL.Core.Tests/Xml/XmlUtilsTests.cs b/SIL.Core.Tests/Xml/XmlUtilsTests.cs index 8cf58f101..1ee82f04e 100644 --- a/SIL.Core.Tests/Xml/XmlUtilsTests.cs +++ b/SIL.Core.Tests/Xml/XmlUtilsTests.cs @@ -176,6 +176,62 @@ public void WriteNode_DoesNotIndentChildWhenSuppressed() } Assert.That(output.ToString(), Is.EqualTo(expectedOutput)); } + + [Test] + public void WriteNode_PreserveNamespacesArePreserved() + { + string input = @" "; + string expectedOutput = + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + var output = new StringBuilder(); + var preserveNamespace = new HashSet(); + preserveNamespace.Add("xml"); + using (var writer = XmlWriter.Create(output, CanonicalXmlSettings.CreateXmlWriterSettings())) + { + writer.WriteStartDocument(); + writer.WriteStartElement("root"); + XmlUtils.WriteNode(writer, input, new HashSet(), preserveNamespace); + writer.WriteEndElement(); + writer.WriteEndDocument(); + } + Assert.That(output.ToString(), Is.EqualTo(expectedOutput)); + } + + [Test] + public void WriteNode_ProtectsAgainstXmlnsFormatThrashing() + { + string input = @" "; + string expectedOutput = + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + var output = new StringBuilder(); + var preserveNamespace = new HashSet(); + preserveNamespace.Add("xml"); + preserveNamespace.Add("xmlns"); + preserveNamespace.Add("fw"); + using (var writer = XmlWriter.Create(output, CanonicalXmlSettings.CreateXmlWriterSettings())) + { + writer.WriteStartDocument(); + writer.WriteStartElement("root"); + Assert.Throws(()=> XmlUtils.WriteNode(writer, input, new HashSet(), preserveNamespace)); + } + } + /// /// This verifies that suppressing pretty-printing of children works for spans nested in spans nested in text. /// @@ -191,8 +247,10 @@ public void WriteNode_DoesNotIndentChildWhenTwoLevelsSuppressed() + " class=\"italic\">bitbt\r\n" + ""; var output = new StringBuilder(); - var suppressIndentingChildren = new HashSet(); - suppressIndentingChildren.Add("text"); + var suppressIndentingChildren = new HashSet + { + "text" + }; using (var writer = XmlWriter.Create(output, CanonicalXmlSettings.CreateXmlWriterSettings())) { writer.WriteStartDocument(); diff --git a/SIL.Core/Xml/XmlUtils.cs b/SIL.Core/Xml/XmlUtils.cs index 5f5bbe362..8f0987431 100644 --- a/SIL.Core/Xml/XmlUtils.cs +++ b/SIL.Core/Xml/XmlUtils.cs @@ -970,10 +970,11 @@ public static string GetTitleOfHtml(XmlDocument dom, string defaultIfMissing) /// /// /// - public static void WriteNode(XmlWriter writer, string dataToWrite, HashSet suppressIndentingChildren) + /// a set of namespaces to preserve when writing out elements + public static void WriteNode(XmlWriter writer, string dataToWrite, HashSet suppressIndentingChildren, HashSet preserveNamespaces = null) { XElement element = XDocument.Parse(dataToWrite).Root; - WriteNode(writer, element, suppressIndentingChildren); + WriteNode(writer, element, suppressIndentingChildren, preserveNamespaces); } /// @@ -984,11 +985,12 @@ public static void WriteNode(XmlWriter writer, string dataToWrite, HashSet /// /// - public static void WriteNode(XmlWriter writer, XElement dataToWrite, HashSet suppressIndentingChildren) + /// a set of namespaces to preserve when writing out elements + public static void WriteNode(XmlWriter writer, XElement dataToWrite, HashSet suppressIndentingChildren, HashSet preserveNamespaces = null) { if (dataToWrite == null) return; - WriteElementTo(writer, dataToWrite, suppressIndentingChildren); + WriteElementTo(writer, dataToWrite, suppressIndentingChildren, preserveNamespaces); } /// @@ -997,11 +999,33 @@ public static void WriteNode(XmlWriter writer, XElement dataToWrite, HashSet /// /// - private static void WriteElementTo(XmlWriter writer, XElement element, HashSet suppressIndentingChildren) + /// a set of namespaces to preserve when writing out elements + private static void WriteElementTo(XmlWriter writer, XElement element, HashSet suppressIndentingChildren, HashSet preserveNamespaces = null) { writer.WriteStartElement(element.Name.LocalName); foreach (var attr in element.Attributes()) - writer.WriteAttributeString(attr.Name.LocalName, attr.Value); + { + // if we are preserving namespaces, we may need to write the attribute with the prefix + if (preserveNamespaces != null && !string.IsNullOrEmpty(attr.Name.NamespaceName)) + { + var attrPrefix = element.GetPrefixOfNamespace(attr.Name.NamespaceName); + if (preserveNamespaces.Contains(attrPrefix)) + { + if (attrPrefix == "xmlns") + { + // If you need to write out the xmlns attribute consistently between platforms custom code will need to be written + // I'm leaving it unimplemented until needed + throw new ArgumentException("The 'xmlns' local namespace declarations are handled differently in framework. Using it could cause thrashing."); + } + writer.WriteAttributeString(attrPrefix, attr.Name.LocalName, attr.Name.NamespaceName, attr.Value); + } + else + writer.WriteAttributeString(attr.Name.LocalName, attr.Value); + } + else + writer.WriteAttributeString(attr.Name.LocalName, attr.Value); + } + // The writer automatically suppresses indenting children for any element that it detects has text children. // However, it won't do this for the first child if that is an element, even if it later encounters text children. // Also, there may be a parent where text including white space is significant, yet it is possible for the @@ -1018,7 +1042,7 @@ private static void WriteElementTo(XmlWriter writer, XElement element, HashSet