GerardSmit.Language.Xml
3.0.0
dotnet add package GerardSmit.Language.Xml --version 3.0.0
NuGet\Install-Package GerardSmit.Language.Xml -Version 3.0.0
<PackageReference Include="GerardSmit.Language.Xml" Version="3.0.0" />
<PackageVersion Include="GerardSmit.Language.Xml" Version="3.0.0" />
<PackageReference Include="GerardSmit.Language.Xml" />
paket add GerardSmit.Language.Xml --version 3.0.0
#r "nuget: GerardSmit.Language.Xml, 3.0.0"
#:package GerardSmit.Language.Xml@3.0.0
#addin nuget:?package=GerardSmit.Language.Xml&version=3.0.0
#tool nuget:?package=GerardSmit.Language.Xml&version=3.0.0
XmlParser
This is a fork of the 'GuiLabs.Language.Xml' project. See KirillOsenkov/XmlParser for the original project. This project is not affiliated with the original project (the namespace and the project name are the same for compatibility reasons).
Changes
In comparison to the original project, this fork has the following changes:
3.0.0 changes what two existing members mean. Value — on elements and attributes alike — now returns the decoded text rather than the raw markup; RawValue is the old behaviour. AddChild and InsertChild now indent the child they add; pass indent: false for the old behaviour. Both changes are silent at the call site, so it is worth grepping for them on upgrade. XmlAttributeSyntax.Equals and ValueNode are also nullable now - they always could be null, for an attribute written as a bare name, and the type now says so.
Removed the interfaces
IXmlElementandIXmlElementSyntax.
Reason: this made editing the syntax tree more difficult, as the interfaces had to be cast to the SyntaxNode constantly. As replacement a new class calledXmlElementBaseSyntaxwas introduced.Added various enumerators for nodes, XML attributes and XML elements.
Reason: before the iterator methods were used, which generated a state machine and allocates memory. The enumerators are more efficient and don't allocate memory.The enumerators also have their own
FirstandFirstOrDefault, because reaching the LINQ ones boxes the enumerator. They start a fresh walk rather than continuing from wherever the enumerator happens to sit, soforeach,FirstandCountmean the same thing whatever order they are called in — a struct enumerator that hands out itself otherwise gives the second reader the tail of the first one's walk.Improved
ReplaceNodefor XML elements.
Reason: Before a visitor was used to replace nodes, which allocated more memory and was less efficient.Values are escaped on the way in and decoded on the way out.
Reason:SetAttributeand the string-taking factories used to write their argument verbatim, so a value containing&,<or a quote produced a document that no longer parsed — and the caller had no way to opt in, because the parameter is astring. They now escape. In the other directionValueresolves the five entities XML predefines plus numeric character references, unwraps CDATA sections and skips comments;RawValueis the text exactly as the document has it.root = root.SetAttribute("Include", "A&B<C"); // Include="A&B<C" root.GetAttributeValue("Include"); // A&B<C root.GetAttribute("Include").RawValue; // A&B<CWhitespace between an element's tags counts as its value, the way it does to an
XDocumentloaded withLoadOptions.PreserveWhitespace— this is a tree that keeps every character of the document, so discarding some of it here was never on offer. The scanner keeps whitespace that runs up to a tag as that tag's trivia rather than as content, soWithText(" ")used to write a document whoseValueread back empty, and<a>\n <b>x</b>\n</a>used to say its value was justx.RawValuecovers the same range, which is exactly the textContentSpanpoints at.Line endings follow the same rule a conforming reader does (XML 1.0 §2.11): a literal CRLF or lone CR in the document — in text, in whitespace between tags, or inside a CDATA section — reads back as one LF, while

is how a document says it means a carriage return and comes back as one.RawValueis untouched either way, and the escaping helpers write
so a value containing a CR survives the round trip.XmlEscaping.EncodeText,XmlEscaping.EncodeAttributeValue,XmlEscaping.NormalizeLineEndingsandXmlEscaping.Decodeare public for callers doing their own formatting.Decoderesolves only what XML defines — an unrecognised reference such as , which has no meaning without a DTD, is left exactly as it was found rather than turned into a character the document does not contain.SetAttributeandWithValuealso keep the quote character the attribute was already written with, and give a valueless attribute (<a x />, which an editor sees constantly) a real=rather than a value with nothing joining it to the name.SetAttributereads a:in the name as a prefix, soSetAttribute("xmlns:p", …)finds its own attribute again on the next call instead of appending a duplicate, and it places a new attribute in front of one that is still being typed (<a x= />), which would otherwise swallow the value it was given.AddChildandInsertChildindent by default.
Reason: they used to weld the new child to its sibling, which turned a one-line diff into a reformatted line. The indent unit and the line ending are taken from what the document already does. Passindent: falsefor the old behaviour.Added the following utility methods:
GetOrAddElement- gets or adds an element to the XML tree, with support for paths. For example:
A leading slash is accepted and ignored, soroot = root.GetOrAddElement("Project/PropertyGroup", out var propertyGroup);"/Project/PropertyGroup"means the same thing. Anything else that would produce a nameless segment - an empty path, a trailing slash, a doubled slash - throwsArgumentExceptionrather than creating an element with no name. Because a creating path writes its segments into the document, they must also be names the document can read back:GetOrAddElement("a b", …)throws rather than producing<a b />, which comes back as an elementacarrying an attributeband so gets created again on every call.GetElementsByPathreads paths by the same rules, minus that last one - it writes nothing, so a segment no element can be named simply matches none.SetAttribute- sets an attribute of an element. If the attribute does not exist, it is added.propertyGroup = propertyGroup.SetAttribute("TargetFramework", "net9.0");GetElement/GetElements- the child elements with a given name, mirroringGetAttributedown to the optional prefix.XmlElementBaseSyntax propertyGroup = root.GetElement("PropertyGroup"); foreach (XmlElementBaseSyntax reference in root.GetElements("PackageReference")) { // ... }GetElementsByPath- every element reachable by a slash-separated child path. Unlike a hand-rolled walker, it expands every segment rather than taking the first match at each step, so a path crossing repeated ancestors sees all of them.foreach (XmlElementBaseSyntax ipSecurity in root.GetElementsByPath("location/system.webServer/security/ipSecurity")) { // one per <location>, not just the first }GetIndentUnit,GetIndentandGetNewLine- what the document already does for formatting, so new nodes can be placed to match it without reimplementingNormalizeTrivia.string unit = root.GetIndentUnit(); // e.g. " " or "\t" string newLine = root.GetNewLine(); // "\r\n" or "\n"Descendants- every element below this one, name-filtered if you want, through a struct enumerator rather thanDescendantNodes().OfType<>().foreach (XmlElementBaseSyntax reference in root.Descendants("PackageReference")) { // including the ones inside Choose/When }GetElementByLocalName,GetElementsByLocalName,GetAttributeByLocalName,GetAttributeValueByLocalName- match the local name whatever the prefix, for a document that is the same model whether or not it was hand-edited to use one.- A
StringComparisonon every name lookup -GetElement,GetAttribute,Descendantsand the rest, though not the path APIs, which match ordinally - for the formats that are case-insensitive about names (MSBuild,packages.config). An empty prefix means "unprefixed", the same asnull, soGetAttributeValue("Type", string.Empty)does what it looks like it does. Attributesis now a struct enumerator over the attribute nodes, matchingElements, so the name, the value and the spans all stay reachable without allocating.ValueSpanandContentSpan- the span of an attribute value inside its quotes, and the range between an element's tags. Both hold up in a buffer being typed into, where the closing quote or end tag is synthesized and zero-width.TextSpan toReplace = attribute.ValueSpan; // excludes the quotesNameSpan- the span of an element's name: on the element itself, and onStartTagandEndTagseparately, which together are the pair a rename or linked editing edits. Zero-width but positioned where the name goes for a tag still being typed.TextSpan hover = element.NameSpan; TextSpan renameSecondEnd = element.EndTag.NameSpan;TextSpandeconstructs into(start, length), so converting to another span type does not need to name this one - it shares its simple name with Roslyn'sTextSpan, and a file naming both needs an alias. The optionalGerardSmit.Language.Xml.Roslynpackage goes further withToRoslynSpan()andToXmlSpan(); the core package stays dependency-free.GetOrAddElementandAddElementtake an optional predicate, so the first path segment can say which match it means.root = root.GetOrAddElement("PropertyGroup", g => g.GetAttribute("Condition") is null, out var group);WithTextsets an element's text content, escaped.SyntaxFactory.XmlEmptyElement(name, attributes)builds<PackageReference Include="A" />without touching tokens.GetElement,GetElements,GetElementsByPath,Descendantsand theirByLocalNamecounterparts also hang offXmlDocumentSyntax, treating the root element as the first path segment, so the nullableRootdance is gone from the common case.document.GetElementsByPath("Project/PropertyGroup/TargetFramework");SyntaxLocator.FindNodeanswers with the node the caret is in at the end of the buffer, instead of falling back to the document.
Original README:
A Roslyn-inspired full-fidelity XML parser with no dependencies and a simple Visual Studio XML language service.
- The parser produces a full-fidelity syntax tree, meaning every character of the source text is represented in the tree. The tree covers the entire source text.
- The parser has no dependencies and can easily be made portable. I would appreciate a high quality pull request making the parser portable.
- The parser is based on the section of the Roslyn VB parser that parses XML literals. The Roslyn code is ported to C# and is made standalone.
- The parser is error-tolerant. It will still produce a full tree even from invalid XML with missing tags, extra invalid text, etc. Missing and skipped tokens are still represented in the tree.
- The resulting tree is immutable and follows Roslyn's green/red separation for maximum reusability of nodes.
- The parser has basic support for incrementality. Given a previous constructed tree and a list of changes it will try to reuse existing nodes and only re-create what is necessary.
- This library is more low-level than XLinq (for instance XLinq doesn't seem to represent whitespace around attributes). Also it has no idea about XML namespaces and just tells you what's in the source text (whereas in XLinq there's too much ceremony around XML namespaces).
This is work in progress and by no means complete. Specifically:
- XML DTD is not supported (Roslyn didn't support it either)
- Code wasn't tuned for performance and allocations, I'm sure a lot can be done to reduce memory consumption by the resulting tree. It should be pretty efficient though.
- We reserve the right to accept only very high quality pull requests. We have very limited time to work on this so I ask everybody to please respect that.
Download from NuGet:
Try it!
https://xmlsyntaxvisualizer.azurewebsites.net/index.html
The above app leverages the parser and can help you visualize the resulting syntax tree generated from an XML document.
Code is available at https://github.com/garuma/XmlSyntaxVisualizer C# UWP example at https://github.com/michael-hawker/XmlSyntaxVisualizerUWP
Also see the blog post: https://blog.neteril.org/blog/2018/03/21/xml-parsing-roslyn/
Resources about Immutable Syntax Trees: https://github.com/KirillOsenkov/Bliki/wiki/Roslyn-Immutable-Trees
FAQ:
How to find a node in the tree given a position in the source text?
SyntaxLocator.FindNode(SyntaxNode node, int position);
How to replace a node in the tree
var original = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>
""";
var expected = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
</Project>
""";
XmlDocumentSyntax root = Parser.ParseText(original);
XmlElementSyntax syntaxToReplace = root
.Descendants()
.OfType<XmlElementSyntax>()
.Single(n => n.Name == "TargetFramework");
SyntaxNode textSyntaxToReplace = syntaxToReplace.Content.Single();
XmlTextSyntax content = SyntaxFactory.XmlText(SyntaxFactory.XmlTextLiteralToken("net9.0", null, null));
root = root.ReplaceNode(textSyntaxToReplace, content);
Assert.Equal(expected, root.ToFullString());
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Collections.Immutable (>= 8.0.0)
- System.Memory (>= 4.5.5)
-
net8.0
- System.Collections.Immutable (>= 8.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on GerardSmit.Language.Xml:
| Package | Downloads |
|---|---|
|
GerardSmit.Language.Xml.Roslyn
Conversions between GerardSmit.Language.Xml's TextSpan and Microsoft.CodeAnalysis.Text.TextSpan, for consumers turning parse results into editor ranges. Kept out of the core package so it stays dependency-free. |
GitHub repositories
This package is not used by any popular GitHub repositories.