CrystalXmlWriter<TRune, TWriter>

Namespace: SnowBank.Data.Xml · struct

Implements: ICrystalXmlEmitter

XML text emitter that produces a byte-exact output, over either UTF-16 or UTF-8 output

Remarks

Replicates, byte for byte, the output produced by DataContractSerializer writing through an XmlWriter (settings CheckCharacters = false, OmitXmlDeclaration = true, no indentation) followed by an invalid-character filter.

Byte-compatibility rules, all measured against that reference implementation and all in force under the default CrystalXmlSettings (compact, self-closing, no declaration):

  • No XML declaration: the document starts with the root element. A caller that sets WriteXmlDeclaration gets one <?xml version="1.0" encoding="..."?> line before the root, naming whatever encoding the finished output is in; version and encoding only, no standalone.
  • A prefix and an xmlns declaration appear exactly where a caller names a namespace, and nowhere else, so a caller that names none produces a document with no prefix and no declaration in it. This writer decides what a prefix is called and where a missing declaration goes (see the section on namespaces below); it does not decide whether an element has a namespace.
  • Text content escapes &``<``>, and only those; the quote characters stay raw.
  • In text content, every line ending (\r\n, a lone \r, a lone \n) becomes a raw\r\n, never a character reference; a TAB stays raw. This normalization is fixed and does not depend on NewLine, which only ever governs the structural whitespace Indented inserts between elements, never a value's own text.
  • Attribute values escape &``<``" but not>.
  • Attribute values write TAB, LF and CR as the character references &#x9;, &#xA; and &#xD;; no line-ending normalization happens inside an attribute.
  • C0 control characters, which XML 1.0 forbids outright, are dropped. This is a deliberate deviation: the reference writer emits them as character references under CheckCharacters = false and its post-filter lets those through (they are plain ASCII once escaped), producing a format that no conformant reader can parse. Set StrictControlCharacters to reproduce that defect exactly.
  • Unpaired surrogate halves are dropped; a valid surrogate pair passes through whole (and becomes a single 4-byte sequence on the UTF-8 core, never CESU-8).
  • U+FFFE and U+FFFF are dropped, like the reference filter does.
  • An element with no content self-closes as <Name />, with a space before the slash; writing an empty string as content forces the expanded form <Name></Name>. A caller that sets EmptyElementStyle to Paired gets the expanded form for the genuinely empty case too, so every element with no content reads <Name></Name>.
  • When Indented is set, every child element starts on its own line, indented one TAB per nesting level, and an element's end tag gets its own line too, but only when that element opened at least one child element. An element whose content is text (or nothing) always stays on one line: indentation never touches a value. The writer is forward-only and cannot retract output it already wrote, so an element whose first child element precedes its own text keeps the indentation already written before that child; the child text values themselves are never altered.

Namespaces and prefixes. A caller names namespaces; this writer names prefixes. Two rules, both chosen so that a document reads like the one the reference implementation writes:

  • The XML Schema instance namespace takes the prefix i and the object-graph serialization namespace takes z, the spellings the reference implementation uses for them. Every other namespace takes d{depth}p{n}, where depth is the depth of the element carrying the declaration, counted from 1 at the root, and n counts the declarations on that element.
  • A namespace that is used and not in scope is declared on the element that is currently open, which is the first element that uses it. The first namespace declared in a document with no default namespace in scope becomes the DEFAULT namespace, so a root writes <Library xmlns="urn:biblio"> and its unprefixed children inherit it. A caller that wants a declaration higher up asks for it with WriteNamespaceDeclaration.

An alias is not part of a document's meaning: a reader resolves a prefix through the declarations in scope and matches the namespace, so two documents that differ only in their aliases and in where the declarations sit read back identically. That is what lets this writer choose both.

Always pass this struct by ref, and abandon the writer variable you constructed it from. The emitter holds the element state and the destination writer inline, so a copy silently loses every write made through it. The constructor copies the writer into Writer: with a pooled sink (for instance SliceWriter) the caller's variable becomes a second claim on the same buffer, so disposing, resetting or reusing it is double ownership and, after the emitter's buffer has grown or been returned, a use-after-return. Everything after construction, including reading the output and disposing the sink, goes through Writer:

var sink = new ValueStringWriter();
var emitter = new CrystalXmlWriter<char, ValueStringWriter>(ref sink);
Emit(ref emitter);                       // always by ref
string xml = emitter.Writer.ToString();  // read back HERE, never from `sink`

The TRune divergence is concentrated in a handful of leaf primitives (ASCII literal, escaped text, precomputed name, character reference); everything above them is written once. The typeof(TRune) == typeof(char) tests are folded away by the JIT, so neither core pays for the other.

Constructors

CrystalXmlWriter<TRune, TWriter>

CrystalXmlWriter<TRune, TWriter>(ref TWriter writer, bool strictControlCharacters = false, CrystalXmlSettings settings = null, string declarationEncoding = null)

Constructs an emitter writing into writer

  • writer — Destination buffer writer, consumed by this constructor: it is copied into Writer, and the caller's variable must not be read, disposed or reused afterwards. The ref only avoids copying a large struct as an argument; no aliasing is established (a ref field is impossible on the netstandard2.0 and net8.0 targets).
  • strictControlCharacters — When true, reproduce the legacy character-reference treatment of C0 control characters instead of dropping them. See StrictControlCharacters.
  • settings — Writer-level output options: Indented, NewLine, EmptyElementStyle and WriteXmlDeclaration. Defaults to General, which reproduces today's compact, self-closing, declaration-less output.
  • declarationEncoding — IANA name written into the declaration's encoding attribute, when WriteXmlDeclaration is set. Defaults to utf-16 on the char core and utf-8 on the byte core; a caller transcoding the finished byte buffer to another encoding passes that encoding's own name here instead.

Properties

Depth

int Depth { get; }

Number of elements currently open

Methods

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 .

Fields

StrictControlCharacters

readonly bool StrictControlCharacters

When true, C0 control characters are emitted as character references instead of being dropped

Reproduces a defect of the legacy format, and produces XML that no conformant reader accepts. Only the certification harness, which compares against captured legacy output, should turn this on.

Writer

TWriter Writer

Destination writer, held inline; the only live view of the output

This is where the caller reads the output back and disposes the sink; the writer variable passed to the constructor is a dead copy: see the remarks on .