|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
namespace System.Xml
{
// Specifies formatting options for XmlTextWriter.
public enum Formatting
{
// No special formatting is done (this is the default).
None,
//This option causes child elements to be indented using the Indentation and IndentChar properties.
// It only indents Element Content (http://www.w3.org/TR/1998/REC-xml-19980210#sec-element-content)
// and not Mixed Content (http://www.w3.org/TR/1998/REC-xml-19980210#sec-mixed-content)
// according to the XML 1.0 definitions of these terms.
Indented,
}
// Represents a writer that provides fast non-cached forward-only way of generating XML streams
// containing XML documents that conform to the W3CExtensible Markup Language (XML) 1.0 specification
// and the Namespaces in XML specification.
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public class XmlTextWriter : XmlWriter
{
//
// Private types
//
private enum NamespaceState
{
Uninitialized,
NotDeclaredButInScope,
DeclaredButNotWrittenOut,
DeclaredAndWrittenOut
}
private struct TagInfo
{
internal string? name;
internal string? prefix;
internal string defaultNs;
internal NamespaceState defaultNsState;
internal XmlSpace xmlSpace;
internal string? xmlLang;
internal int prevNsTop;
internal int prefixCount;
internal bool mixed; // whether to pretty print the contents of this element.
internal void Init(int nsTop)
{
name = null;
defaultNs = string.Empty;
defaultNsState = NamespaceState.Uninitialized;
xmlSpace = XmlSpace.None;
xmlLang = null;
prevNsTop = nsTop;
prefixCount = 0;
mixed = false;
}
}
private struct Namespace
{
internal string prefix;
internal string ns;
internal bool declared;
internal int prevNsIndex;
internal void Set(string prefix, string ns, bool declared)
{
this.prefix = prefix;
this.ns = ns;
this.declared = declared;
this.prevNsIndex = -1;
}
}
private enum SpecialAttr
{
None,
XmlSpace,
XmlLang,
XmlNs
};
// State machine is working through autocomplete
private enum State
{
Start,
Prolog,
PostDTD,
Element,
Attribute,
Content,
AttrOnly,
Epilog,
Error,
Closed,
}
private enum Token
{
PI,
Doctype,
Comment,
CData,
StartElement,
EndElement,
LongEndElement,
StartAttribute,
EndAttribute,
Content,
Base64,
RawData,
Whitespace,
Empty
}
//
// Fields
//
// output
private readonly TextWriter _textWriter = null!;
private readonly XmlTextEncoder _xmlEncoder = null!;
private readonly Encoding? _encoding;
// formatting
private Formatting _formatting;
private bool _indented; // perf - faster to check a boolean.
private int _indentation;
private char[] _indentChars;
private static readonly char[] s_defaultIndentChars = CreateDefaultIndentChars();
private static char[] CreateDefaultIndentChars()
{
var result = new char[IndentArrayLength];
Array.Fill(result, DefaultIndentChar);
return result;
}
// element stack
private TagInfo[] _stack;
private int _top;
// state machine for AutoComplete
private State[] _stateTable;
private State _currentState;
private Token _lastToken;
// Base64 content
private XmlTextWriterBase64Encoder? _base64Encoder;
// misc
private char _quoteChar;
private char _curQuoteChar;
private bool _namespaces;
private SpecialAttr _specialAttr;
private string? _prefixForXmlNs;
private bool _flush;
// namespaces
private Namespace[] _nsStack;
private int _nsTop;
private Dictionary<string, int>? _nsHashtable;
private bool _useNsHashtable;
//
// Constants and constant tables
//
private const int IndentArrayLength = 64;
private const char DefaultIndentChar = ' ';
private const int NamespaceStackInitialSize = 8;
#if DEBUG
private const int MaxNamespacesWalkCount = 3;
#else
private const int MaxNamespacesWalkCount = 16;
#endif
private static readonly string[] s_stateName = {
"Start",
"Prolog",
"PostDTD",
"Element",
"Attribute",
"Content",
"AttrOnly",
"Epilog",
"Error",
"Closed",
};
private static readonly string[] s_tokenName = {
"PI",
"Doctype",
"Comment",
"CData",
"StartElement",
"EndElement",
"LongEndElement",
"StartAttribute",
"EndAttribute",
"Content",
"Base64",
"RawData",
"Whitespace",
"Empty"
};
private static readonly State[] s_stateTableDefault = {
// State.Start State.Prolog State.PostDTD State.Element State.Attribute State.Content State.AttrOnly State.Epilog
//
/* Token.PI */ State.Prolog, State.Prolog, State.PostDTD, State.Content, State.Content, State.Content, State.Error, State.Epilog,
/* Token.Doctype */ State.PostDTD, State.PostDTD, State.Error, State.Error, State.Error, State.Error, State.Error, State.Error,
/* Token.Comment */ State.Prolog, State.Prolog, State.PostDTD, State.Content, State.Content, State.Content, State.Error, State.Epilog,
/* Token.CData */ State.Content, State.Content, State.Error, State.Content, State.Content, State.Content, State.Error, State.Epilog,
/* Token.StartElement */ State.Element, State.Element, State.Element, State.Element, State.Element, State.Element, State.Error, State.Element,
/* Token.EndElement */ State.Error, State.Error, State.Error, State.Content, State.Content, State.Content, State.Error, State.Error,
/* Token.LongEndElement */ State.Error, State.Error, State.Error, State.Content, State.Content, State.Content, State.Error, State.Error,
/* Token.StartAttribute */ State.AttrOnly, State.Error, State.Error, State.Attribute, State.Attribute, State.Error, State.Error, State.Error,
/* Token.EndAttribute */ State.Error, State.Error, State.Error, State.Error, State.Element, State.Error, State.Epilog, State.Error,
/* Token.Content */ State.Content, State.Content, State.Error, State.Content, State.Attribute, State.Content, State.Attribute, State.Epilog,
/* Token.Base64 */ State.Content, State.Content, State.Error, State.Content, State.Attribute, State.Content, State.Attribute, State.Epilog,
/* Token.RawData */ State.Prolog, State.Prolog, State.PostDTD, State.Content, State.Attribute, State.Content, State.Attribute, State.Epilog,
/* Token.Whitespace */ State.Prolog, State.Prolog, State.PostDTD, State.Content, State.Attribute, State.Content, State.Attribute, State.Epilog,
};
private static readonly State[] s_stateTableDocument = {
// State.Start State.Prolog State.PostDTD State.Element State.Attribute State.Content State.AttrOnly State.Epilog
//
/* Token.PI */ State.Error, State.Prolog, State.PostDTD, State.Content, State.Content, State.Content, State.Error, State.Epilog,
/* Token.Doctype */ State.Error, State.PostDTD, State.Error, State.Error, State.Error, State.Error, State.Error, State.Error,
/* Token.Comment */ State.Error, State.Prolog, State.PostDTD, State.Content, State.Content, State.Content, State.Error, State.Epilog,
/* Token.CData */ State.Error, State.Error, State.Error, State.Content, State.Content, State.Content, State.Error, State.Error,
/* Token.StartElement */ State.Error, State.Element, State.Element, State.Element, State.Element, State.Element, State.Error, State.Error,
/* Token.EndElement */ State.Error, State.Error, State.Error, State.Content, State.Content, State.Content, State.Error, State.Error,
/* Token.LongEndElement */ State.Error, State.Error, State.Error, State.Content, State.Content, State.Content, State.Error, State.Error,
/* Token.StartAttribute */ State.Error, State.Error, State.Error, State.Attribute, State.Attribute, State.Error, State.Error, State.Error,
/* Token.EndAttribute */ State.Error, State.Error, State.Error, State.Error, State.Element, State.Error, State.Error, State.Error,
/* Token.Content */ State.Error, State.Error, State.Error, State.Content, State.Attribute, State.Content, State.Error, State.Error,
/* Token.Base64 */ State.Error, State.Error, State.Error, State.Content, State.Attribute, State.Content, State.Error, State.Error,
/* Token.RawData */ State.Error, State.Prolog, State.PostDTD, State.Content, State.Attribute, State.Content, State.Error, State.Epilog,
/* Token.Whitespace */ State.Error, State.Prolog, State.PostDTD, State.Content, State.Attribute, State.Content, State.Error, State.Epilog,
};
//
// Constructors
//
private XmlTextWriter()
{
_namespaces = true;
_formatting = Formatting.None;
_indentation = 2;
_indentChars = s_defaultIndentChars;
// namespaces
_nsStack = new Namespace[NamespaceStackInitialSize];
_nsTop = -1;
// element stack
_stack = new TagInfo[10];
_top = 0; // 0 is an empty sentanial element
_stack[_top].Init(-1);
_quoteChar = '"';
_stateTable = s_stateTableDefault;
_currentState = State.Start;
_lastToken = Token.Empty;
}
// Creates an instance of the XmlTextWriter class using the specified stream.
public XmlTextWriter(Stream w, Encoding? encoding) : this()
{
_encoding = encoding;
if (encoding != null)
_textWriter = new StreamWriter(w, encoding);
else
_textWriter = new StreamWriter(w);
_xmlEncoder = new XmlTextEncoder(_textWriter);
_xmlEncoder.QuoteChar = _quoteChar;
}
// Creates an instance of the XmlTextWriter class using the specified file.
public XmlTextWriter(string filename, Encoding? encoding)
: this(new FileStream(filename, FileMode.Create,
FileAccess.Write, FileShare.Read), encoding)
{
}
// Creates an instance of the XmlTextWriter class using the specified TextWriter.
public XmlTextWriter(TextWriter w) : this()
{
_textWriter = w;
_encoding = w.Encoding;
_xmlEncoder = new XmlTextEncoder(w);
_xmlEncoder.QuoteChar = _quoteChar;
}
//
// XmlTextWriter properties
//
// Gets the XmlTextWriter base stream.
public Stream? BaseStream
{
get
{
if (_textWriter is StreamWriter streamWriter)
{
return streamWriter.BaseStream;
}
else
{
return null;
}
}
}
// Gets or sets a value indicating whether to do namespace support.
public bool Namespaces
{
get { return _namespaces; }
set
{
if (_currentState != State.Start)
throw new InvalidOperationException(SR.Xml_NotInWriteState);
_namespaces = value;
}
}
// Indicates how the output is formatted.
public Formatting Formatting
{
get { return _formatting; }
set { _formatting = value; _indented = value == Formatting.Indented; }
}
// Gets or sets how many IndentChars to write for each level in the hierarchy when Formatting is set to "Indented".
public int Indentation
{
get { return _indentation; }
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
_indentation = value;
}
}
// Gets or sets which character to use for indenting when Formatting is set to "Indented".
public char IndentChar
{
get { return _indentChars[0]; }
set
{
if (value == DefaultIndentChar)
{
_indentChars = s_defaultIndentChars;
return;
}
if (ReferenceEquals(_indentChars, s_defaultIndentChars))
{
_indentChars = new char[IndentArrayLength];
}
for (int i = 0; i < IndentArrayLength; i++)
{
_indentChars[i] = value;
}
}
}
// Gets or sets which character to use to quote attribute values.
public char QuoteChar
{
get { return _quoteChar; }
set
{
if (value != '"' && value != '\'')
{
throw new ArgumentException(SR.Xml_InvalidQuote);
}
_quoteChar = value;
_xmlEncoder.QuoteChar = value;
}
}
//
// XmlWriter implementation
//
// Writes out the XML declaration with the version "1.0".
public override void WriteStartDocument()
{
StartDocument(-1);
}
// Writes out the XML declaration with the version "1.0" and the standalone attribute.
public override void WriteStartDocument(bool standalone)
{
StartDocument(standalone ? 1 : 0);
}
// Closes any open elements or attributes and puts the writer back in the Start state.
public override void WriteEndDocument()
{
try
{
AutoCompleteAll();
if (_currentState != State.Epilog)
{
if (_currentState == State.Closed)
{
throw new ArgumentException(SR.Xml_ClosedOrError);
}
else
{
throw new ArgumentException(SR.Xml_NoRoot);
}
}
_stateTable = s_stateTableDefault;
_currentState = State.Start;
_lastToken = Token.Empty;
}
catch
{
_currentState = State.Error;
throw;
}
}
// Writes out the DOCTYPE declaration with the specified name and optional attributes.
public override void WriteDocType(string name, string? pubid, string? sysid, string? subset)
{
try
{
ValidateName(name, false);
AutoComplete(Token.Doctype);
_textWriter.Write("<!DOCTYPE ");
_textWriter.Write(name);
if (pubid != null)
{
_textWriter.Write($" PUBLIC {_quoteChar}");
_textWriter.Write(pubid);
_textWriter.Write($"{_quoteChar} {_quoteChar}");
_textWriter.Write(sysid);
_textWriter.Write(_quoteChar);
}
else if (sysid != null)
{
_textWriter.Write($" SYSTEM {_quoteChar}");
_textWriter.Write(sysid);
_textWriter.Write(_quoteChar);
}
if (subset != null)
{
_textWriter.Write("[");
_textWriter.Write(subset);
_textWriter.Write("]");
}
_textWriter.Write('>');
}
catch
{
_currentState = State.Error;
throw;
}
}
// Writes out the specified start tag and associates it with the given namespace and prefix.
public override void WriteStartElement(string? prefix, string localName, string? ns)
{
try
{
AutoComplete(Token.StartElement);
PushStack();
_textWriter.Write('<');
if (_namespaces)
{
// Propagate default namespace and mix model down the stack.
_stack[_top].defaultNs = _stack[_top - 1].defaultNs;
if (_stack[_top - 1].defaultNsState != NamespaceState.Uninitialized)
_stack[_top].defaultNsState = NamespaceState.NotDeclaredButInScope;
_stack[_top].mixed = _stack[_top - 1].mixed;
if (ns == null)
{
// use defined prefix
if (!string.IsNullOrEmpty(prefix) && (LookupNamespace(prefix) == -1))
{
throw new ArgumentException(SR.Xml_UndefPrefix);
}
}
else
{
if (prefix == null)
{
string? definedPrefix = FindPrefix(ns);
if (definedPrefix != null)
{
prefix = definedPrefix;
}
else
{
PushNamespace(null, ns, false); // new default
}
}
else if (prefix.Length == 0)
{
PushNamespace(null, ns, false); // new default
}
else
{
if (ns.Length == 0)
{
prefix = null;
}
VerifyPrefixXml(prefix, ns);
PushNamespace(prefix, ns, false); // define
}
}
_stack[_top].prefix = null;
if (!string.IsNullOrEmpty(prefix))
{
_stack[_top].prefix = prefix;
_textWriter.Write(prefix);
_textWriter.Write(':');
}
}
else
{
if (!string.IsNullOrEmpty(ns) || !string.IsNullOrEmpty(prefix))
{
throw new ArgumentException(SR.Xml_NoNamespaces);
}
}
_stack[_top].name = localName;
_textWriter.Write(localName);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Closes one element and pops the corresponding namespace scope.
public override void WriteEndElement()
{
InternalWriteEndElement(false);
}
// Closes one element and pops the corresponding namespace scope.
public override void WriteFullEndElement()
{
InternalWriteEndElement(true);
}
// Writes the start of an attribute.
public override void WriteStartAttribute(string? prefix, string localName, string? ns)
{
try
{
AutoComplete(Token.StartAttribute);
_specialAttr = SpecialAttr.None;
if (_namespaces)
{
if (prefix != null && prefix.Length == 0)
{
prefix = null;
}
if (ns == XmlReservedNs.NsXmlNs && prefix == null && localName != "xmlns")
{
prefix = "xmlns";
}
if (prefix == "xml")
{
if (localName == "lang")
{
_specialAttr = SpecialAttr.XmlLang;
}
else if (localName == "space")
{
_specialAttr = SpecialAttr.XmlSpace;
}
/* bug54408. to be fwd compatible we need to treat xml prefix as reserved
and not really insist on a specific value. Who knows in the future it
might be OK to say xml:blabla
else {
throw new ArgumentException(SR.Xml_InvalidPrefix);
}*/
}
else if (prefix == "xmlns")
{
if (XmlReservedNs.NsXmlNs != ns && ns != null)
{
throw new ArgumentException(SR.Xml_XmlnsBelongsToReservedNs);
}
if (string.IsNullOrEmpty(localName))
{
localName = prefix;
prefix = null;
_prefixForXmlNs = null;
}
else
{
_prefixForXmlNs = localName;
}
_specialAttr = SpecialAttr.XmlNs;
}
else if (prefix == null && localName == "xmlns")
{
if (XmlReservedNs.NsXmlNs != ns && ns != null)
{
// add the below line back in when DOM is fixed
throw new ArgumentException(SR.Xml_XmlnsBelongsToReservedNs);
}
_specialAttr = SpecialAttr.XmlNs;
_prefixForXmlNs = null;
}
else
{
if (ns == null)
{
// use defined prefix
if (prefix != null && (LookupNamespace(prefix) == -1))
{
throw new ArgumentException(SR.Xml_UndefPrefix);
}
}
else if (ns.Length == 0)
{
// empty namespace require null prefix
prefix = string.Empty;
}
else
{ // ns.Length != 0
VerifyPrefixXml(prefix, ns);
if (prefix != null && LookupNamespaceInCurrentScope(prefix) != -1)
{
prefix = null;
}
// Now verify prefix validity
string? definedPrefix = FindPrefix(ns);
if (definedPrefix != null && (prefix == null || prefix == definedPrefix))
{
prefix = definedPrefix;
}
else
{
prefix ??= GeneratePrefix(); // need a prefix if
PushNamespace(prefix, ns, false);
}
}
}
if (!string.IsNullOrEmpty(prefix))
{
_textWriter.Write(prefix);
_textWriter.Write(':');
}
}
else
{
if (!string.IsNullOrEmpty(ns) || !string.IsNullOrEmpty(prefix))
{
throw new ArgumentException(SR.Xml_NoNamespaces);
}
if (localName == "xml:lang")
{
_specialAttr = SpecialAttr.XmlLang;
}
else if (localName == "xml:space")
{
_specialAttr = SpecialAttr.XmlSpace;
}
}
_xmlEncoder.StartAttribute(_specialAttr != SpecialAttr.None);
_textWriter.Write(localName);
_textWriter.Write('=');
if (_curQuoteChar != _quoteChar)
{
_curQuoteChar = _quoteChar;
_xmlEncoder.QuoteChar = _quoteChar;
}
_textWriter.Write(_curQuoteChar);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Closes the attribute opened by WriteStartAttribute.
public override void WriteEndAttribute()
{
try
{
AutoComplete(Token.EndAttribute);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Writes out a <![CDATA[...]]> block containing the specified text.
public override void WriteCData(string? text)
{
try
{
AutoComplete(Token.CData);
if (null != text && text.Contains("]]>"))
{
throw new ArgumentException(SR.Xml_InvalidCDataChars);
}
_textWriter.Write("<![CDATA[");
if (null != text)
{
_xmlEncoder.WriteRawWithSurrogateChecking(text);
}
_textWriter.Write("]]>");
}
catch
{
_currentState = State.Error;
throw;
}
}
// Writes out a comment <!--...--> containing the specified text.
public override void WriteComment(string? text)
{
try
{
if (null != text && (text.Contains("--") || text.EndsWith('-')))
{
throw new ArgumentException(SR.Xml_InvalidCommentChars);
}
AutoComplete(Token.Comment);
_textWriter.Write("<!--");
if (null != text)
{
_xmlEncoder.WriteRawWithSurrogateChecking(text);
}
_textWriter.Write("-->");
}
catch
{
_currentState = State.Error;
throw;
}
}
// Writes out a processing instruction with a space between the name and text as follows: <?name text?>
public override void WriteProcessingInstruction(string name, string? text)
{
try
{
if (null != text && text.Contains("?>"))
{
throw new ArgumentException(SR.Xml_InvalidPiChars);
}
if (string.Equals(name, "xml", StringComparison.OrdinalIgnoreCase) && _stateTable == s_stateTableDocument)
{
throw new ArgumentException(SR.Xml_DupXmlDecl);
}
AutoComplete(Token.PI);
InternalWriteProcessingInstruction(name, text);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Writes out an entity reference as follows: "&"+name+";".
public override void WriteEntityRef(string name)
{
try
{
ValidateName(name, false);
AutoComplete(Token.Content);
_xmlEncoder.WriteEntityRef(name);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Forces the generation of a character entity for the specified Unicode character value.
public override void WriteCharEntity(char ch)
{
try
{
AutoComplete(Token.Content);
_xmlEncoder.WriteCharEntity(ch);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Writes out the given whitespace.
public override void WriteWhitespace(string? ws)
{
try
{
if (null == ws)
{
ws = string.Empty;
}
if (!XmlCharType.IsOnlyWhitespace(ws))
{
throw new ArgumentException(SR.Xml_NonWhitespace);
}
AutoComplete(Token.Whitespace);
_xmlEncoder.Write(ws);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Writes out the specified text content.
public override void WriteString(string? text)
{
try
{
if (null != text && text.Length != 0)
{
AutoComplete(Token.Content);
_xmlEncoder.Write(text);
}
}
catch
{
_currentState = State.Error;
throw;
}
}
// Writes out the specified surrogate pair as a character entity.
public override void WriteSurrogateCharEntity(char lowChar, char highChar)
{
try
{
AutoComplete(Token.Content);
_xmlEncoder.WriteSurrogateCharEntity(lowChar, highChar);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Writes out the specified text content.
public override void WriteChars(char[] buffer, int index, int count)
{
try
{
AutoComplete(Token.Content);
_xmlEncoder.Write(buffer, index, count);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Writes raw markup from the specified character buffer.
public override void WriteRaw(char[] buffer, int index, int count)
{
try
{
AutoComplete(Token.RawData);
_xmlEncoder.WriteRaw(buffer, index, count);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Writes raw markup from the specified character string.
public override void WriteRaw(string data)
{
try
{
AutoComplete(Token.RawData);
_xmlEncoder.WriteRawWithSurrogateChecking(data);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Encodes the specified binary bytes as base64 and writes out the resulting text.
public override void WriteBase64(byte[] buffer, int index, int count)
{
try
{
if (!_flush)
{
AutoComplete(Token.Base64);
}
_flush = true;
// No need for us to explicitly validate the args. The StreamWriter will do
// it for us.
if (null == _base64Encoder)
{
_base64Encoder = new XmlTextWriterBase64Encoder(_xmlEncoder);
}
// Encode will call WriteRaw to write out the encoded characters
_base64Encoder.Encode(buffer, index, count);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Encodes the specified binary bytes as binhex and writes out the resulting text.
public override void WriteBinHex(byte[] buffer, int index, int count)
{
try
{
AutoComplete(Token.Content);
BinHexEncoder.Encode(buffer, index, count, this);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Returns the state of the XmlWriter.
public override WriteState WriteState
{
get
{
switch (_currentState)
{
case State.Start:
return WriteState.Start;
case State.Prolog:
case State.PostDTD:
return WriteState.Prolog;
case State.Element:
return WriteState.Element;
case State.Attribute:
case State.AttrOnly:
return WriteState.Attribute;
case State.Content:
case State.Epilog:
return WriteState.Content;
case State.Error:
return WriteState.Error;
case State.Closed:
return WriteState.Closed;
default:
Debug.Fail($"Unexpected state {_currentState}");
return WriteState.Error;
}
}
}
// Closes the XmlWriter and the underlying stream/TextWriter.
public override void Close()
{
try
{
AutoCompleteAll();
}
catch
{ // never fail
}
finally
{
_currentState = State.Closed;
_textWriter.Dispose();
}
}
// Flushes whatever is in the buffer to the underlying stream/TextWriter and flushes the underlying stream/TextWriter.
public override void Flush()
{
_textWriter.Flush();
}
// Writes out the specified name, ensuring it is a valid Name according to the XML specification
// (http://www.w3.org/TR/1998/REC-xml-19980210#NT-Name
public override void WriteName(string name)
{
try
{
AutoComplete(Token.Content);
InternalWriteName(name, false);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Writes out the specified namespace-qualified name by looking up the prefix that is in scope for the given namespace.
public override void WriteQualifiedName(string localName, string? ns)
{
try
{
AutoComplete(Token.Content);
if (_namespaces)
{
if (!string.IsNullOrEmpty(ns) && ns != _stack[_top].defaultNs)
{
string? prefix = FindPrefix(ns);
if (prefix == null)
{
if (_currentState != State.Attribute)
{
throw new ArgumentException(SR.Format(SR.Xml_UndefNamespace, ns));
}
prefix = GeneratePrefix();
PushNamespace(prefix, ns, false);
}
if (prefix.Length != 0)
{
InternalWriteName(prefix, true);
_textWriter.Write(':');
}
}
}
else if (!string.IsNullOrEmpty(ns))
{
throw new ArgumentException(SR.Xml_NoNamespaces);
}
InternalWriteName(localName, true);
}
catch
{
_currentState = State.Error;
throw;
}
}
// Returns the closest prefix defined in the current namespace scope for the specified namespace URI.
public override string? LookupPrefix(string ns)
{
ArgumentException.ThrowIfNullOrEmpty(ns);
string? s = FindPrefix(ns);
if (s == null && ns == _stack[_top].defaultNs)
{
s = string.Empty;
}
return s;
}
// Gets an XmlSpace representing the current xml:space scope.
public override XmlSpace XmlSpace
{
get
{
for (int i = _top; i > 0; i--)
{
XmlSpace xs = _stack[i].xmlSpace;
if (xs != XmlSpace.None)
return xs;
}
return XmlSpace.None;
}
}
// Gets the current xml:lang scope.
public override string? XmlLang
{
get
{
for (int i = _top; i > 0; i--)
{
string? xlang = _stack[i].xmlLang;
if (xlang != null)
return xlang;
}
return null;
}
}
// Writes out the specified name, ensuring it is a valid NmToken
// according to the XML specification (http://www.w3.org/TR/1998/REC-xml-19980210#NT-Name).
public override void WriteNmToken(string name)
{
try
{
AutoComplete(Token.Content);
ArgumentException.ThrowIfNullOrEmpty(name);
if (!ValidateNames.IsNmtokenNoNamespaces(name))
{
throw new ArgumentException(SR.Format(SR.Xml_InvalidNameChars, name));
}
_textWriter.Write(name);
}
catch
{
_currentState = State.Error;
throw;
}
}
//
// Private implementation methods
//
private void StartDocument(int standalone)
{
try
{
if (_currentState != State.Start)
{
throw new InvalidOperationException(SR.Xml_NotTheFirst);
}
_stateTable = s_stateTableDocument;
_currentState = State.Prolog;
StringBuilder bufBld = new StringBuilder(128);
bufBld.Append($"version={_quoteChar}1.0{_quoteChar}");
if (_encoding != null)
{
bufBld.Append(" encoding=");
bufBld.Append(_quoteChar);
bufBld.Append(_encoding.WebName);
bufBld.Append(_quoteChar);
}
if (standalone >= 0)
{
bufBld.Append(" standalone=");
bufBld.Append(_quoteChar);
bufBld.Append(standalone == 0 ? "no" : "yes");
bufBld.Append(_quoteChar);
}
InternalWriteProcessingInstruction("xml", bufBld.ToString());
}
catch
{
_currentState = State.Error;
throw;
}
}
private void AutoComplete(Token token)
{
if (_currentState == State.Closed)
{
throw new InvalidOperationException(SR.Xml_Closed);
}
else if (_currentState == State.Error)
{
throw new InvalidOperationException(SR.Format(SR.Xml_WrongToken, s_tokenName[(int)token], s_stateName[(int)State.Error]));
}
State newState = _stateTable[(int)token * 8 + (int)_currentState];
if (newState == State.Error)
{
throw new InvalidOperationException(SR.Format(SR.Xml_WrongToken, s_tokenName[(int)token], s_stateName[(int)_currentState]));
}
switch (token)
{
case Token.Doctype:
if (_indented && _currentState != State.Start)
{
Indent(false);
}
break;
case Token.StartElement:
case Token.Comment:
case Token.PI:
case Token.CData:
if (_currentState == State.Attribute)
{
WriteEndAttributeQuote();
WriteEndStartTag(false);
}
else if (_currentState == State.Element)
{
WriteEndStartTag(false);
}
if (token == Token.CData)
{
_stack[_top].mixed = true;
}
else if (_indented && _currentState != State.Start)
{
Indent(false);
}
break;
case Token.EndElement:
case Token.LongEndElement:
if (_flush)
{
FlushEncoders();
}
if (_currentState == State.Attribute)
{
WriteEndAttributeQuote();
}
if (_currentState == State.Content)
{
token = Token.LongEndElement;
}
else
{
WriteEndStartTag(token == Token.EndElement);
}
if (s_stateTableDocument == _stateTable && _top == 1)
{
newState = State.Epilog;
}
break;
case Token.StartAttribute:
if (_flush)
{
FlushEncoders();
}
if (_currentState == State.Attribute)
{
WriteEndAttributeQuote();
_textWriter.Write(' ');
}
else if (_currentState == State.Element)
{
_textWriter.Write(' ');
}
break;
case Token.EndAttribute:
if (_flush)
{
FlushEncoders();
}
WriteEndAttributeQuote();
break;
case Token.Whitespace:
case Token.Content:
case Token.RawData:
case Token.Base64:
if (token != Token.Base64 && _flush)
{
FlushEncoders();
}
if (_currentState == State.Element && _lastToken != Token.Content)
{
WriteEndStartTag(false);
}
if (newState == State.Content)
{
_stack[_top].mixed = true;
}
break;
default:
throw new InvalidOperationException(SR.Xml_InvalidOperation);
}
_currentState = newState;
_lastToken = token;
}
private void AutoCompleteAll()
{
if (_flush)
{
FlushEncoders();
}
while (_top > 0)
{
WriteEndElement();
}
}
private static readonly char[] s_selfClosingTagOpen = new char[] { '<', '/' };
private void InternalWriteEndElement(bool longFormat)
{
try
{
if (_top <= 0)
{
throw new InvalidOperationException(SR.Xml_NoStartTag);
}
// if we are in the element, we need to close it.
AutoComplete(longFormat ? Token.LongEndElement : Token.EndElement);
if (_lastToken == Token.LongEndElement)
{
if (_indented)
{
Indent(true);
}
_textWriter.Write(s_selfClosingTagOpen);
if (_namespaces && _stack[_top].prefix != null)
{
_textWriter.Write(_stack[_top].prefix);
_textWriter.Write(':');
}
_textWriter.Write(_stack[_top].name);
_textWriter.Write('>');
}
// pop namespaces
int prevNsTop = _stack[_top].prevNsTop;
if (_useNsHashtable && prevNsTop < _nsTop)
{
PopNamespaces(prevNsTop + 1, _nsTop);
}
_nsTop = prevNsTop;
_top--;
}
catch
{
_currentState = State.Error;
throw;
}
}
private static readonly char[] s_closeTagEnd = new char[] { ' ', '/', '>' };
private void WriteEndStartTag(bool empty)
{
_xmlEncoder.StartAttribute(false);
for (int i = _nsTop; i > _stack[_top].prevNsTop; i--)
{
if (!_nsStack[i].declared)
{
_textWriter.Write(" xmlns:");
_textWriter.Write(_nsStack[i].prefix);
_textWriter.Write('=');
_textWriter.Write(_quoteChar);
_xmlEncoder.Write(_nsStack[i].ns);
_textWriter.Write(_quoteChar);
}
}
// Default
if ((_stack[_top].defaultNs != _stack[_top - 1].defaultNs) &&
(_stack[_top].defaultNsState == NamespaceState.DeclaredButNotWrittenOut))
{
_textWriter.Write(" xmlns=");
_textWriter.Write(_quoteChar);
_xmlEncoder.Write(_stack[_top].defaultNs);
_textWriter.Write(_quoteChar);
_stack[_top].defaultNsState = NamespaceState.DeclaredAndWrittenOut;
}
_xmlEncoder.EndAttribute();
if (empty)
{
_textWriter.Write(s_closeTagEnd);
}
else
{
_textWriter.Write('>');
}
}
private void WriteEndAttributeQuote()
{
if (_specialAttr != SpecialAttr.None)
{
// Ok, now to handle xmlspace, etc.
HandleSpecialAttribute();
}
_xmlEncoder.EndAttribute();
_textWriter.Write(_curQuoteChar);
}
private void Indent(bool beforeEndElement)
{
// pretty printing.
if (_top == 0)
{
_textWriter.WriteLine();
}
else if (!_stack[_top].mixed)
{
_textWriter.WriteLine();
int i = (beforeEndElement ? _top - 1 : _top) * _indentation;
if (i <= _indentChars.Length)
{
_textWriter.Write(_indentChars, 0, i);
}
else
{
while (i > 0)
{
_textWriter.Write(_indentChars, 0, Math.Min(i, _indentChars.Length));
i -= _indentChars.Length;
}
}
}
}
// pushes new namespace scope, and returns generated prefix, if one
// was needed to resolve conflicts.
private void PushNamespace(string? prefix, string ns, bool declared)
{
if (XmlReservedNs.NsXmlNs == ns)
{
throw new ArgumentException(SR.Xml_CanNotBindToReservedNamespace);
}
if (prefix == null)
{
switch (_stack[_top].defaultNsState)
{
case NamespaceState.DeclaredButNotWrittenOut:
Debug.Assert(declared, "Unexpected situation!!");
// the first namespace that the user gave us is what we
// like to keep.
break;
case NamespaceState.Uninitialized:
case NamespaceState.NotDeclaredButInScope:
// we now got a brand new namespace that we need to remember
_stack[_top].defaultNs = ns;
break;
default:
Debug.Fail("Should have never come here");
return;
}
_stack[_top].defaultNsState = (declared ? NamespaceState.DeclaredAndWrittenOut : NamespaceState.DeclaredButNotWrittenOut);
}
else
{
if (prefix.Length != 0 && ns.Length == 0)
{
throw new ArgumentException(SR.Xml_PrefixForEmptyNs);
}
int existingNsIndex = LookupNamespace(prefix);
if (existingNsIndex != -1 && _nsStack[existingNsIndex].ns == ns)
{
// it is already in scope.
if (declared)
{
_nsStack[existingNsIndex].declared = true;
}
}
else
{
// see if prefix conflicts for the current element
if (declared)
{
if (existingNsIndex != -1 && existingNsIndex > _stack[_top].prevNsTop)
{
_nsStack[existingNsIndex].declared = true; // old one is silenced now
}
}
AddNamespace(prefix, ns, declared);
}
}
}
private void AddNamespace(string prefix, string ns, bool declared)
{
int nsIndex = ++_nsTop;
if (nsIndex == _nsStack.Length)
{
Namespace[] newStack = new Namespace[nsIndex * 2];
Array.Copy(_nsStack, newStack, nsIndex);
_nsStack = newStack;
}
_nsStack[nsIndex].Set(prefix, ns, declared);
if (_useNsHashtable)
{
AddToNamespaceHashtable(nsIndex);
}
else if (nsIndex == MaxNamespacesWalkCount)
{
// add all
_nsHashtable = new Dictionary<string, int>();
_useNsHashtable = true;
for (int i = 0; i <= nsIndex; i++)
{
AddToNamespaceHashtable(i);
}
}
}
private void AddToNamespaceHashtable(int namespaceIndex)
{
Debug.Assert(_useNsHashtable);
Debug.Assert(_nsHashtable != null);
string prefix = _nsStack[namespaceIndex].prefix;
int existingNsIndex;
if (_nsHashtable.TryGetValue(prefix, out existingNsIndex))
{
_nsStack[namespaceIndex].prevNsIndex = existingNsIndex;
}
_nsHashtable[prefix] = namespaceIndex;
}
private void PopNamespaces(int indexFrom, int indexTo)
{
Debug.Assert(_useNsHashtable);
Debug.Assert(_nsHashtable != null);
for (int i = indexTo; i >= indexFrom; i--)
{
Debug.Assert(_nsHashtable.ContainsKey(_nsStack[i].prefix));
if (_nsStack[i].prevNsIndex == -1)
{
_nsHashtable.Remove(_nsStack[i].prefix);
}
else
{
_nsHashtable[_nsStack[i].prefix] = _nsStack[i].prevNsIndex;
}
}
}
private string GeneratePrefix()
{
int temp = _stack[_top].prefixCount++ + 1;
return string.Create(CultureInfo.InvariantCulture, $"d{_top:d}p{temp:d}");
}
private void InternalWriteProcessingInstruction(string name, string? text)
{
_textWriter.Write("<?");
ValidateName(name, false);
_textWriter.Write(name);
_textWriter.Write(' ');
if (null != text)
{
_xmlEncoder.WriteRawWithSurrogateChecking(text);
}
_textWriter.Write("?>");
}
private int LookupNamespace(string prefix)
{
if (_useNsHashtable)
{
Debug.Assert(_nsHashtable != null);
int nsIndex;
if (_nsHashtable.TryGetValue(prefix, out nsIndex))
{
return nsIndex;
}
}
else
{
for (int i = _nsTop; i >= 0; i--)
{
if (_nsStack[i].prefix == prefix)
{
return i;
}
}
}
return -1;
}
private int LookupNamespaceInCurrentScope(string prefix)
{
if (_useNsHashtable)
{
Debug.Assert(_nsHashtable != null);
int nsIndex;
if (_nsHashtable.TryGetValue(prefix, out nsIndex))
{
if (nsIndex > _stack[_top].prevNsTop)
{
return nsIndex;
}
}
}
else
{
for (int i = _nsTop; i > _stack[_top].prevNsTop; i--)
{
if (_nsStack[i].prefix == prefix)
{
return i;
}
}
}
return -1;
}
private string? FindPrefix(string ns)
{
for (int i = _nsTop; i >= 0; i--)
{
if (_nsStack[i].ns == ns)
{
if (LookupNamespace(_nsStack[i].prefix) == i)
{
return _nsStack[i].prefix;
}
}
}
return null;
}
// There are three kind of strings we write out - Name, LocalName and Prefix.
// Both LocalName and Prefix can be represented with NCName == false and Name
// can be represented as NCName == true
private void InternalWriteName(string name, bool isNCName)
{
ValidateName(name, isNCName);
_textWriter.Write(name);
}
// This method is used for validation of the DOCTYPE, processing instruction and entity names plus names
// written out by the user via WriteName and WriteQualifiedName.
// Unfortunatelly the names of elements and attributes are not validated by the XmlTextWriter.
// Also this method does not check wheather the character after ':' is a valid start name character. It accepts
// all valid name characters at that position. This can't be changed because of backwards compatibility.
private void ValidateName(string name, bool isNCName)
{
ArgumentException.ThrowIfNullOrEmpty(name);
int nameLength = name.Length;
// Namespaces supported
if (_namespaces)
{
// We can't use ValidateNames.ParseQName here because of backwards compatibility bug we need to preserve.
// The bug is that the character after ':' is validated only as a NCName characters instead of NCStartName.
int colonPosition = -1;
// Parse NCName (may be prefix, may be local name)
int position = ValidateNames.ParseNCName(name);
Continue:
if (position == nameLength)
{
return;
}
// we have prefix:localName
if (name[position] == ':')
{
if (!isNCName)
{
// first colon in qname
if (colonPosition == -1)
{
// make sure it is not the first or last characters
if (position > 0 && position + 1 < nameLength)
{
colonPosition = position;
// Because of the back-compat bug (described above) parse the rest as Nmtoken
position++;
position += ValidateNames.ParseNmtoken(name, position);
goto Continue;
}
}
}
}
}
// Namespaces not supported
else
{
if (ValidateNames.IsNameNoNamespaces(name))
{
return;
}
}
throw new ArgumentException(SR.Format(SR.Xml_InvalidNameChars, name));
}
private void HandleSpecialAttribute()
{
string value = _xmlEncoder.AttributeValue;
switch (_specialAttr)
{
case SpecialAttr.XmlLang:
_stack[_top].xmlLang = value;
break;
case SpecialAttr.XmlSpace:
// validate XmlSpace attribute
switch (value.AsSpan().Trim(XmlConvert.WhitespaceChars))
{
case "default":
_stack[_top].xmlSpace = XmlSpace.Default;
break;
case "preserve":
_stack[_top].xmlSpace = XmlSpace.Preserve;
break;
default:
throw new ArgumentException(SR.Format(SR.Xml_InvalidXmlSpace, value));
}
break;
case SpecialAttr.XmlNs:
VerifyPrefixXml(_prefixForXmlNs, value);
PushNamespace(_prefixForXmlNs, value, true);
break;
}
}
private static void VerifyPrefixXml(string? prefix, string ns)
{
if (prefix != null &&
prefix.Equals("xml", StringComparison.OrdinalIgnoreCase) &&
XmlReservedNs.NsXml != ns)
{
throw new ArgumentException(SR.Xml_InvalidPrefix);
}
}
private void PushStack()
{
if (_top == _stack.Length - 1)
{
TagInfo[] na = new TagInfo[_stack.Length + 10];
if (_top > 0) Array.Copy(_stack, na, _top + 1);
_stack = na;
}
_top++; // Move up stack
_stack[_top].Init(_nsTop);
}
private void FlushEncoders()
{
// The Flush will call WriteRaw to write out the rest of the encoded characters
_base64Encoder?.Flush();
_flush = false;
}
}
}
|