Created
August 26, 2024 20:41
-
-
Save nathan130200/a0e0d00096c1492178e1865fe321d3ea to your computer and use it in GitHub Desktop.
C# helper methods & types to manage XML Linq Elements in .NET
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Diagnostics; | |
using System.Diagnostics.CodeAnalysis; | |
using System.Globalization; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
using System.Xml.Linq; | |
namespace Jabber; | |
public static partial class Xml | |
{ | |
[GeneratedRegex(@"(?<tagName>.*)\[@xmlns\=\'(?<namespaceURI>.*)\'\]")] | |
internal static partial Regex GetElementByNamespace(); | |
[GeneratedRegex(@"(?<tagName>.*)\[(?<fromEnd>\^)?(?<elementIndex>[0-9+]+)\]")] | |
internal static partial Regex GetElementByIndex(); | |
[GeneratedRegex(@"(?<tagName>.*)\[@(?<attrName>.*)=['""](?<attrVal>.*)['""]\]")] | |
internal static partial Regex GetElementByAttribute(); | |
public static XElement Root(this XElement child) | |
{ | |
while (child.Parent != null) | |
child = child.Parent; | |
return child; | |
} | |
public static XElement? XPathQueryForElement(this XElement element, string expression) | |
{ | |
if (expression.StartsWith('/')) | |
{ | |
element = element.Root(); | |
expression = expression[1..]; | |
} | |
var args = expression.Split('/'); | |
if (args.Length == 0) | |
return null; | |
XElement? temp = element; | |
foreach (var arg in args) | |
{ | |
// indicate we have "complex" filter here. | |
if (arg.Contains('[') && arg.Contains(']')) | |
{ | |
// find by namespace filter: [@xmlns=value] | |
if (GetElementByNamespace().TryMatch(arg, out var xmlnsMatch)) | |
{ | |
string tagName = xmlnsMatch.Groups["tagName"].Value; | |
string namespaceURI = xmlnsMatch.Groups["namespaceURI"].Value; | |
temp = temp.Elements().FirstOrDefault(x => | |
{ | |
return x.GetTagName() == tagName | |
&& x.GetNamespace() == namespaceURI; | |
}); | |
if (temp != null) | |
continue; | |
} | |
//find by: element[index] | |
if (GetElementByIndex().TryMatch(arg, out var indexMatch)) | |
{ | |
var tagName = indexMatch.Groups["tagName"].Value; | |
var elementIndex = int.Parse(indexMatch.Groups["elementIndex"].Value); | |
// using new Index operator syntax from C#. if char '^' is found at start, indicate the index is from end. | |
var isFromEnd = indexMatch.Groups["fromEnd"].Value == "^"; | |
var allElements = temp.Elements(); | |
var count = allElements.Count(); | |
// should we clamp index from count? | |
// or leave LINQ thrown exception if out of range? | |
// .Count() is expensive method call? | |
temp = allElements.ElementAtOrDefault(isFromEnd | |
? Index.FromEnd(elementIndex % count) | |
: Index.FromStart(elementIndex % count)); | |
if (temp != null) | |
continue; | |
} | |
// find by: [@attribute=value] | |
if (GetElementByAttribute().TryMatch(arg, out var byAttributeMatch)) | |
{ | |
var tagName = byAttributeMatch.Groups["tagName"].Value; | |
var attrName = byAttributeMatch.Groups["attrName"].Value; | |
var attrVal = byAttributeMatch.Groups["attrVal"].Value; | |
var sub = temp.Elements().FirstOrDefault(x => | |
{ | |
var attr = x.Attributes().FirstOrDefault(a => | |
a.GetName() == attrName && a.Value == attrVal | |
); | |
return attr != null; | |
}); | |
if (sub != null) | |
{ | |
temp = sub; | |
continue; | |
} | |
} | |
// not found, abort | |
return null; | |
} | |
else | |
{ | |
// find just by qualified XML element name. | |
temp = temp.Elements().FirstOrDefault(x => x.GetTagName() == arg); | |
if (temp == null) | |
break; | |
} | |
} | |
return temp; | |
} | |
// A simple hacky to execute regex match inplace. | |
static bool TryMatch(this Regex r, string term, [NotNullWhen(true)] out Match? result) | |
{ | |
try | |
{ | |
result = r.Match(term); | |
return result.Success; | |
} | |
catch (Exception ex) | |
{ | |
Debug.WriteLine(ex); | |
result = null; | |
return false; | |
} | |
} | |
// A simple hacky to get qualified name of the XML element (used in our XPath query parser) | |
public static string GetTagName(this XElement element) | |
{ | |
ArgumentNullException.ThrowIfNull(element); | |
var localName = element.Name.LocalName; | |
var namespaceURI = element.Name.Namespace; | |
var prefix = element.GetPrefixOfNamespace(namespaceURI); | |
if (prefix == null) | |
return localName; | |
return string.Concat(prefix, ':', localName); | |
} | |
// A simple hacky to get qualified name of the XML attribute (used in our XPath query parser) | |
public static string GetName(this XAttribute attribute) | |
{ | |
ArgumentNullException.ThrowIfNull(attribute); | |
var localName = attribute.Name.LocalName; | |
var namespaceURI = attribute.Name.Namespace; | |
if (namespaceURI == XNamespace.Xml) | |
return $"xml:{localName}"; | |
else if (namespaceURI == XNamespace.Xmlns) | |
return $"xmlns:{localName}"; | |
else | |
{ | |
var prefix = attribute.Parent?.GetPrefixOfNamespace(namespaceURI); | |
if (prefix == null) | |
return localName; | |
return string.Concat(prefix, ':', localName); | |
} | |
} | |
// Simple way to wrap namespace from XNamespace to our own custom Namespace type. | |
// I need to do this shit because XNamespace don't have implicit operators for conversion between XNamespace + string, | |
// so if i need to set XNamespace as attribute i need MANUALLY call XNamespace.NamespaceName EVERY FUCKING TIME to get namespace URI. | |
public static Namespace GetNamespace(this XElement element, string? prefix = default) | |
{ | |
if (prefix != null) | |
{ | |
var result = element.GetNamespaceOfPrefix(prefix); | |
if (result == null) | |
return Namespace.None; | |
return result; | |
} | |
var rawPrefix = element.GetPrefix(); | |
if (rawPrefix != null) | |
return element.GetDefaultNamespace(); | |
var ns = element.Name.Namespace; | |
if (ns == null || ns == XNamespace.None) | |
return Namespace.None; | |
return ns; | |
} | |
// Fast way to get start tag from XElement (useful in XMPP / stream start) | |
public static string StartTag(this XElement element) | |
{ | |
ArgumentNullException.ThrowIfNull(element); | |
var sb = new StringBuilder($"<{element.GetTagName()}"); | |
foreach (var attr in element.Attributes()) | |
sb.Append($" {attr.GetName()}=\"{attr.Value}\""); | |
return sb.Append('>').ToString(); | |
} | |
// Fast way to get end tag from XElement (useful in XMPP / stream end) | |
public static string EndTag(this XElement element) | |
{ | |
ArgumentNullException.ThrowIfNull(element); | |
return string.Concat("</", element.GetTagName(), '>'); | |
} | |
public static void SetAttribute(this XElement element, string name, object value, string? namespaceURI = default) | |
{ | |
var attributeName = BuildXName(element, name, namespaceURI, false); | |
var attr = element.Attribute(attributeName); | |
if (value == null) | |
attr?.Remove(); | |
else | |
{ | |
if (attr != null) | |
attr.SetValue(value); | |
else | |
{ | |
element.Add(new XAttribute(attributeName, value)); | |
} | |
} | |
} | |
public static string? GetAttribute(this XElement element, string name, string? defaultValue = default, string? namespaceURI = default) | |
=> element.Attribute(BuildXName(element, name, namespaceURI))?.Value ?? defaultValue; | |
// Dynamic way to parse attribute values with fallback value if attribute is absent or cannot be parsed. | |
[return: NotNullIfNotNull(nameof(defaultValue))] | |
public static T GetAttribute<T>(this XElement element, string name, T defaultValue = default!, string? namespaceURI = default) where T : IParsable<T> | |
{ | |
var str = element.GetAttribute(name, namespaceURI: namespaceURI); | |
// An hacky way to fast parse bool from string/int form. | |
if (typeof(T) == typeof(bool)) | |
{ | |
object? temp = default; | |
if (str == "1" || str == "true") | |
temp = true; | |
else if (str == "0" || str == "false") | |
temp = false; | |
if (temp != null) | |
return (T)temp; | |
} | |
if (T.TryParse(str, CultureInfo.InvariantCulture, out var result)) | |
return result; | |
return defaultValue; | |
} | |
// Dynamic way to parse attribute values as nullable primitives. | |
public static T? GetAttribute<T>(this XElement element, string name, string? namespaceURI = default) where T : struct, IParsable<T> | |
{ | |
var str = element.GetAttribute(name, namespaceURI: namespaceURI); | |
// Yet again the hacky way to fast parse bool from string/int form. | |
if (typeof(T) == typeof(bool)) | |
{ | |
object? temp = default; | |
if (str == "1" || str == "true") | |
temp = true; | |
else if (str == "0" || str == "false") | |
temp = false; | |
if (temp != null) | |
return (T)temp; | |
} | |
if (T.TryParse(str, CultureInfo.InvariantCulture, out var result)) | |
return result; | |
return default; | |
} | |
// Helper method to extract namespace prefix from this element. | |
public static string? GetPrefix(this XElement element) | |
=> element.GetPrefixOfNamespace(element.Name.Namespace); | |
// To be honest this is a terrible way to inherit XML namespaces between parent and children XML elements. But will work for now. | |
static XName BuildXName(XElement parent, string qualifiedName, string? namespaceURI = default, bool inherited = true) | |
{ | |
var ofs = qualifiedName.IndexOf(':'); | |
if (ofs == -1) | |
{ | |
if (namespaceURI == null) | |
{ | |
// When this namespace can inherits? Who decide this? | |
if (inherited) | |
return parent.Name.Namespace + qualifiedName; | |
return parent.GetDefaultNamespace() + qualifiedName; | |
} | |
return (XNamespace)namespaceURI + qualifiedName; | |
} | |
else | |
{ | |
var prefix = qualifiedName[0..ofs]; | |
var localName = qualifiedName[(ofs + 1)..]; | |
if (prefix == parent.GetPrefix()) | |
return parent.Name.Namespace + localName; | |
else | |
{ | |
// Thanks .NET for screw up with XML Namespace declarations in XElement. | |
if (prefix == "xml") | |
return XNamespace.Xml + localName; | |
else if (prefix == "xmlns") | |
return XNamespace.Xmlns + localName; | |
else | |
{ | |
if (namespaceURI != null) | |
return (XNamespace)namespaceURI + localName; | |
var ns = parent.GetNamespaceOfPrefix(prefix); | |
if (ns != null) | |
return ns + localName; | |
// Should inherit namespace too? Or leave as empty? Thanks again .NET for this mess! | |
return localName; | |
} | |
} | |
} | |
} | |
public static XElement C(this XElement parent, string tagName, bool inheritNamespace = true) | |
{ | |
var result = new XElement(BuildXName(parent, tagName, inherited: inheritNamespace)); | |
parent.Add(result); | |
return result; | |
} | |
public static XElement C(this XElement parent, XName name) | |
{ | |
var result = new XElement(name); | |
parent.Add(result); | |
return result; | |
} | |
public static XElement C(this XElement parent, string tagName, string? namespaceURI) | |
{ | |
var result = new XElement(BuildXName(parent, tagName, namespaceURI, false)); | |
parent.Add(result); | |
return result; | |
} | |
public static XElement? GetChild(this XElement element, string tagName, string? namespaceURI = default) | |
{ | |
return element.Element(BuildXName(element, tagName, namespaceURI)); | |
} | |
public static bool GetChild(this XElement element, string tagName, [NotNullWhen(true)] out XElement? result, string? namespaceURI = default) | |
{ | |
result = element.Element(BuildXName(element, tagName, namespaceURI)); | |
return result != null; | |
} | |
public static IEnumerable<XElement> GetChildren(this XElement element, string tagName, string? namespaceURI = default) | |
{ | |
return element.Elements(BuildXName(element, tagName, namespaceURI)); | |
} | |
} | |
public readonly struct Namespace | |
{ | |
private readonly string _value; | |
private readonly XNamespace _namespaceName; | |
Namespace(XNamespace other) | |
{ | |
_value = other.NamespaceName; | |
_namespaceName = other; | |
} | |
Namespace(string value) | |
{ | |
_value = value; | |
_namespaceName = XNamespace.Get(value); | |
} | |
public XAttribute GetNamespaceDeclaration() | |
=> new("xmlns", _value); | |
public XAttribute GetNamespaceDeclaration(string prefix) | |
=> new(Xmlns + prefix, _value); | |
// ------------------------------------------------------------------------------------------------------------------------ // | |
public static readonly Namespace None = new(XNamespace.None); | |
public static readonly Namespace Xml = new(XNamespace.Xml); | |
public static readonly Namespace Xmlns = new(XNamespace.Xmlns); | |
// ------------------------------------------------------------------------------------------------------------------------ // | |
public static readonly Namespace Stream = new("http://etherx.jabber.org/streams"); | |
public static readonly Namespace Client = new("jabber:client"); | |
public static readonly Namespace Server = new("jabber:server"); | |
public static class Component | |
{ | |
public static readonly Namespace Connect = new("jabber:component:connect"); | |
public static readonly Namespace Accept = new("jabber:component:accept"); | |
} | |
public static readonly Namespace CryOnline = new("urn:cryonline:k01"); | |
// ------------------------------------------------------------------------------------------------------------------------ // | |
public override int GetHashCode() => _namespaceName.GetHashCode(); | |
public override string ToString() => _value; | |
public override bool Equals([NotNullWhen(true)] object? obj) | |
{ | |
return obj is Namespace other | |
&& _value.Equals(other._value, StringComparison.Ordinal); | |
} | |
// ------------------------------------------------------------------------------------------------------------------------ // | |
public static implicit operator string(Namespace self) => self._value; | |
public static implicit operator Namespace(string str) => new(str); | |
public static implicit operator Namespace(XNamespace ns) => new(ns); | |
// ------------------------------------------------------------------------------------------------------------------------ // | |
public static XName operator +(Namespace self, string other) | |
{ | |
return self._namespaceName + other; | |
} | |
public static bool operator ==(Namespace left, Namespace right) | |
{ | |
return left.Equals(right); | |
} | |
public static bool operator !=(Namespace left, Namespace right) | |
{ | |
return !(left == right); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment