Skip to content
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
27 changes: 27 additions & 0 deletions PowerKit.Tests/XmlTests.cs
Comment thread
Tyrrrz marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using FluentAssertions;
using Xunit;

namespace PowerKit.Tests;

public class XmlTests
{
[Fact]
public void Escape_Test()
{
// Act & assert
Xml.Escape("hello world").Should().Be("hello world");
Xml.Escape("foo & bar").Should().Be("foo & bar");
Xml.Escape("1 < 2").Should().Be("1 &lt; 2");
Xml.Escape("2 > 1").Should().Be("2 &gt; 1");
Xml.Escape("say \"hello\"").Should().Be("say &quot;hello&quot;");
Xml.Escape("it's").Should().Be("it&apos;s");
Xml.Escape("& < > \" '").Should().Be("&amp; &lt; &gt; &quot; &apos;");
Xml.Escape("foo\u0001bar").Should().Be("foobar");
Xml.Escape("foo\t\n\rbar").Should().Be("foo\t\n\rbar");
Xml.Escape("\U0001F600").Should().Be("\U0001F600");
Xml.Escape("foo\uD800bar").Should().Be("foobar");
Xml.Escape("foo\uDC00bar").Should().Be("foobar");
Xml.Escape("").Should().BeEmpty();
}
}
94 changes: 94 additions & 0 deletions PowerKit/Xml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.Text;

namespace PowerKit;

/// <summary>
/// Helper methods for working with XML.
/// </summary>
public static class Xml
{
private static bool IsValidXmlChar(char ch) =>
ch == '\t'
|| ch == '\n'
|| ch == '\r'
|| (ch >= '\x20' && ch <= '\xD7FF')
|| (ch >= '\xE000' && ch <= '\xFFFD');

/// <summary>
/// Escapes invalid XML characters in the specified string, returning a valid XML string.
/// Characters that cannot be represented in XML (such as most control characters) are removed.
/// Special XML characters (<c>&amp;</c>, <c>&lt;</c>, <c>&gt;</c>, <c>&quot;</c>, <c>&apos;</c>)
/// are replaced with their corresponding XML entities.
/// </summary>
public static string Escape(string str)
{
var builder = default(StringBuilder);

var i = 0;
while (i < str.Length)
{
var ch = str[i];

string? replacement;
if (ch == '&')
{
replacement = "&amp;";
}
else if (ch == '<')
{
replacement = "&lt;";
}
else if (ch == '>')
{
replacement = "&gt;";
}
else if (ch == '"')
{
replacement = "&quot;";
}
else if (ch == '\'')
{
replacement = "&apos;";
}
else if (
char.IsHighSurrogate(ch)
&& i + 1 < str.Length
&& char.IsLowSurrogate(str[i + 1])
)
{
// Valid surrogate pair — represents a supplementary character (U+10000..U+10FFFF),
// which is valid in XML. Append both chars and advance past the pair.
builder?.Append(ch);
builder?.Append(str[i + 1]);
i += 2;
continue;
}
else if (IsValidXmlChar(ch))
{
replacement = null;
}
else
{
// Truly invalid XML character — skip it.
builder ??= new StringBuilder(str, 0, i, str.Length);
i++;
continue;
}

if (replacement is not null)
{
builder ??= new StringBuilder(str, 0, i, str.Length);
builder.Append(replacement);
}
else
{
builder?.Append(ch);
}

i++;
}

return builder?.ToString() ?? str;
}
}
Loading