diff --git a/PowerKit.Tests/XmlTests.cs b/PowerKit.Tests/XmlTests.cs
new file mode 100644
index 0000000..14ca8da
--- /dev/null
+++ b/PowerKit.Tests/XmlTests.cs
@@ -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 < 2");
+ Xml.Escape("2 > 1").Should().Be("2 > 1");
+ Xml.Escape("say \"hello\"").Should().Be("say "hello"");
+ Xml.Escape("it's").Should().Be("it's");
+ Xml.Escape("& < > \" '").Should().Be("& < > " '");
+ 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();
+ }
+}
diff --git a/PowerKit/Xml.cs b/PowerKit/Xml.cs
new file mode 100644
index 0000000..bedafe9
--- /dev/null
+++ b/PowerKit/Xml.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Text;
+
+namespace PowerKit;
+
+///
+/// Helper methods for working with XML.
+///
+public static class Xml
+{
+ private static bool IsValidXmlChar(char ch) =>
+ ch == '\t'
+ || ch == '\n'
+ || ch == '\r'
+ || (ch >= '\x20' && ch <= '\xD7FF')
+ || (ch >= '\xE000' && ch <= '\xFFFD');
+
+ ///
+ /// 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 (&, <, >, ", ')
+ /// are replaced with their corresponding XML entities.
+ ///
+ 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 = "&";
+ }
+ else if (ch == '<')
+ {
+ replacement = "<";
+ }
+ else if (ch == '>')
+ {
+ replacement = ">";
+ }
+ else if (ch == '"')
+ {
+ replacement = """;
+ }
+ else if (ch == '\'')
+ {
+ replacement = "'";
+ }
+ 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;
+ }
+}