Meziantou.Framework.Language.Xml 4.0.6

Prefix Reserved
dotnet add package Meziantou.Framework.Language.Xml --version 4.0.6
                    
NuGet\Install-Package Meziantou.Framework.Language.Xml -Version 4.0.6
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Meziantou.Framework.Language.Xml" Version="4.0.6" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Meziantou.Framework.Language.Xml" Version="4.0.6" />
                    
Directory.Packages.props
<PackageReference Include="Meziantou.Framework.Language.Xml" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Meziantou.Framework.Language.Xml --version 4.0.6
                    
#r "nuget: Meziantou.Framework.Language.Xml, 4.0.6"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Meziantou.Framework.Language.Xml@4.0.6
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Meziantou.Framework.Language.Xml&version=4.0.6
                    
Install as a Cake Addin
#tool nuget:?package=Meziantou.Framework.Language.Xml&version=4.0.6
                    
Install as a Cake Tool

Meziantou.Framework.Language.Xml

An immutable XML syntax tree that keeps every character of the text it was parsed from — comments, whitespace, attribute quoting and all — and lets you edit it without reformatting anything you did not touch.

It is modelled on Roslyn. If you have used Microsoft.CodeAnalysis, everything here will look familiar: a SyntaxTree over a SourceText, nodes and tokens with spans, trivia, SyntaxKind, visitors, rewriters, and annotations. On top of that it speaks XPath, so a node can be found the way XML people expect to find one.

Parsing never throws and never gives up. Whatever the text says, the tree reproduces it exactly, and anything wrong with it is reported through GetDiagnostics().

The types are in the Meziantou.Framework.Language.Xml namespace, and the shared ones (SyntaxAnnotation, TextChange, …) in Meziantou.Framework.Language.

const string Xml = "<root><item version='1.0.0' /></root>";

var tree = XmlSyntaxTree.ParseText(Xml);

// The text comes back exactly as it went in.
Assert.Equal(tree.GetText().Text, tree.GetRoot().ToFullString());

// Find a node with XPath, and edit it without touching anything else.
var updated = tree.GetRoot().ReplaceNode("//item/@version", node => ((XmlAttributeSyntax)node).WithValue("2.0.0"));

Assert.Equal("<root><item version='2.0.0' /></root>", updated.ToFullString());

The shape of a tree

XmlDocumentSyntax
├── Nodes : SyntaxList<XmlNodeSyntax>
│   └── XmlElementSyntax
│       ├── StartTag : XmlElementStartTagSyntax  →  LessThanToken, NameToken, Attributes, GreaterThanToken
│       ├── Content  : SyntaxList<XmlNodeSyntax>
│       └── EndTag   : XmlElementEndTagSyntax?   →  LessThanSlashToken, NameToken, GreaterThanToken
└── EndOfFileToken

A node in a document or in an element's content is an XmlElementSyntax, XmlEmptyElementSyntax, XmlTextSyntax, XmlCommentSyntax, XmlCDataSectionSyntax, XmlDeclarationSyntax, XmlProcessingInstructionSyntax, XmlDocumentTypeSyntax, or XmlSkippedTextSyntax for text the parser could not use.

An element written as one self-closing tag is its own node type. <item/> is an XmlEmptyElementSyntax, which has attributes but no content and no end tag; <item></item> is an XmlElementSyntax. Both carry Name, Attributes, and GetAttribute(name).

Everything is a token, including the punctuation. <, >, />, =, and the quotes around an attribute value are each a token of their own, so an edit rebuilds the tree rather than splicing text:

var attribute = (XmlAttributeSyntax)tree.GetRoot().SelectSingleSyntaxNode("//item/@version")!;

Assert.Equal("version", attribute.NameToken.Text);
Assert.Equal("'", attribute.StartQuoteToken.Text);
Assert.Equal("1.0.0", attribute.ValueToken.Text);
Assert.Equal("1.0.0", attribute.Value);

Which whitespace is trivia

Inside a tag, whitespace is trivia; between tags it is content. That is what XML says: the space in <book id = '1'> carries no meaning, while the newline between two elements is character data an application may care about.

var tree = XmlSyntaxTree.ParseText("<root>\n  <book id = '1' />\n</root>");
var book = (XmlEmptyElementSyntax)tree.GetRoot().SelectSingleSyntaxNode("//book")!;

Assert.Equal(" ", book.Attributes[0].NameToken.LeadingTrivia.ToFullString());
Assert.Equal(" ", book.Attributes[0].EqualsToken.LeadingTrivia.ToFullString());

var root = (XmlElementSyntax)tree.GetRoot().Nodes[0];
Assert.Equal("\n  ", ((XmlTextSyntax)root.Content[0]).Text);

Editing without reformatting

An edit rebuilds only the path from the changed node up to the root. Everything else is carried over as-is, so the rest of the document is untouched — and the result is the same type you started with, so no cast is needed.

var tree = XmlSyntaxTree.ParseText("<root><a>1</a><b>2</b></root>");
var root = (XmlElementSyntax)tree.GetRoot().Nodes[0];
var b = root.Content.OfType<XmlElementSyntax>().Single(element => element.Name == "b");

XmlDocumentSyntax updated = tree.GetRoot().ReplaceNode(b, b.WithInnerText("two"));

Assert.Equal("<root><a>1</a><b>two</b></root>", updated.ToFullString());

// <a> was not touched, so it is the very same node.
Assert.True(root.Content[0].IsIncrementallyIdenticalTo(((XmlElementSyntax)updated.Nodes[0]).Content[0]));

There is a WithX method for every slot of every node, SyntaxFactory to build nodes from scratch, and RemoveNode with SyntaxRemoveOptions to take one out.

Nothing is carried over onto a replacement, including the trivia in front of the node being replaced. Ask for it with WithTriviaFrom when you want it.

Finding a node

With XPath, including namespaces:

var tree = XmlSyntaxTree.ParseText("<root xmlns:pkg='urn:test'><pkg:item pkg:version='1.0.0' /></root>");
var namespaces = new XmlNamespaceManager(new NameTable());
namespaces.AddNamespace("pkg", "urn:test");

var item = tree.GetRoot().SelectSingleSyntaxNode("//pkg:item", namespaces);
var version = tree.GetRoot().SelectSingleSyntaxNode("//pkg:item/@pkg:version", namespaces);

Assert.IsType<XmlEmptyElementSyntax>(item);
Assert.Equal("1.0.0", ((XmlAttributeSyntax)version!).Value);

SelectNodes returns XPathNavigators, SelectSyntaxNodes returns the syntax nodes behind them, and CreateNavigator() gives an XPathNavigator over the whole document. Or, without XPath: root.DescendantNodes(), DescendantTokens(), DescendantTrivia(), FindToken(position), FindNode(span).

Finding a node again after an edit

An annotation is a marker you attach to a node and find again in the tree an edit produced, wherever it ended up:

var root = XmlSyntaxTree.ParseText("<root><a>1</a><b>2</b></root>").GetRoot();
var a = root.DescendantNodes().OfType<XmlElementSyntax>().Single(element => element.Name == "a");

var marker = new SyntaxAnnotation();
var marked = root.ReplaceNode(a, a.WithAdditionalAnnotations(marker));

// Something else entirely: replace <b>, which rebuilds the path from <b> up to the root
var b = marked.DescendantNodes().OfType<XmlElementSyntax>().Single(element => element.Name == "b");
var edited = marked.ReplaceNode(b, b.WithInnerText("two"));

var found = edited.GetAnnotatedNodes(marker).Single();
Assert.Equal("<a>1</a>", found.ToFullString());

Diagnostics

Nothing is thrown for bad input; it is reported instead, in the order it appears in the text, with a location you can turn into line and character positions.

The checks are those of a non-validating XML processor: every well-formedness constraint of XML 1.0 (Fifth Edition) and of Namespaces in XML 1.0. A document System.Xml can read produces no diagnostic, with two deliberate exceptions where the specifications are stricter than it is — an encoding name that is not an EncName, and an element name with the xmlns prefix — and one where they are more permissive: any 1.x version is accepted.

var messages = new List<string>();
foreach (var diagnostic in XmlSyntaxTree.ParseText("<root>\n  <item>\n</root>").GetDiagnostics())
{
    var line = diagnostic.Location.GetLineSpan().Start;
    messages.Add($"{diagnostic.Id} ({line.Line},{line.Character}): {diagnostic.Message}");
}

Assert.Equal(["XML0001 (1,2): Missing end tag for 'item'."], messages);
Id Reported for
XML0001 An element with no end tag
XML0002 An unexpected, mismatched, or malformed end tag
XML0003 A character XML does not allow, such as U+0001, U+FFFE, or a lone surrogate
XML0004 A malformed or invalid character reference, an & that starts no reference, or an undeclared entity
XML0005 A < in an attribute value, or ]]> in character data
XML0006 A duplicate attribute, including two that differ only by a prefix bound to the same namespace
XML0007 An unterminated XML declaration
XML0008 An unterminated comment
XML0009 An unterminated CDATA section
XML0010 An invalid start tag or attribute: text a tag cannot use, a missing >, a missing or unquoted value, or attributes not separated by whitespace
XML0011 An unterminated document type declaration
XML0012 An unterminated processing instruction
XML0013 An invalid or misplaced XML declaration
XML0014 A document that does not have exactly one root element, or holds text, a CDATA section, or a document type declaration where it cannot
XML0015 A comment containing --
XML0016 A processing instruction with no target, the reserved target xml, or no whitespace after its target
XML0017 A namespace error: an undeclared prefix, a name that is not a qualified name, or a reserved prefix misused
XML0018 A malformed document type declaration

An entity reference counts as declared when the internal subset declares it, or when the document has an external subset or a parameter entity reference, which could declare it where the parser cannot see.

Recovery

The parser keeps the shape the author most likely meant, so one mistake produces one diagnostic rather than a cascade:

  • An end tag that matches an element further up closes the elements left open in between, each reported as missing its end tag. One that matches no open element is kept as XmlSkippedTextSyntax.
  • A start tag missing its > still opens its element, with a missing GreaterThanToken.
  • Text a tag cannot use — <a @ b="1"> — is kept as SkippedTextTrivia on the next token, and the element and the rest of its attributes are read as usual.
  • An attribute value that meets what looks like the next tag before its closing quote stops there.
  • A < that starts no markup, as in a < b, is skipped on its own rather than swallowing the text up to the next >.
  • An unterminated XML declaration, document type declaration, or end tag stops at the next <.

Walking a tree

XmlSyntaxWalker visits a node and everything below it; XmlSyntaxVisitor dispatches on kind without recursing; XmlSyntaxRewriter builds a new tree from an old one.

private sealed class ElementCounter : XmlSyntaxWalker
{
    public int Count { get; private set; }

    public override void VisitElement(XmlElementSyntax node)
    {
        Count++;
        base.VisitElement(node);
    }
}
var counter = new ElementCounter();
counter.Visit(XmlSyntaxTree.ParseText("<root><a><b></b></a><c /></root>").GetRoot());

// <c /> is an XmlEmptyElementSyntax, so VisitElement does not count it
Assert.Equal(3, counter.Count);

Formatting

Formatter.Format lays a document out again, with XmlFormattingOptions controlling indentation, line breaks, attribute wrapping, the spacing inside tags, and whether elements written on one line stay that way. It is the one thing here that does reformat, and it is opt-in. It never changes text: an element that contains text keeps its content as written, and so does an element under xml:space="preserve".

Editing by text

var tree = XmlSyntaxTree.ParseText("<root><a /></root>");
var updated = tree.WithChanges(new TextChange(new TextSpan(6, 5), "<b />"));

Assert.Equal("<root><b /></root>", updated.GetRoot().ToFullString());

Coming from version 3

Version 4 moved the tree onto the same model Roslyn uses, which changed most of the API.

Version 3 Version 4
XmlSyntaxKind SyntaxKind
node.Kind node.Kind()
XmlSyntaxToken, XmlSyntaxTrivia, XmlSyntaxNodeOrToken (classes) SyntaxToken, SyntaxTrivia, SyntaxNodeOrToken from Meziantou.Framework.Language (structs)
XmlSyntaxAnnotation SyntaxAnnotation from Meziantou.Framework.Language
tree.Root, tree.Text, tree.SourceText, tree.Diagnostics tree.GetRoot(), tree.GetText(), tree.GetDiagnostics()
document.ChildNodes document.Nodes
element.IsSelfClosing a self-closing element is an XmlEmptyElementSyntax
XmlEndTagSyntax XmlElementEndTagSyntax, reached through element.EndTag
element.Tokens, node.Tokens a named property per token, or ChildTokens()
element.StartTagText element.StartTag, a node of its own
new XmlTextSyntax(text) and the other constructors SyntaxFactory.XmlText(text) and friends
SyntaxFactory.Element/Attribute/Text/Comment/… SyntaxFactory.XmlElement/XmlAttribute/XmlText/XmlComment/…
SyntaxFactory.Element(name, attributes, content, isSelfClosing) XmlElement(name, attributes, content) or XmlEmptyElement(name, attributes)
token.WithText(text) SyntaxFactory.Token(kind, text).WithTriviaFrom(token)
node.WithLeadingTrivia(...) doing string surgery the shared WithLeadingTrivia, which sets the first token's trivia
root.ReplaceNode(...) re-parsing the whole document root.ReplaceNode(...) rebuilding only what changed
XmlSyntaxRewriter overrides returning XmlSyntaxNode? returning SyntaxNode?
XmlSyntaxVisitor.DefaultVisit recursing it does nothing; derive from XmlSyntaxWalker to walk a tree

New in version 4: every character is a token, so Span and FullSpan differ where a token carries trivia; WithX on every slot; SyntaxAnnotation; RemoveNode/SyntaxRemoveOptions; XmlSyntaxWalker; FindToken/ FindNode; and structural sharing — an edit keeps every node it did not touch.

Notes

Replacing a node, token or trivium rebuilds only the spine from it to the root; WithChanges reparses, because an edit expressed as text can change how everything after it reads.

Nodes are shared between the trees an edit produces, so holding several versions of a document costs little more than holding one.

This is a parser, not a validator. It reads the shape of a document without applying a DTD or a schema.

The syntax keeps text exactly as written, references included; XmlAttributeSyntax.Value and XmlTextSyntax.Value are what an XML processor reads it as. Character references and the five predefined entities are resolved, line breaks are normalized to \n, and an attribute value turns each tab and line break into a space. Other entities are left as written, since resolving one means reading the DTD. WithValue escapes a tab or a line break as a character reference, so the value it sets is the value read back. Names do follow XML's own NameStartChar and NameChar productions, so a name may hold a combining mark, a middle dot, or a character from outside the basic plane, and a Unicode letter XML leaves out — ª, say — does not start one.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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.  net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Meziantou.Framework.Language.Xml:

Package Downloads
Meziantou.Framework.DependencyScanning

Find dependencies in source files. Support multiple package managers such as NuGet, npm, Docker, PyPi, and so on

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.0.6 145 10/28/2026
4.0.5 118 10/21/2026
4.0.4 138 10/18/2026
4.0.3 272 9/30/2026
4.0.2 109 9/27/2026
4.0.1 199 9/15/2026
4.0.0 162 9/10/2026
3.0.0 101 9/9/2026
2.0.2 174 9/6/2026
2.0.1 291 8/29/2026
2.0.0 658 7/5/2026
1.0.1 146 7/1/2026