Skip to content

Commit

Permalink
Add an option for WriteNode to keep attribute namespaces
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jasonleenaylor committed Nov 7, 2024
1 parent 55e98d8 commit de37405
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 60 additions & 2 deletions SIL.Core.Tests/Xml/XmlUtilsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,62 @@ public void WriteNode_DoesNotIndentChildWhenSuppressed()
}
Assert.That(output.ToString(), Is.EqualTo(expectedOutput));
}

[Test]
public void WriteNode_PreserveNamespacesArePreserved()
{
string input = @"<text><span class='bold' xml:space='preserve'> </span></text>";
string expectedOutput =
"<?xml version=\"1.0\" encoding=\"utf-16\"?>\r\n"
+ "<root>\r\n"
+ " <text>\r\n"
+ " <span\r\n"
+ " class=\"bold\"\r\n"
+ " xml:space=\"preserve\"> </span>\r\n"
+ " </text>\r\n"
+ "</root>";
var output = new StringBuilder();
var preserveNamespace = new HashSet<string>();
preserveNamespace.Add("xml");
using (var writer = XmlWriter.Create(output, CanonicalXmlSettings.CreateXmlWriterSettings()))
{
writer.WriteStartDocument();
writer.WriteStartElement("root");
XmlUtils.WriteNode(writer, input, new HashSet<string>(), preserveNamespace);
writer.WriteEndElement();
writer.WriteEndDocument();
}
Assert.That(output.ToString(), Is.EqualTo(expectedOutput));
}

[Test]
public void WriteNode_ProtectsAgainstXmlnsFormatThrashing()
{
string input = @"<text><span class='bold' xmlns:fw='http://software.sil.org/fieldworks' fw:special='yes' xml:space='preserve'> </span></text>";
string expectedOutput =

Check warning on line 211 in SIL.Core.Tests/Xml/XmlUtilsTests.cs

View workflow job for this annotation

GitHub Actions / extract-and-upload-strings

The variable 'expectedOutput' is assigned but its value is never used

Check warning on line 211 in SIL.Core.Tests/Xml/XmlUtilsTests.cs

View workflow job for this annotation

GitHub Actions / extract-and-upload-strings

The variable 'expectedOutput' is assigned but its value is never used
"<?xml version=\"1.0\" encoding=\"utf-16\"?>\r\n"
+ "<root>\r\n"
+ " <text>\r\n"
+ " <span\r\n"
+ " class=\"bold\"\r\n"
+ " xmlns:fw=\"http://software.sil.org/fieldworks\"\r\n"
+ " fw:special=\"yes\"\r\n"
+ " xml:space=\"preserve\"> </span>\r\n"
+ " </text>\r\n"
+ "</root>";
var output = new StringBuilder();
var preserveNamespace = new HashSet<string>();
preserveNamespace.Add("xml");
preserveNamespace.Add("xmlns");
preserveNamespace.Add("fw");
using (var writer = XmlWriter.Create(output, CanonicalXmlSettings.CreateXmlWriterSettings()))
{
writer.WriteStartDocument();
writer.WriteStartElement("root");
Assert.Throws<ArgumentException>(()=> XmlUtils.WriteNode(writer, input, new HashSet<string>(), preserveNamespace));
}
}

/// <summary>
/// This verifies that suppressing pretty-printing of children works for spans nested in spans nested in text.
/// </summary>
Expand All @@ -191,8 +247,10 @@ public void WriteNode_DoesNotIndentChildWhenTwoLevelsSuppressed()
+ " class=\"italic\">bit</span>bt</span></text>\r\n"
+ "</root>";
var output = new StringBuilder();
var suppressIndentingChildren = new HashSet<string>();
suppressIndentingChildren.Add("text");
var suppressIndentingChildren = new HashSet<string>
{
"text"
};
using (var writer = XmlWriter.Create(output, CanonicalXmlSettings.CreateXmlWriterSettings()))
{
writer.WriteStartDocument();
Expand Down
38 changes: 31 additions & 7 deletions SIL.Core/Xml/XmlUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -970,10 +970,11 @@ public static string GetTitleOfHtml(XmlDocument dom, string defaultIfMissing)
/// <param name="writer"></param>
/// <param name="dataToWrite"></param>
/// <param name="suppressIndentingChildren"></param>
public static void WriteNode(XmlWriter writer, string dataToWrite, HashSet<string> suppressIndentingChildren)
/// <param name="preserveNamespaces">a set of namespaces to preserve when writing out elements</param>
public static void WriteNode(XmlWriter writer, string dataToWrite, HashSet<string> suppressIndentingChildren, HashSet<string> preserveNamespaces = null)
{
XElement element = XDocument.Parse(dataToWrite).Root;
WriteNode(writer, element, suppressIndentingChildren);
WriteNode(writer, element, suppressIndentingChildren, preserveNamespaces);
}

/// <summary>
Expand All @@ -984,11 +985,12 @@ public static void WriteNode(XmlWriter writer, string dataToWrite, HashSet<strin
/// <param name="writer"></param>
/// <param name="dataToWrite"></param>
/// <param name="suppressIndentingChildren"></param>
public static void WriteNode(XmlWriter writer, XElement dataToWrite, HashSet<string> suppressIndentingChildren)
/// <param name="preserveNamespaces">a set of namespaces to preserve when writing out elements</param>
public static void WriteNode(XmlWriter writer, XElement dataToWrite, HashSet<string> suppressIndentingChildren, HashSet<string> preserveNamespaces = null)
{
if (dataToWrite == null)
return;
WriteElementTo(writer, dataToWrite, suppressIndentingChildren);
WriteElementTo(writer, dataToWrite, suppressIndentingChildren, preserveNamespaces);
}

/// <summary>
Expand All @@ -997,11 +999,33 @@ public static void WriteNode(XmlWriter writer, XElement dataToWrite, HashSet<str
/// <param name="writer"></param>
/// <param name="element"></param>
/// <param name="suppressIndentingChildren"></param>
private static void WriteElementTo(XmlWriter writer, XElement element, HashSet<string> suppressIndentingChildren)
/// <param name="preserveNamespaces">a set of namespaces to preserve when writing out elements</param>
private static void WriteElementTo(XmlWriter writer, XElement element, HashSet<string> suppressIndentingChildren, HashSet<string> 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
Expand All @@ -1018,7 +1042,7 @@ private static void WriteElementTo(XmlWriter writer, XElement element, HashSet<s
{
var xElement = child as XElement;
if (xElement != null)
WriteElementTo(writer, xElement, suppressIndentingChildren);
WriteElementTo(writer, xElement, suppressIndentingChildren, preserveNamespaces);
else
child.WriteTo(writer); // defaults are fine for everything else.
}
Expand Down

0 comments on commit de37405

Please sign in to comment.