Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an optional parameter to XmlUtils.WriteNode to preserve specific namespaces #1357

Merged
merged 2 commits into from
Nov 7, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:

- name: Publish logs on failure
if: failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binary-logs
path: |
Expand Down
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 =
"<?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