CrystalXDocumentEmitter

Namespace: SnowBank.Data.Xml · struct

Implements: ICrystalXmlEmitter

XML infoset emitter that builds an in-memory XDocument

Remarks

One of the two infoset sinks for ICrystalXmlEmitter (the other wraps a XmlWriter in CrystalXmlWriterEmitter). Unlike CrystalXmlWriter, this emitter makes no byte-exactness promise: only a node tree that is infoset-equivalent to what a conformant parser would build from the same logical content.

The format core's control-character sanitization does NOT apply here. The characters XML 1.0 cannot represent (C0 controls, unpaired surrogate halves, U+FFFE/U+FFFF) land in the node tree verbatim, producing a document no conformant parser would accept back; content that may carry them must be sanitized before it reaches the emitter.

Elements are built bottom-up: WriteStartElement pushes an unparented XElement onto a stack, attributes and text accumulate on the element at the top, and WriteEndElement pops it and appends it to its parent (or records it as the document root). The attribute-ordering precondition from ICrystalXmlEmitter is still asserted in DEBUG, for consistency with the other emitters.

The null-vs-empty distinction survives into the DOM. Adding an empty string flips IsEmpty to false (matching a parse of <x></x>), while an element that never received content stays IsEmpty (matching <x />). So WriteText and WriteRawAscii must add nothing at all for null, not even an empty node.

Line-ending normalization is applied by hand.Add stores a string verbatim, without the XML 1.0 section 2.11 end-of-line normalization (\r\n or lone \r becomes \n) that Parse applies. This emitter replicates it itself, so that XNode.DeepEquals(emitter.ToDocument(), XDocument.Parse(referenceOutput)) holds for any text containing a line break.

Namespace declarations are the DOM's business, not this emitter's. An element and an attribute each carry their namespace in their XName, and ToString derives the declarations and the prefixes a document needs from those names when it serializes the tree. So WriteNamespaceDeclaration and WriteDefaultNamespaceDeclaration do nothing here: a caller placing a declaration high in a document is expressing where the TEXT should carry it, and this emitter produces no text. The one exception is a qualified name inside an attribute value, whose namespace the DOM cannot see; see WriteQNameAttribute.

Not thread-safe, and, like every ICrystalXmlEmitter, must be passed by ref: Root is a plain field assigned once by the final WriteEndElement, so a copy taken before that point would never see it.

Constructors

CrystalXDocumentEmitter

CrystalXDocumentEmitter()

Constructs an emitter that accumulates events into a fresh, empty document

Methods

ToDocument

XDocument ToDocument()

Returns the document built from the events written so far

Returns: A new XDocument wrapping the completed root element

WriteAttribute

void WriteAttribute(in CrystalXmlName name, ReadOnlySpan<char> value)

Appends an attribute to the start tag that is currently open

  • name — Name of the attribute, in both its text and UTF-8 representations
  • value — Value of the attribute, escaped by the implementation

Only valid between a and the first content event or of that element.

void WriteAttribute(in CrystalXmlName name, in CrystalXmlNamespace ns, ReadOnlySpan<char> value)

Appends an attribute in an explicit namespace to the start tag that is currently open

  • name — Local name of the attribute, in both its text and UTF-8 representations
  • ns — Namespace of the attribute, which takes precedence over the one name carries
  • value — Value of the attribute, escaped by the implementation

An attribute is never in the default namespace: a namespaced attribute always takes a prefix, which is why this overload and the one above are not the same call with an empty namespace.

WriteDefaultNamespaceDeclaration

void WriteDefaultNamespaceDeclaration(in CrystalXmlNamespace ns)

Declares a namespace as the DEFAULT namespace on the start tag that is currently open

  • ns — Namespace to declare, or the empty namespace to cancel an inherited default

The default namespace covers the ELEMENTS of the open element's subtree, never their attributes. An element whose own namespace is the default therefore needs no prefix, which is what makes a document readable.

It does not change the namespace of the element that carries it. An element gets its namespace when it is opened, and nothing afterwards moves it. So an element that is itself in the namespace it declares says so at WriteStartElement, which also declares it when nothing in scope binds it. The text emitter would produce the same bytes either way, but the infoset implementations have committed the name by then, and a call that means one thing on one sink and another on the next is worth no bytes.

WriteEndElement

void WriteEndElement(in CrystalXmlName name)

Closes the element that is currently open

  • name — Name of the element being closed, in both its text and UTF-8 representations

The name is passed back by the caller (the generated code always knows it statically) so that implementations do not have to allocate a stack of names.

WriteNamespaceDeclaration

void WriteNamespaceDeclaration(in CrystalXmlNamespace ns)

Declares a namespace on the start tag that is currently open, under a prefix the implementation picks

  • ns — Namespace to declare

The declaration covers the open element and everything inside it, so this is how a caller places one declaration above several uses instead of letting each use declare its own. A collection wrapper naming its items' namespace, or an element whose nested contract lives in another namespace than its own name, are the two shapes that need it: without them each child declares the same namespace again.

Asking for a namespace that is already in scope writes NOTHING. The call means "this namespace is usable inside this element", and an inherited declaration already says so; binding a second alias to one namespace would only add bytes. So a caller can ask unconditionally, which is what lets one generated body serve both a root element (whose namespace its caller already declared) and a nested one (whose caller did not).

Only valid while the start tag is still open.

WriteQNameAttribute

void WriteQNameAttribute(in CrystalXmlName name, in CrystalXmlName value)

Appends an attribute in an explicit namespace whose VALUE is a qualified name

  • name — Local name of the attribute (type, on the DataContract format)
  • value — The qualified name the attribute carries: its local name, and the namespace it belongs to

Separate from because a qualified name is a namespace and a local name, not text: the implementation resolves the namespace to a prefix in scope and writes prefix:Local, so nothing formats a string to describe a name the implementation already holds.

void WriteQNameAttribute(in CrystalXmlName name, in CrystalXmlNamespace ns, in CrystalXmlName value)

Appends an attribute in an explicit namespace whose VALUE is a qualified name

  • name — Local name of the attribute (type, on the DataContract format)
  • ns — Namespace of the attribute, which takes precedence over the one name carries
  • value — The qualified name the attribute carries: its local name, and the namespace it belongs to

Separate from because a qualified name is a namespace and a local name, not text: the implementation resolves the namespace to a prefix in scope and writes prefix:Local, so nothing formats a string to describe a name the implementation already holds.

WriteRawAscii

void WriteRawAscii(ReadOnlySpan<char> ascii)

Appends content that is already known to be valid, unescaped ASCII

  • ascii — Pre-validated ASCII content: a formatted number, a date, a base64 payload, ...

This bypasses the escaper entirely, which is the point: these forms cannot contain a character that would need escaping. Passing arbitrary user text here would emit malformed XML.

Like WriteText, this counts as content.

void WriteRawAscii(string ascii)

Appends pre-validated ASCII content, treating null as "no content at all"

  • ascii — Pre-validated ASCII content. null writes nothing, leaving the element free to self-close; an empty string counts as content and forces the expanded form.

On the interface for the same reason as : an interface-constrained caller must get the same format as a caller holding the concrete struct.

WriteStartElement

void WriteStartElement(in CrystalXmlName name)

Opens a new element

  • name — Name of the element, in both its text and UTF-8 representations

The start tag stays open until content is written or the element is closed, so that can still append attributes to it.

void WriteStartElement(in CrystalXmlName name, in CrystalXmlNamespace ns)

Opens a new element in an explicit namespace

  • name — Local name of the element, in both its text and UTF-8 representations
  • ns — Namespace of the element, which takes precedence over the one name carries

This overload exists so that one cached name can be written in more than one namespace: the item name string is in the collections namespace under a List and in the XML Schema namespace as the value of a type annotation, and caching it twice would be caching the same three bytes twice.

WriteText

void WriteText(ReadOnlySpan<char> text)

Appends text content to the element that is currently open, escaping it as needed

  • text — Raw text; the implementation escapes it

Writing text always counts as content, so an empty span still forces the expanded form instead of the self-closing one.

void WriteText(string text)

Appends text content to the element that is currently open, treating null as "no content at all"

  • text — Raw text. null writes nothing, leaving the element free to self-close as <Name />; an empty string counts as content and forces the expanded <Name></Name> form.

This member is on the interface, and not merely an overload on the concrete emitters, because generated bodies only see interface members through the where TEmitter : struct, ICrystalXmlEmitter constraint. Were it absent, emitter.WriteText(someString) would bind through the implicit string conversion, turning a into an empty span and flipping the output from to .