2794 lines
79 KiB
C#
2794 lines
79 KiB
C#
/*
|
|
*
|
|
* An XmlReader implementation for loading SGML (including HTML) converting it
|
|
* to well formed XML, by adding missing quotes, empty attribute values, ignoring
|
|
* duplicate attributes, case folding on tag names, adding missing closing tags
|
|
* based on SGML DTD information, and so on.
|
|
*
|
|
* Copyright (c) 2002 Microsoft Corporation. All rights reserved. (Chris Lovett)
|
|
*
|
|
*/
|
|
|
|
/*
|
|
*
|
|
* Copyright (c) 2007-2013 MindTouch. All rights reserved.
|
|
* www.mindtouch.com oss@mindtouch.com
|
|
*
|
|
* For community documentation and downloads visit wiki.developer.mindtouch.com;
|
|
* please review the licensing section.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Xml;
|
|
|
|
namespace ReadSharp.Ports.Sgml
|
|
{
|
|
/// <summary>
|
|
/// SGML is case insensitive, so here you can choose between converting
|
|
/// to lower case or upper case tags. "None" means that the case is left
|
|
/// alone, except that end tags will be folded to match the start tags.
|
|
/// </summary>
|
|
public enum CaseFolding
|
|
{
|
|
/// <summary>
|
|
/// Do not convert case, except for converting end tags to match start tags.
|
|
/// </summary>
|
|
None,
|
|
|
|
/// <summary>
|
|
/// Convert tags to upper case.
|
|
/// </summary>
|
|
ToUpper,
|
|
|
|
/// <summary>
|
|
/// Convert tags to lower case.
|
|
/// </summary>
|
|
ToLower
|
|
}
|
|
|
|
#if PORTABLE
|
|
/// <summary>
|
|
/// System.Xml.WhitespaceHandling is not available in .NETPortable.
|
|
/// </summary>
|
|
public enum WhitespaceHandling
|
|
{
|
|
All,
|
|
None,
|
|
Significant
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// This stack maintains a high water mark for allocated objects so the client
|
|
/// can reuse the objects in the stack to reduce memory allocations, this is
|
|
/// used to maintain current state of the parser for element stack, and attributes
|
|
/// in each element.
|
|
/// </summary>
|
|
internal class HWStack
|
|
{
|
|
private object[] m_items;
|
|
private int m_size;
|
|
private int m_count;
|
|
private int m_growth;
|
|
|
|
/// <summary>
|
|
/// Initialises a new instance of the HWStack class.
|
|
/// </summary>
|
|
/// <param name="growth">The amount to grow the stack space by, if more space is needed on the stack.</param>
|
|
public HWStack(int growth)
|
|
{
|
|
this.m_growth = growth;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The number of items currently in the stack.
|
|
/// </summary>
|
|
public int Count
|
|
{
|
|
get
|
|
{
|
|
return this.m_count;
|
|
}
|
|
set
|
|
{
|
|
this.m_count = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The size (capacity) of the stack.
|
|
/// </summary>
|
|
[SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Kept for potential future usage.")]
|
|
public int Size
|
|
{
|
|
get
|
|
{
|
|
return this.m_size;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the item at the requested index or null if index is out of bounds
|
|
/// </summary>
|
|
/// <param name="i">The index of the item to retrieve.</param>
|
|
/// <returns>The item at the requested index or null if index is out of bounds.</returns>
|
|
public object this[int i]
|
|
{
|
|
get
|
|
{
|
|
return (i >= 0 && i < this.m_size) ? m_items[i] : null;
|
|
}
|
|
set
|
|
{
|
|
this.m_items[i] = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes and returns the item at the top of the stack
|
|
/// </summary>
|
|
/// <returns>The item at the top of the stack.</returns>
|
|
public object Pop()
|
|
{
|
|
this.m_count--;
|
|
if (this.m_count > 0)
|
|
{
|
|
return m_items[this.m_count - 1];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pushes a new slot at the top of the stack.
|
|
/// </summary>
|
|
/// <returns>The object at the top of the stack.</returns>
|
|
/// <remarks>
|
|
/// This method tries to reuse a slot, if it returns null then
|
|
/// the user has to call the other Push method.
|
|
/// </remarks>
|
|
public object Push()
|
|
{
|
|
if (this.m_count == this.m_size)
|
|
{
|
|
int newsize = this.m_size + this.m_growth;
|
|
object[] newarray = new object[newsize];
|
|
if (this.m_items != null)
|
|
Array.Copy(this.m_items, newarray, this.m_size);
|
|
|
|
this.m_size = newsize;
|
|
this.m_items = newarray;
|
|
}
|
|
return m_items[this.m_count++];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove a specific item from the stack.
|
|
/// </summary>
|
|
/// <param name="i">The index of the item to remove.</param>
|
|
[SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Kept for potential future usage.")]
|
|
public void RemoveAt(int i)
|
|
{
|
|
this.m_items[i] = null;
|
|
Array.Copy(this.m_items, i + 1, this.m_items, i, this.m_count - i - 1);
|
|
this.m_count--;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This class represents an attribute. The AttDef is assigned
|
|
/// from a validation process, and is used to provide default values.
|
|
/// </summary>
|
|
internal class Attribute
|
|
{
|
|
internal string Name; // the atomized name.
|
|
internal AttDef DtdType; // the AttDef of the attribute from the SGML DTD.
|
|
internal char QuoteChar; // the quote character used for the attribute value.
|
|
private string m_literalValue; // the attribute value
|
|
|
|
/// <summary>
|
|
/// Attribute objects are reused during parsing to reduce memory allocations,
|
|
/// hence the Reset method.
|
|
/// </summary>
|
|
public void Reset(string name, string value, char quote)
|
|
{
|
|
this.Name = name;
|
|
this.m_literalValue = value;
|
|
this.QuoteChar = quote;
|
|
this.DtdType = null;
|
|
}
|
|
|
|
public string Value
|
|
{
|
|
get
|
|
{
|
|
if (this.m_literalValue != null)
|
|
return this.m_literalValue;
|
|
if (this.DtdType != null)
|
|
return this.DtdType.Default;
|
|
return null;
|
|
}
|
|
/* set
|
|
{
|
|
this.m_literalValue = value;
|
|
}*/
|
|
}
|
|
|
|
public bool IsDefault
|
|
{
|
|
get
|
|
{
|
|
return (this.m_literalValue == null);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This class models an XML node, an array of elements in scope is maintained while parsing
|
|
/// for validation purposes, and these Node objects are reused to reduce object allocation,
|
|
/// hence the reset method.
|
|
/// </summary>
|
|
internal class Node
|
|
{
|
|
internal XmlNodeType NodeType;
|
|
internal string Value;
|
|
internal XmlSpace Space;
|
|
internal string XmlLang;
|
|
internal bool IsEmpty;
|
|
internal string Name;
|
|
internal ElementDecl DtdType; // the DTD type found via validation
|
|
internal State CurrentState;
|
|
internal bool Simulated; // tag was injected into result stream.
|
|
HWStack attributes = new HWStack(10);
|
|
|
|
/// <summary>
|
|
/// Attribute objects are reused during parsing to reduce memory allocations,
|
|
/// hence the Reset method.
|
|
/// </summary>
|
|
public void Reset(string name, XmlNodeType nt, string value)
|
|
{
|
|
this.Value = value;
|
|
this.Name = name;
|
|
this.NodeType = nt;
|
|
this.Space = XmlSpace.None;
|
|
this.XmlLang = null;
|
|
this.IsEmpty = true;
|
|
this.attributes.Count = 0;
|
|
this.DtdType = null;
|
|
}
|
|
|
|
public Attribute AddAttribute(string name, string value, char quotechar, bool caseInsensitive)
|
|
{
|
|
Attribute a;
|
|
// check for duplicates!
|
|
for (int i = 0, n = this.attributes.Count; i < n; i++)
|
|
{
|
|
a = (Attribute)this.attributes[i];
|
|
if (string.Equals(a.Name, name, caseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
// This code makes use of the high water mark for attribute objects,
|
|
// and reuses exisint Attribute objects to avoid memory allocation.
|
|
a = (Attribute)this.attributes.Push();
|
|
if (a == null)
|
|
{
|
|
a = new Attribute();
|
|
this.attributes[this.attributes.Count - 1] = a;
|
|
}
|
|
a.Reset(name, value, quotechar);
|
|
return a;
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Kept for potential future usage.")]
|
|
public void RemoveAttribute(string name)
|
|
{
|
|
for (int i = 0, n = this.attributes.Count; i < n; i++)
|
|
{
|
|
Attribute a = (Attribute)this.attributes[i];
|
|
if (string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
this.attributes.RemoveAt(i);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
public void CopyAttributes(Node n)
|
|
{
|
|
for (int i = 0, len = n.attributes.Count; i < len; i++)
|
|
{
|
|
Attribute a = (Attribute)n.attributes[i];
|
|
Attribute na = this.AddAttribute(a.Name, a.Value, a.QuoteChar, false);
|
|
na.DtdType = a.DtdType;
|
|
}
|
|
}
|
|
|
|
public int AttributeCount
|
|
{
|
|
get
|
|
{
|
|
return this.attributes.Count;
|
|
}
|
|
}
|
|
|
|
public int GetAttribute(string name)
|
|
{
|
|
for (int i = 0, n = this.attributes.Count; i < n; i++)
|
|
{
|
|
Attribute a = (Attribute)this.attributes[i];
|
|
if (string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
public Attribute GetAttribute(int i)
|
|
{
|
|
if (i >= 0 && i < this.attributes.Count)
|
|
{
|
|
Attribute a = (Attribute)this.attributes[i];
|
|
return a;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
internal enum State
|
|
{
|
|
Initial, // The initial state (Read has not been called yet)
|
|
Markup, // Expecting text or markup
|
|
EndTag, // Positioned on an end tag
|
|
Attr, // Positioned on an attribute
|
|
AttrValue, // Positioned in an attribute value
|
|
Text, // Positioned on a Text node.
|
|
PartialTag, // Positioned on a text node, and we have hit a start tag
|
|
AutoClose, // We are auto-closing tags (this is like State.EndTag), but end tag was generated
|
|
CData, // We are on a CDATA type node, eg. <scipt> where we have special parsing rules.
|
|
PartialText,
|
|
PseudoStartTag, // we pushed a pseudo-start tag, need to continue with previous start tag.
|
|
Eof
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// SgmlReader is an XmlReader API over any SGML document (including built in
|
|
/// support for HTML).
|
|
/// </summary>
|
|
public class SgmlReader : XmlReader
|
|
{
|
|
/// <summary>
|
|
/// The value returned when a namespace is queried and none has been specified.
|
|
/// </summary>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1705", Justification = "SgmlReader's standards for constants are different to Microsoft's and in line with older C++ style constants naming conventions. Visually, constants using this style are more easily identifiable as such.")]
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707", Justification = "SgmlReader's standards for constants are different to Microsoft's and in line with older C++ style constants naming conventions. Visually, constants using this style are more easily identifiable as such.")]
|
|
public const string UNDEFINED_NAMESPACE = "#unknown";
|
|
|
|
private SgmlDtd m_dtd;
|
|
private Entity m_current;
|
|
private State m_state;
|
|
private char m_partial;
|
|
private string m_endTag;
|
|
private HWStack m_stack;
|
|
private Node m_node; // current node (except for attributes)
|
|
// Attributes are handled separately using these members.
|
|
private Attribute m_a;
|
|
private int m_apos; // which attribute are we positioned on in the collection.
|
|
private Uri m_baseUri;
|
|
private StringBuilder m_sb;
|
|
private StringBuilder m_name;
|
|
private TextWriter m_log;
|
|
private bool m_foundRoot;
|
|
private bool m_ignoreDtd;
|
|
|
|
// autoclose support
|
|
private Node m_newnode;
|
|
private int m_poptodepth;
|
|
private int m_rootCount;
|
|
private bool m_isHtml;
|
|
private string m_rootElementName;
|
|
|
|
private string m_href;
|
|
private Entity m_lastError;
|
|
private string m_proxy;
|
|
private TextReader m_inputStream;
|
|
private string m_syslit;
|
|
private string m_pubid;
|
|
private string m_subset;
|
|
private string m_docType;
|
|
private WhitespaceHandling m_whitespaceHandling;
|
|
private CaseFolding m_folding = CaseFolding.None;
|
|
private bool m_stripDocType = true;
|
|
//private string m_startTag;
|
|
private Dictionary<string, string> unknownNamespaces = new Dictionary<string, string>();
|
|
|
|
/// <summary>
|
|
/// Initialises a new instance of the SgmlReader class.
|
|
/// </summary>
|
|
public SgmlReader()
|
|
{
|
|
Init();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialises a new instance of the SgmlReader class with an existing <see cref="XmlNameTable"/>, which is NOT used.
|
|
/// </summary>
|
|
/// <param name="nt">The nametable to use.</param>
|
|
public SgmlReader(XmlNameTable nt)
|
|
{
|
|
Init();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Specify the SgmlDtd object directly. This allows you to cache the Dtd and share
|
|
/// it across multipl SgmlReaders. To load a DTD from a URL use the SystemLiteral property.
|
|
/// </summary>
|
|
public SgmlDtd Dtd
|
|
{
|
|
get
|
|
{
|
|
if (this.m_dtd == null)
|
|
{
|
|
LazyLoadDtd(this.m_baseUri);
|
|
}
|
|
|
|
return this.m_dtd;
|
|
}
|
|
set
|
|
{
|
|
this.m_dtd = value;
|
|
}
|
|
}
|
|
|
|
private void LazyLoadDtd(Uri baseUri)
|
|
{
|
|
if (this.m_dtd == null && !this.m_ignoreDtd)
|
|
{
|
|
if (string.IsNullOrEmpty(this.m_syslit))
|
|
{
|
|
if (this.m_docType != null && StringUtilities.EqualsIgnoreCase(this.m_docType, "html"))
|
|
{
|
|
Assembly a = typeof(SgmlReader).GetTypeInfo().Assembly;
|
|
string name = a.FullName.Split(',')[0] + ".Html.dtd";
|
|
Stream stm = a.GetManifestResourceStream(name);
|
|
if (stm != null)
|
|
{
|
|
StreamReader sr = new StreamReader(stm);
|
|
this.m_dtd = SgmlDtd.Parse(baseUri, "HTML", sr, null, this.m_proxy, null);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (baseUri != null)
|
|
{
|
|
baseUri = new Uri(baseUri, this.m_syslit);
|
|
}
|
|
else if (this.m_baseUri != null)
|
|
{
|
|
baseUri = new Uri(this.m_baseUri, this.m_syslit);
|
|
}
|
|
else
|
|
{
|
|
#if PORTABLE
|
|
throw new InvalidOperationException("Cannot load DTD without specifying base URI.");
|
|
#else
|
|
baseUri = new Uri(new Uri(Directory.GetCurrentDirectory() + "/"), this.m_syslit);
|
|
#endif
|
|
}
|
|
this.m_dtd = SgmlDtd.Parse(baseUri, this.m_docType, this.m_pubid, baseUri.AbsoluteUri, this.m_subset, this.m_proxy, null);
|
|
}
|
|
}
|
|
|
|
if (this.m_dtd != null && this.m_dtd.Name != null)
|
|
{
|
|
switch (this.CaseFolding)
|
|
{
|
|
case CaseFolding.ToUpper:
|
|
this.m_rootElementName = this.m_dtd.Name.ToUpperInvariant();
|
|
break;
|
|
case CaseFolding.ToLower:
|
|
this.m_rootElementName = this.m_dtd.Name.ToLowerInvariant();
|
|
break;
|
|
default:
|
|
this.m_rootElementName = this.m_dtd.Name;
|
|
break;
|
|
}
|
|
|
|
this.m_isHtml = StringUtilities.EqualsIgnoreCase(this.m_dtd.Name, "html");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The name of root element specified in the DOCTYPE tag.
|
|
/// </summary>
|
|
public string DocType
|
|
{
|
|
get
|
|
{
|
|
return this.m_docType;
|
|
}
|
|
set
|
|
{
|
|
this.m_docType = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The root element of the document.
|
|
/// </summary>
|
|
public string RootElementName
|
|
{
|
|
get
|
|
{
|
|
return m_rootElementName;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The PUBLIC identifier in the DOCTYPE tag
|
|
/// </summary>
|
|
public string PublicIdentifier
|
|
{
|
|
get
|
|
{
|
|
return this.m_pubid;
|
|
}
|
|
set
|
|
{
|
|
this.m_pubid = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The SYSTEM literal in the DOCTYPE tag identifying the location of the DTD.
|
|
/// </summary>
|
|
public string SystemLiteral
|
|
{
|
|
get
|
|
{
|
|
return this.m_syslit;
|
|
}
|
|
set
|
|
{
|
|
this.m_syslit = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DTD internal subset in the DOCTYPE tag
|
|
/// </summary>
|
|
public string InternalSubset
|
|
{
|
|
get
|
|
{
|
|
return this.m_subset;
|
|
}
|
|
set
|
|
{
|
|
this.m_subset = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The input stream containing SGML data to parse.
|
|
/// You must specify this property or the Href property before calling Read().
|
|
/// </summary>
|
|
public TextReader InputStream
|
|
{
|
|
get
|
|
{
|
|
return this.m_inputStream;
|
|
}
|
|
set
|
|
{
|
|
this.m_inputStream = value;
|
|
Init();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sometimes you need to specify a proxy server in order to load data via HTTP
|
|
/// from outside the firewall. For example: "itgproxy:80".
|
|
/// </summary>
|
|
public string WebProxy
|
|
{
|
|
get
|
|
{
|
|
return this.m_proxy;
|
|
}
|
|
set
|
|
{
|
|
this.m_proxy = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The base Uri is used to resolve relative Uri's like the SystemLiteral and
|
|
/// Href properties. This is a method because BaseURI is a read-only
|
|
/// property on the base XmlReader class.
|
|
/// </summary>
|
|
public void SetBaseUri(string uri)
|
|
{
|
|
this.m_baseUri = new Uri(uri);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Specify the location of the input SGML document as a URL.
|
|
/// </summary>
|
|
public string Href
|
|
{
|
|
get
|
|
{
|
|
return this.m_href;
|
|
}
|
|
set
|
|
{
|
|
this.m_href = value;
|
|
Init();
|
|
if (this.m_baseUri == null)
|
|
{
|
|
if (this.m_href.IndexOf("://") > 0)
|
|
{
|
|
this.m_baseUri = new Uri(this.m_href);
|
|
}
|
|
else
|
|
{
|
|
#if PORTABLE
|
|
throw new ArgumentException("Relative URLs are not supported.");
|
|
#else
|
|
this.m_baseUri = new Uri("file:///" + Directory.GetCurrentDirectory() + "//");
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether to strip out the DOCTYPE tag from the output (default true)
|
|
/// </summary>
|
|
public bool StripDocType
|
|
{
|
|
get
|
|
{
|
|
return this.m_stripDocType;
|
|
}
|
|
set
|
|
{
|
|
this.m_stripDocType = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether to ignore any DTD reference.
|
|
/// </summary>
|
|
/// <value><c>true</c> if DTD references should be ignored; otherwise, <c>false</c>.</value>
|
|
public bool IgnoreDtd
|
|
{
|
|
get { return m_ignoreDtd; }
|
|
set { m_ignoreDtd = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The case conversion behaviour while processing tags.
|
|
/// </summary>
|
|
public CaseFolding CaseFolding
|
|
{
|
|
get
|
|
{
|
|
return this.m_folding;
|
|
}
|
|
set
|
|
{
|
|
this.m_folding = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// DTD validation errors are written to this stream.
|
|
/// </summary>
|
|
public TextWriter ErrorLog
|
|
{
|
|
get
|
|
{
|
|
return this.m_log;
|
|
}
|
|
set
|
|
{
|
|
this.m_log = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// DTD validation errors are written to this log file.
|
|
/// </summary>
|
|
#if PORTABLE
|
|
[Obsolete("Not supported on the .NETPortable runtime.")]
|
|
#endif
|
|
public string ErrorLogFile
|
|
{
|
|
#if PORTABLE
|
|
get
|
|
{
|
|
throw new NotSupportedException();
|
|
}
|
|
set
|
|
{
|
|
throw new NotSupportedException();
|
|
}
|
|
#else
|
|
get
|
|
{
|
|
return this.m_errorLogFile;
|
|
}
|
|
set
|
|
{
|
|
this.m_errorLogFile = value;
|
|
this.m_log = new StreamWriter(value);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private void Log(string msg, params string[] args)
|
|
{
|
|
if (ErrorLog != null)
|
|
{
|
|
string err = string.Format(CultureInfo.CurrentUICulture, msg, args);
|
|
if (this.m_lastError != this.m_current)
|
|
{
|
|
err = err + " " + this.m_current.Context();
|
|
this.m_lastError = this.m_current;
|
|
ErrorLog.WriteLine("### Error:" + err);
|
|
}
|
|
else
|
|
{
|
|
string path = "";
|
|
if (this.m_current.ResolvedUri != null)
|
|
{
|
|
path = this.m_current.ResolvedUri.AbsolutePath;
|
|
}
|
|
|
|
ErrorLog.WriteLine("### Error in {0}#{1}, line {2}, position {3}: {4}", path, this.m_current.Name, this.m_current.Line, this.m_current.LinePosition, err);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Log(string msg, char ch)
|
|
{
|
|
Log(msg, ch.ToString());
|
|
}
|
|
|
|
private void Init()
|
|
{
|
|
this.m_state = State.Initial;
|
|
this.m_stack = new HWStack(10);
|
|
this.m_node = Push(null, XmlNodeType.Document, null);
|
|
this.m_node.IsEmpty = false;
|
|
this.m_sb = new StringBuilder();
|
|
this.m_name = new StringBuilder();
|
|
this.m_poptodepth = 0;
|
|
this.m_current = null;
|
|
this.m_partial = '\0';
|
|
this.m_endTag = null;
|
|
this.m_a = null;
|
|
this.m_apos = 0;
|
|
this.m_newnode = null;
|
|
this.m_rootCount = 0;
|
|
this.m_foundRoot = false;
|
|
this.unknownNamespaces.Clear();
|
|
}
|
|
|
|
private Node Push(string name, XmlNodeType nt, string value)
|
|
{
|
|
Node result = (Node)this.m_stack.Push();
|
|
if (result == null)
|
|
{
|
|
result = new Node();
|
|
this.m_stack[this.m_stack.Count - 1] = result;
|
|
}
|
|
|
|
result.Reset(name, nt, value);
|
|
this.m_node = result;
|
|
return result;
|
|
}
|
|
|
|
private void SwapTopNodes()
|
|
{
|
|
int top = this.m_stack.Count - 1;
|
|
if (top > 0)
|
|
{
|
|
Node n = (Node)this.m_stack[top - 1];
|
|
this.m_stack[top - 1] = this.m_stack[top];
|
|
this.m_stack[top] = n;
|
|
}
|
|
}
|
|
|
|
private Node Push(Node n)
|
|
{
|
|
// we have to do a deep clone of the Node object because
|
|
// it is reused in the stack.
|
|
Node n2 = Push(n.Name, n.NodeType, n.Value);
|
|
n2.DtdType = n.DtdType;
|
|
n2.IsEmpty = n.IsEmpty;
|
|
n2.Space = n.Space;
|
|
n2.XmlLang = n.XmlLang;
|
|
n2.CurrentState = n.CurrentState;
|
|
n2.CopyAttributes(n);
|
|
this.m_node = n2;
|
|
return n2;
|
|
}
|
|
|
|
private void Pop()
|
|
{
|
|
if (this.m_stack.Count > 1)
|
|
{
|
|
this.m_node = (Node)this.m_stack.Pop();
|
|
}
|
|
}
|
|
|
|
private Node Top()
|
|
{
|
|
int top = this.m_stack.Count - 1;
|
|
if (top > 0)
|
|
{
|
|
return (Node)this.m_stack[top];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The node type of the node currently being parsed.
|
|
/// </summary>
|
|
public override XmlNodeType NodeType
|
|
{
|
|
get
|
|
{
|
|
if (this.m_state == State.Attr)
|
|
{
|
|
return XmlNodeType.Attribute;
|
|
}
|
|
else if (this.m_state == State.AttrValue)
|
|
{
|
|
return XmlNodeType.Text;
|
|
}
|
|
else if (this.m_state == State.EndTag || this.m_state == State.AutoClose)
|
|
{
|
|
return XmlNodeType.EndElement;
|
|
}
|
|
|
|
return this.m_node.NodeType;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The name of the current node, if currently positioned on a node or attribute.
|
|
/// </summary>
|
|
public override string Name
|
|
{
|
|
get
|
|
{
|
|
string result = null;
|
|
if (this.m_state == State.Attr)
|
|
{
|
|
result = XmlConvert.EncodeName(this.m_a.Name);
|
|
}
|
|
else if (this.m_state != State.AttrValue)
|
|
{
|
|
result = this.m_node.Name;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The local name of the current node, if currently positioned on a node or attribute.
|
|
/// </summary>
|
|
public override string LocalName
|
|
{
|
|
get
|
|
{
|
|
string result = Name;
|
|
if (result != null)
|
|
{
|
|
int colon = result.IndexOf(':');
|
|
if (colon != -1)
|
|
{
|
|
result = result.Substring(colon + 1);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The namespace of the current node, if currently positioned on a node or attribute.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If not positioned on a node or attribute, <see cref="UNDEFINED_NAMESPACE"/> is returned.
|
|
/// </remarks>
|
|
[SuppressMessage("Microsoft.Performance", "CA1820", Justification = "Cannot use IsNullOrEmpty in a switch statement and swapping the elegance of switch for a load of 'if's is not worth it.")]
|
|
public override string NamespaceURI
|
|
{
|
|
get
|
|
{
|
|
// SGML has no namespaces, unless this turned out to be an xmlns attribute.
|
|
if (this.m_state == State.Attr && string.Equals(this.m_a.Name, "xmlns", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return "http://www.w3.org/2000/xmlns/";
|
|
}
|
|
|
|
string prefix = Prefix;
|
|
switch (Prefix)
|
|
{
|
|
case "xmlns":
|
|
return "http://www.w3.org/2000/xmlns/";
|
|
case "xml":
|
|
return "http://www.w3.org/XML/1998/namespace";
|
|
case null: // Should never occur since Prefix never returns null
|
|
case "":
|
|
if (NodeType == XmlNodeType.Attribute)
|
|
{
|
|
// attributes without a prefix are never in any namespace
|
|
return string.Empty;
|
|
}
|
|
else if (NodeType == XmlNodeType.Element)
|
|
{
|
|
// check if a 'xmlns:prefix' attribute is defined
|
|
for (int i = this.m_stack.Count - 1; i > 0; --i)
|
|
{
|
|
Node node = this.m_stack[i] as Node;
|
|
if ((node != null) && (node.NodeType == XmlNodeType.Element))
|
|
{
|
|
int index = node.GetAttribute("xmlns");
|
|
if (index >= 0)
|
|
{
|
|
string value = node.GetAttribute(index).Value;
|
|
if (value != null)
|
|
{
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return string.Empty;
|
|
default:
|
|
{
|
|
string value;
|
|
if ((NodeType == XmlNodeType.Attribute) || (NodeType == XmlNodeType.Element))
|
|
{
|
|
|
|
// check if a 'xmlns:prefix' attribute is defined
|
|
string key = "xmlns:" + prefix;
|
|
for (int i = this.m_stack.Count - 1; i > 0; --i)
|
|
{
|
|
Node node = this.m_stack[i] as Node;
|
|
if ((node != null) && (node.NodeType == XmlNodeType.Element))
|
|
{
|
|
int index = node.GetAttribute(key);
|
|
if (index >= 0)
|
|
{
|
|
value = node.GetAttribute(index).Value;
|
|
if (value != null)
|
|
{
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// check if we've seen this prefix before
|
|
if (!unknownNamespaces.TryGetValue(prefix, out value))
|
|
{
|
|
if (unknownNamespaces.Count > 0)
|
|
{
|
|
value = UNDEFINED_NAMESPACE + unknownNamespaces.Count.ToString();
|
|
}
|
|
else
|
|
{
|
|
value = UNDEFINED_NAMESPACE;
|
|
}
|
|
unknownNamespaces[prefix] = value;
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The prefix of the current node's name.
|
|
/// </summary>
|
|
public override string Prefix
|
|
{
|
|
get
|
|
{
|
|
string result = Name;
|
|
if (result != null)
|
|
{
|
|
int colon = result.IndexOf(':');
|
|
if (colon != -1)
|
|
{
|
|
result = result.Substring(0, colon);
|
|
}
|
|
else
|
|
{
|
|
result = string.Empty;
|
|
}
|
|
}
|
|
return result ?? string.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the current node has a value or not.
|
|
/// </summary>
|
|
public override bool HasValue
|
|
{
|
|
get
|
|
{
|
|
if (this.m_state == State.Attr || this.m_state == State.AttrValue)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return (this.m_node.Value != null);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The value of the current node.
|
|
/// </summary>
|
|
public override string Value
|
|
{
|
|
get
|
|
{
|
|
if (this.m_state == State.Attr || this.m_state == State.AttrValue)
|
|
{
|
|
return this.m_a.Value;
|
|
}
|
|
|
|
return this.m_node.Value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the depth of the current node in the XML document.
|
|
/// </summary>
|
|
/// <value>The depth of the current node in the XML document.</value>
|
|
public override int Depth
|
|
{
|
|
get
|
|
{
|
|
if (this.m_state == State.Attr)
|
|
{
|
|
return this.m_stack.Count;
|
|
}
|
|
else if (this.m_state == State.AttrValue)
|
|
{
|
|
return this.m_stack.Count + 1;
|
|
}
|
|
|
|
return this.m_stack.Count - 1;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the base URI of the current node.
|
|
/// </summary>
|
|
/// <value>The base URI of the current node.</value>
|
|
public override string BaseURI
|
|
{
|
|
get
|
|
{
|
|
return this.m_baseUri == null ? "" : this.m_baseUri.AbsoluteUri;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the current node is an empty element (for example, <MyElement/>).
|
|
/// </summary>
|
|
public override bool IsEmptyElement
|
|
{
|
|
get
|
|
{
|
|
if (this.m_state == State.Markup || this.m_state == State.Attr || this.m_state == State.AttrValue)
|
|
{
|
|
return this.m_node.IsEmpty;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the current node is an attribute that was generated from the default value defined in the DTD or schema.
|
|
/// </summary>
|
|
/// <value>
|
|
/// true if the current node is an attribute whose value was generated from the default value defined in the DTD or
|
|
/// schema; false if the attribute value was explicitly set.
|
|
/// </value>
|
|
public override bool IsDefault
|
|
{
|
|
get
|
|
{
|
|
if (this.m_state == State.Attr || this.m_state == State.AttrValue)
|
|
return this.m_a.IsDefault;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the quotation mark character used to enclose the value of an attribute node.
|
|
/// </summary>
|
|
/// <value>The quotation mark character (" or ') used to enclose the value of an attribute node.</value>
|
|
/// <remarks>
|
|
/// This property applies only to an attribute node.
|
|
/// </remarks>
|
|
#if PORTABLE
|
|
public char QuoteChar
|
|
#else
|
|
public override char QuoteChar
|
|
#endif
|
|
{
|
|
get
|
|
{
|
|
if (this.m_a != null)
|
|
return this.m_a.QuoteChar;
|
|
|
|
return '\0';
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the current xml:space scope.
|
|
/// </summary>
|
|
/// <value>One of the <see cref="XmlSpace"/> values. If no xml:space scope exists, this property defaults to XmlSpace.None.</value>
|
|
public override XmlSpace XmlSpace
|
|
{
|
|
get
|
|
{
|
|
for (int i = this.m_stack.Count - 1; i > 1; i--)
|
|
{
|
|
Node n = (Node)this.m_stack[i];
|
|
XmlSpace xs = n.Space;
|
|
if (xs != XmlSpace.None)
|
|
return xs;
|
|
}
|
|
|
|
return XmlSpace.None;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the current xml:lang scope.
|
|
/// </summary>
|
|
/// <value>The current xml:lang scope.</value>
|
|
public override string XmlLang
|
|
{
|
|
get
|
|
{
|
|
for (int i = this.m_stack.Count - 1; i > 1; i--)
|
|
{
|
|
Node n = (Node)this.m_stack[i];
|
|
string xmllang = n.XmlLang;
|
|
if (xmllang != null)
|
|
return xmllang;
|
|
}
|
|
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Specifies how white space is handled.
|
|
/// </summary>
|
|
public WhitespaceHandling WhitespaceHandling
|
|
{
|
|
get
|
|
{
|
|
return this.m_whitespaceHandling;
|
|
}
|
|
set
|
|
{
|
|
this.m_whitespaceHandling = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the number of attributes on the current node.
|
|
/// </summary>
|
|
/// <value>The number of attributes on the current node.</value>
|
|
public override int AttributeCount
|
|
{
|
|
get
|
|
{
|
|
if (this.m_state == State.Attr || this.m_state == State.AttrValue)
|
|
//For compatibility with mono
|
|
return this.m_node.AttributeCount;
|
|
else if (this.m_node.NodeType == XmlNodeType.Element || this.m_node.NodeType == XmlNodeType.DocumentType)
|
|
return this.m_node.AttributeCount;
|
|
else
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of an attribute with the specified <see cref="Name"/>.
|
|
/// </summary>
|
|
/// <param name="name">The name of the attribute to retrieve.</param>
|
|
/// <returns>The value of the specified attribute. If the attribute is not found, a null reference (Nothing in Visual Basic) is returned. </returns>
|
|
public override string GetAttribute(string name)
|
|
{
|
|
if (this.m_state != State.Attr && this.m_state != State.AttrValue)
|
|
{
|
|
int i = this.m_node.GetAttribute(name);
|
|
if (i >= 0)
|
|
return GetAttribute(i);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of the attribute with the specified <see cref="LocalName"/> and <see cref="NamespaceURI"/>.
|
|
/// </summary>
|
|
/// <param name="name">The local name of the attribute.</param>
|
|
/// <param name="namespaceURI">The namespace URI of the attribute.</param>
|
|
/// <returns>The value of the specified attribute. If the attribute is not found, a null reference (Nothing in Visual Basic) is returned. This method does not move the reader.</returns>
|
|
public override string GetAttribute(string name, string namespaceURI)
|
|
{
|
|
return GetAttribute(name); // SGML has no namespaces.
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of the attribute with the specified index.
|
|
/// </summary>
|
|
/// <param name="i">The index of the attribute.</param>
|
|
/// <returns>The value of the specified attribute. This method does not move the reader.</returns>
|
|
public override string GetAttribute(int i)
|
|
{
|
|
if (this.m_state != State.Attr && this.m_state != State.AttrValue)
|
|
{
|
|
Attribute a = this.m_node.GetAttribute(i);
|
|
if (a != null)
|
|
return a.Value;
|
|
}
|
|
|
|
throw new ArgumentOutOfRangeException("i");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of the attribute with the specified index.
|
|
/// </summary>
|
|
/// <param name="i">The index of the attribute.</param>
|
|
/// <returns>The value of the specified attribute. This method does not move the reader.</returns>
|
|
public override string this[int i]
|
|
{
|
|
get
|
|
{
|
|
return GetAttribute(i);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of an attribute with the specified <see cref="Name"/>.
|
|
/// </summary>
|
|
/// <param name="name">The name of the attribute to retrieve.</param>
|
|
/// <returns>The value of the specified attribute. If the attribute is not found, a null reference (Nothing in Visual Basic) is returned. </returns>
|
|
public override string this[string name]
|
|
{
|
|
get
|
|
{
|
|
return GetAttribute(name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of the attribute with the specified <see cref="LocalName"/> and <see cref="NamespaceURI"/>.
|
|
/// </summary>
|
|
/// <param name="name">The local name of the attribute.</param>
|
|
/// <param name="namespaceURI">The namespace URI of the attribute.</param>
|
|
/// <returns>The value of the specified attribute. If the attribute is not found, a null reference (Nothing in Visual Basic) is returned. This method does not move the reader.</returns>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1023", Justification = "This design is that of Microsoft's XmlReader class and overriding its method is merely continuing the same design.")]
|
|
public override string this[string name, string namespaceURI]
|
|
{
|
|
get
|
|
{
|
|
return GetAttribute(name, namespaceURI);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves to the atttribute with the specified <see cref="Name"/>.
|
|
/// </summary>
|
|
/// <param name="name">The qualified name of the attribute.</param>
|
|
/// <returns>true if the attribute is found; otherwise, false. If false, the reader's position does not change.</returns>
|
|
public override bool MoveToAttribute(string name)
|
|
{
|
|
int i = this.m_node.GetAttribute(name);
|
|
if (i >= 0)
|
|
{
|
|
MoveToAttribute(i);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves to the attribute with the specified <see cref="LocalName"/> and <see cref="NamespaceURI"/>.
|
|
/// </summary>
|
|
/// <param name="name">The local name of the attribute.</param>
|
|
/// <param name="ns">The namespace URI of the attribute.</param>
|
|
/// <returns>true if the attribute is found; otherwise, false. If false, the reader's position does not change.</returns>
|
|
public override bool MoveToAttribute(string name, string ns)
|
|
{
|
|
return MoveToAttribute(name);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves to the attribute with the specified index.
|
|
/// </summary>
|
|
/// <param name="i">The index of the attribute to move to.</param>
|
|
public override void MoveToAttribute(int i)
|
|
{
|
|
Attribute a = this.m_node.GetAttribute(i);
|
|
if (a != null)
|
|
{
|
|
this.m_apos = i;
|
|
this.m_a = a;
|
|
//Make sure that AttrValue does not overwrite the preserved value
|
|
if (this.m_state != State.Attr && this.m_state != State.AttrValue)
|
|
{
|
|
this.m_node.CurrentState = this.m_state; //save current state.
|
|
}
|
|
|
|
this.m_state = State.Attr;
|
|
return;
|
|
}
|
|
|
|
throw new ArgumentOutOfRangeException("i");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves to the first attribute.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public override bool MoveToFirstAttribute()
|
|
{
|
|
if (this.m_node.AttributeCount > 0)
|
|
{
|
|
MoveToAttribute(0);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves to the next attribute.
|
|
/// </summary>
|
|
/// <returns>true if there is a next attribute; false if there are no more attributes.</returns>
|
|
/// <remarks>
|
|
/// If the current node is an element node, this method is equivalent to <see cref="MoveToFirstAttribute"/>. If <see cref="MoveToNextAttribute"/> returns true,
|
|
/// the reader moves to the next attribute; otherwise, the position of the reader does not change.
|
|
/// </remarks>
|
|
public override bool MoveToNextAttribute()
|
|
{
|
|
if (this.m_state != State.Attr && this.m_state != State.AttrValue)
|
|
{
|
|
return MoveToFirstAttribute();
|
|
}
|
|
else if (this.m_apos < this.m_node.AttributeCount - 1)
|
|
{
|
|
MoveToAttribute(this.m_apos + 1);
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves to the element that contains the current attribute node.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// true if the reader is positioned on an attribute (the reader moves to the element that owns the attribute); false if the reader is not positioned
|
|
/// on an attribute (the position of the reader does not change).
|
|
/// </returns>
|
|
public override bool MoveToElement()
|
|
{
|
|
if (this.m_state == State.Attr || this.m_state == State.AttrValue)
|
|
{
|
|
this.m_state = this.m_node.CurrentState;
|
|
this.m_a = null;
|
|
return true;
|
|
}
|
|
else
|
|
return (this.m_node.NodeType == XmlNodeType.Element);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets whether the content is HTML or not.
|
|
/// </summary>
|
|
public bool IsHtml
|
|
{
|
|
get
|
|
{
|
|
return this.m_isHtml;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the encoding of the current entity.
|
|
/// </summary>
|
|
/// <returns>The encoding of the current entity.</returns>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024", Justification = "This method to get the encoding does not simply read a value, but potentially causes significant processing of the input stream.")]
|
|
public Encoding GetEncoding()
|
|
{
|
|
if (this.m_current == null)
|
|
{
|
|
OpenInput();
|
|
}
|
|
|
|
return this.m_current.Encoding;
|
|
}
|
|
|
|
private void OpenInput()
|
|
{
|
|
LazyLoadDtd(this.m_baseUri);
|
|
|
|
if (this.Href != null)
|
|
{
|
|
this.m_current = new Entity("#document", null, this.m_href, this.m_proxy);
|
|
}
|
|
else if (this.m_inputStream != null)
|
|
{
|
|
this.m_current = new Entity("#document", null, this.m_inputStream, this.m_proxy);
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException("You must specify input either via Href or InputStream properties");
|
|
}
|
|
|
|
this.m_current.IsHtml = this.IsHtml;
|
|
this.m_current.Open(null, this.m_baseUri);
|
|
if (this.m_current.ResolvedUri != null)
|
|
this.m_baseUri = this.m_current.ResolvedUri;
|
|
|
|
if (this.m_current.IsHtml && this.m_dtd == null)
|
|
{
|
|
this.m_docType = "HTML";
|
|
LazyLoadDtd(this.m_baseUri);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the next node from the stream.
|
|
/// </summary>
|
|
/// <returns>true if the next node was read successfully; false if there are no more nodes to read.</returns>
|
|
public override bool Read()
|
|
{
|
|
if (m_current == null)
|
|
{
|
|
OpenInput();
|
|
}
|
|
|
|
if (m_node.Simulated)
|
|
{
|
|
// return the next node
|
|
m_node.Simulated = false;
|
|
this.m_node = Top();
|
|
this.m_state = this.m_node.CurrentState;
|
|
return true;
|
|
}
|
|
|
|
bool foundnode = false;
|
|
while (!foundnode)
|
|
{
|
|
switch (this.m_state)
|
|
{
|
|
case State.Initial:
|
|
this.m_state = State.Markup;
|
|
this.m_current.ReadChar();
|
|
goto case State.Markup;
|
|
case State.Eof:
|
|
if (this.m_current.Parent != null)
|
|
{
|
|
this.m_current.Close();
|
|
this.m_current = this.m_current.Parent;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
break;
|
|
case State.EndTag:
|
|
if (string.Equals(this.m_endTag, this.m_node.Name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Pop(); // we're done!
|
|
this.m_state = State.Markup;
|
|
goto case State.Markup;
|
|
}
|
|
Pop(); // close one element
|
|
foundnode = true;// return another end element.
|
|
break;
|
|
case State.Markup:
|
|
if (this.m_node.IsEmpty)
|
|
{
|
|
Pop();
|
|
}
|
|
foundnode = ParseMarkup();
|
|
break;
|
|
case State.PartialTag:
|
|
Pop(); // remove text node.
|
|
this.m_state = State.Markup;
|
|
foundnode = ParseTag(this.m_partial);
|
|
break;
|
|
case State.PseudoStartTag:
|
|
foundnode = ParseStartTag('<');
|
|
break;
|
|
case State.AutoClose:
|
|
Pop(); // close next node.
|
|
if (this.m_stack.Count <= this.m_poptodepth)
|
|
{
|
|
this.m_state = State.Markup;
|
|
if (this.m_newnode != null)
|
|
{
|
|
Push(this.m_newnode); // now we're ready to start the new node.
|
|
this.m_newnode = null;
|
|
this.m_state = State.Markup;
|
|
}
|
|
else if (this.m_node.NodeType == XmlNodeType.Document)
|
|
{
|
|
this.m_state = State.Eof;
|
|
goto case State.Eof;
|
|
}
|
|
}
|
|
foundnode = true;
|
|
break;
|
|
case State.CData:
|
|
foundnode = ParseCData();
|
|
break;
|
|
case State.Attr:
|
|
goto case State.AttrValue;
|
|
case State.AttrValue:
|
|
this.m_state = State.Markup;
|
|
goto case State.Markup;
|
|
case State.Text:
|
|
Pop();
|
|
goto case State.Markup;
|
|
case State.PartialText:
|
|
if (ParseText(this.m_current.Lastchar, false))
|
|
{
|
|
this.m_node.NodeType = XmlNodeType.Whitespace;
|
|
}
|
|
|
|
foundnode = true;
|
|
break;
|
|
}
|
|
|
|
if (foundnode && this.m_node.NodeType == XmlNodeType.Whitespace && this.m_whitespaceHandling == WhitespaceHandling.None)
|
|
{
|
|
// strip out whitespace (caller is probably pretty printing the XML).
|
|
foundnode = false;
|
|
}
|
|
if (!foundnode && this.m_state == State.Eof && this.m_stack.Count > 1)
|
|
{
|
|
this.m_poptodepth = 1;
|
|
this.m_state = State.AutoClose;
|
|
this.m_node = Top();
|
|
return true;
|
|
}
|
|
}
|
|
if (!m_foundRoot && (this.NodeType == XmlNodeType.Element ||
|
|
this.NodeType == XmlNodeType.Text ||
|
|
this.NodeType == XmlNodeType.CDATA))
|
|
{
|
|
m_foundRoot = true;
|
|
if (this.IsHtml && (this.NodeType != XmlNodeType.Element ||
|
|
!string.Equals(this.LocalName, "html", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
// Simulate an HTML root element!
|
|
this.m_node.CurrentState = this.m_state;
|
|
Node root = Push("html", XmlNodeType.Element, null);
|
|
SwapTopNodes(); // make html the outer element.
|
|
this.m_node = root;
|
|
root.Simulated = true;
|
|
root.IsEmpty = false;
|
|
this.m_state = State.Markup;
|
|
//this.state = State.PseudoStartTag;
|
|
//this.startTag = name;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool ParseMarkup()
|
|
{
|
|
char ch = this.m_current.Lastchar;
|
|
if (ch == '<')
|
|
{
|
|
ch = this.m_current.ReadChar();
|
|
return ParseTag(ch);
|
|
}
|
|
else if (ch != Entity.EOF)
|
|
{
|
|
if (this.m_node.DtdType != null && this.m_node.DtdType.ContentModel.DeclaredContent == DeclaredContent.CDATA)
|
|
{
|
|
// e.g. SCRIPT or STYLE tags which contain unparsed character data.
|
|
this.m_partial = '\0';
|
|
this.m_state = State.CData;
|
|
return false;
|
|
}
|
|
else if (ParseText(ch, true))
|
|
{
|
|
this.m_node.NodeType = XmlNodeType.Whitespace;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
this.m_state = State.Eof;
|
|
return false;
|
|
}
|
|
|
|
private const string declterm = " \t\r\n><";
|
|
private bool ParseTag(char ch)
|
|
{
|
|
if (ch == '%')
|
|
{
|
|
return ParseAspNet();
|
|
}
|
|
else if (ch == '!')
|
|
{
|
|
ch = this.m_current.ReadChar();
|
|
if (ch == '-')
|
|
{
|
|
return ParseComment();
|
|
}
|
|
else if (ch == '[')
|
|
{
|
|
return ParseConditionalBlock();
|
|
}
|
|
else if (ch != '_' && !char.IsLetter(ch))
|
|
{
|
|
// perhaps it's one of those nasty office document hacks like '<![if ! ie ]>'
|
|
string value = this.m_current.ScanToEnd(this.m_sb, "Recovering", ">"); // skip it
|
|
Log("Ignoring invalid markup '<!" + value + ">");
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
string name = this.m_current.ScanToken(this.m_sb, SgmlReader.declterm, false);
|
|
if (string.Equals(name, "DOCTYPE", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
ParseDocType();
|
|
|
|
// In SGML DOCTYPE SYSTEM attribute is optional, but in XML it is required,
|
|
// therefore if there is no SYSTEM literal then add an empty one.
|
|
if (this.GetAttribute("SYSTEM") == null && this.GetAttribute("PUBLIC") != null)
|
|
{
|
|
this.m_node.AddAttribute("SYSTEM", "", '"', this.m_folding == CaseFolding.None);
|
|
}
|
|
|
|
if (m_stripDocType)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
this.m_node.NodeType = XmlNodeType.DocumentType;
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log("Invalid declaration '<!{0}...'. Expecting '<!DOCTYPE' only.", name);
|
|
this.m_current.ScanToEnd(null, "Recovering", ">"); // skip it
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else if (ch == '?')
|
|
{
|
|
this.m_current.ReadChar();// consume the '?' character.
|
|
return ParsePI();
|
|
}
|
|
else if (ch == '/')
|
|
{
|
|
return ParseEndTag();
|
|
}
|
|
else
|
|
{
|
|
return ParseStartTag(ch);
|
|
}
|
|
}
|
|
|
|
private string ScanName(string terminators)
|
|
{
|
|
string name = this.m_current.ScanToken(this.m_sb, terminators, false);
|
|
switch (this.m_folding)
|
|
{
|
|
case CaseFolding.ToUpper:
|
|
name = name.ToUpperInvariant();
|
|
break;
|
|
case CaseFolding.ToLower:
|
|
name = name.ToLowerInvariant();
|
|
break;
|
|
}
|
|
return name;
|
|
}
|
|
|
|
private static bool VerifyName(string name)
|
|
{
|
|
try
|
|
{
|
|
XmlConvert.VerifyName(name);
|
|
return true;
|
|
}
|
|
catch (XmlException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private const string tagterm = " \t\r\n=/><";
|
|
private const string aterm = " \t\r\n='\"/>";
|
|
private const string avterm = " \t\r\n>";
|
|
private bool ParseStartTag(char ch)
|
|
{
|
|
string name = null;
|
|
if (m_state != State.PseudoStartTag)
|
|
{
|
|
if (SgmlReader.tagterm.IndexOf(ch) >= 0)
|
|
{
|
|
this.m_sb.Length = 0;
|
|
this.m_sb.Append('<');
|
|
this.m_state = State.PartialText;
|
|
return false;
|
|
}
|
|
|
|
name = ScanName(SgmlReader.tagterm);
|
|
}
|
|
else
|
|
{
|
|
// TODO: Changes by mindtouch mean that this.startTag is never non-null. The effects of this need checking.
|
|
|
|
//name = this.startTag;
|
|
m_state = State.Markup;
|
|
}
|
|
|
|
Node n = Push(name, XmlNodeType.Element, null);
|
|
n.IsEmpty = false;
|
|
Validate(n);
|
|
ch = this.m_current.SkipWhitespace();
|
|
while (ch != Entity.EOF && ch != '>')
|
|
{
|
|
if (ch == '/')
|
|
{
|
|
n.IsEmpty = true;
|
|
ch = this.m_current.ReadChar();
|
|
if (ch != '>')
|
|
{
|
|
Log("Expected empty start tag '/>' sequence instead of '{0}'", ch);
|
|
this.m_current.ScanToEnd(null, "Recovering", ">");
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
else if (ch == '<')
|
|
{
|
|
Log("Start tag '{0}' is missing '>'", name);
|
|
break;
|
|
}
|
|
|
|
string aname = ScanName(SgmlReader.aterm);
|
|
ch = this.m_current.SkipWhitespace();
|
|
if (string.Equals(aname, ",", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(aname, "=", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(aname, ":", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(aname, ";", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string value = null;
|
|
char quote = '\0';
|
|
if (ch == '=' || ch == '"' || ch == '\'')
|
|
{
|
|
if (ch == '=')
|
|
{
|
|
this.m_current.ReadChar();
|
|
ch = this.m_current.SkipWhitespace();
|
|
}
|
|
|
|
if (ch == '\'' || ch == '\"')
|
|
{
|
|
quote = ch;
|
|
value = ScanLiteral(this.m_sb, ch);
|
|
}
|
|
else if (ch != '>')
|
|
{
|
|
string term = SgmlReader.avterm;
|
|
value = this.m_current.ScanToken(this.m_sb, term, false);
|
|
}
|
|
}
|
|
|
|
if (ValidAttributeName(aname))
|
|
{
|
|
Attribute a = n.AddAttribute(aname, value ?? aname, quote, this.m_folding == CaseFolding.None);
|
|
if (a == null)
|
|
{
|
|
Log("Duplicate attribute '{0}' ignored", aname);
|
|
}
|
|
else
|
|
{
|
|
ValidateAttribute(n, a);
|
|
}
|
|
}
|
|
|
|
ch = this.m_current.SkipWhitespace();
|
|
}
|
|
|
|
if (ch == Entity.EOF)
|
|
{
|
|
this.m_current.Error("Unexpected EOF parsing start tag '{0}'", name);
|
|
}
|
|
else if (ch == '>')
|
|
{
|
|
this.m_current.ReadChar(); // consume '>'
|
|
}
|
|
|
|
if (this.Depth == 1)
|
|
{
|
|
if (this.m_rootCount == 1)
|
|
{
|
|
// Hmmm, we found another root level tag, soooo, the only
|
|
// thing we can do to keep this a valid XML document is stop
|
|
this.m_state = State.Eof;
|
|
return false;
|
|
}
|
|
this.m_rootCount++;
|
|
}
|
|
|
|
ValidateContent(n);
|
|
return true;
|
|
}
|
|
|
|
private bool ParseEndTag()
|
|
{
|
|
this.m_state = State.EndTag;
|
|
this.m_current.ReadChar(); // consume '/' char.
|
|
string name = this.ScanName(SgmlReader.tagterm);
|
|
char ch = this.m_current.SkipWhitespace();
|
|
if (ch != '>')
|
|
{
|
|
Log("Expected empty start tag '/>' sequence instead of '{0}'", ch);
|
|
this.m_current.ScanToEnd(null, "Recovering", ">");
|
|
}
|
|
|
|
this.m_current.ReadChar(); // consume '>'
|
|
|
|
this.m_endTag = name;
|
|
|
|
// Make sure there's a matching start tag for it.
|
|
bool caseInsensitive = (this.m_folding == CaseFolding.None);
|
|
this.m_node = (Node)this.m_stack[this.m_stack.Count - 1];
|
|
for (int i = this.m_stack.Count - 1; i > 0; i--)
|
|
{
|
|
Node n = (Node)this.m_stack[i];
|
|
if (string.Equals(n.Name, name, caseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
|
|
{
|
|
this.m_endTag = n.Name;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
Log("No matching start tag for '</{0}>'", name);
|
|
this.m_state = State.Markup;
|
|
return false;
|
|
}
|
|
|
|
private bool ParseAspNet()
|
|
{
|
|
string value = "<%" + this.m_current.ScanToEnd(this.m_sb, "AspNet", "%>") + "%>";
|
|
Push(null, XmlNodeType.CDATA, value);
|
|
return true;
|
|
}
|
|
|
|
private bool ParseComment()
|
|
{
|
|
char ch = this.m_current.ReadChar();
|
|
if (ch != '-')
|
|
{
|
|
Log("Expecting comment '<!--' but found {0}", ch);
|
|
this.m_current.ScanToEnd(null, "Comment", ">");
|
|
return false;
|
|
}
|
|
|
|
string value = this.m_current.ScanToEnd(this.m_sb, "Comment", "-->");
|
|
|
|
// Make sure it's a valid comment!
|
|
int i = value.IndexOf("--");
|
|
|
|
while (i >= 0)
|
|
{
|
|
int j = i + 2;
|
|
while (j < value.Length && value[j] == '-')
|
|
j++;
|
|
|
|
if (i > 0)
|
|
{
|
|
value = value.Substring(0, i - 1) + "-" + value.Substring(j);
|
|
}
|
|
else
|
|
{
|
|
value = "-" + value.Substring(j);
|
|
}
|
|
|
|
i = value.IndexOf("--");
|
|
}
|
|
|
|
if (value.Length > 0 && value[value.Length - 1] == '-')
|
|
{
|
|
value += " "; // '-' cannot be last character
|
|
}
|
|
|
|
Push(null, XmlNodeType.Comment, value);
|
|
return true;
|
|
}
|
|
|
|
private const string cdataterm = "\t\r\n[]<>";
|
|
private bool ParseConditionalBlock()
|
|
{
|
|
char ch = m_current.ReadChar(); // skip '['
|
|
ch = m_current.SkipWhitespace();
|
|
string name = m_current.ScanToken(m_sb, cdataterm, false);
|
|
if (name.StartsWith("if "))
|
|
{
|
|
// 'downlevel-revealed' comment (another atrocity of the IE team)
|
|
m_current.ScanToEnd(null, "CDATA", ">");
|
|
return false;
|
|
}
|
|
else if (!string.Equals(name, "CDATA", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Log("Expecting CDATA but found '{0}'", name);
|
|
m_current.ScanToEnd(null, "CDATA", ">");
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
ch = m_current.SkipWhitespace();
|
|
if (ch != '[')
|
|
{
|
|
Log("Expecting '[' but found '{0}'", ch);
|
|
m_current.ScanToEnd(null, "CDATA", ">");
|
|
return false;
|
|
}
|
|
|
|
string value = m_current.ScanToEnd(m_sb, "CDATA", "]]>");
|
|
|
|
Push(null, XmlNodeType.CDATA, value);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private const string dtterm = " \t\r\n>";
|
|
private void ParseDocType()
|
|
{
|
|
char ch = this.m_current.SkipWhitespace();
|
|
string name = this.ScanName(SgmlReader.dtterm);
|
|
Push(name, XmlNodeType.DocumentType, null);
|
|
ch = this.m_current.SkipWhitespace();
|
|
if (ch != '>')
|
|
{
|
|
string subset = "";
|
|
string pubid = "";
|
|
string syslit = "";
|
|
|
|
if (ch != '[')
|
|
{
|
|
string token = this.m_current.ScanToken(this.m_sb, SgmlReader.dtterm, false);
|
|
if (string.Equals(token, "PUBLIC", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
ch = this.m_current.SkipWhitespace();
|
|
if (ch == '\"' || ch == '\'')
|
|
{
|
|
pubid = this.m_current.ScanLiteral(this.m_sb, ch);
|
|
this.m_node.AddAttribute(token, pubid, ch, this.m_folding == CaseFolding.None);
|
|
}
|
|
}
|
|
else if (!string.Equals(token, "SYSTEM", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Log("Unexpected token in DOCTYPE '{0}'", token);
|
|
this.m_current.ScanToEnd(null, "DOCTYPE", ">");
|
|
}
|
|
ch = this.m_current.SkipWhitespace();
|
|
if (ch == '\"' || ch == '\'')
|
|
{
|
|
token = "SYSTEM";
|
|
syslit = this.m_current.ScanLiteral(this.m_sb, ch);
|
|
this.m_node.AddAttribute(token, syslit, ch, this.m_folding == CaseFolding.None);
|
|
}
|
|
ch = this.m_current.SkipWhitespace();
|
|
}
|
|
|
|
if (ch == '[')
|
|
{
|
|
subset = this.m_current.ScanToEnd(this.m_sb, "Internal Subset", "]");
|
|
this.m_node.Value = subset;
|
|
}
|
|
|
|
ch = this.m_current.SkipWhitespace();
|
|
if (ch != '>')
|
|
{
|
|
Log("Expecting end of DOCTYPE tag, but found '{0}'", ch);
|
|
this.m_current.ScanToEnd(null, "DOCTYPE", ">");
|
|
}
|
|
|
|
if (this.m_dtd != null && !string.Equals(this.m_dtd.Name, name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new InvalidOperationException("DTD does not match document type");
|
|
}
|
|
|
|
this.m_docType = name;
|
|
this.m_pubid = pubid;
|
|
this.m_syslit = syslit;
|
|
this.m_subset = subset;
|
|
LazyLoadDtd(this.m_current.ResolvedUri);
|
|
}
|
|
|
|
this.m_current.ReadChar();
|
|
}
|
|
|
|
private const string piterm = " \t\r\n?";
|
|
private bool ParsePI()
|
|
{
|
|
string name = this.m_current.ScanToken(this.m_sb, SgmlReader.piterm, false);
|
|
string value = null;
|
|
if (this.m_current.Lastchar != '?')
|
|
{
|
|
// Notice this is not "?>". This is because Office generates bogus PI's that end with "/>".
|
|
value = this.m_current.ScanToEnd(this.m_sb, "Processing Instruction", ">");
|
|
value = value.TrimEnd('/');
|
|
}
|
|
else
|
|
{
|
|
// error recovery.
|
|
value = this.m_current.ScanToEnd(this.m_sb, "Processing Instruction", ">");
|
|
}
|
|
|
|
// check if the name has a prefix; if so, ignore it
|
|
int colon = name.IndexOf(':');
|
|
if (colon > 0)
|
|
{
|
|
name = name.Substring(colon + 1);
|
|
}
|
|
|
|
// skip xml declarations, since these are generated in the output instead.
|
|
if (!string.Equals(name, "xml", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Push(name, XmlNodeType.ProcessingInstruction, value);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool ParseText(char ch, bool newtext)
|
|
{
|
|
bool ws = !newtext || this.m_current.IsWhitespace;
|
|
if (newtext)
|
|
this.m_sb.Length = 0;
|
|
|
|
//this.sb.Append(ch);
|
|
//ch = this.current.ReadChar();
|
|
this.m_state = State.Text;
|
|
while (ch != Entity.EOF)
|
|
{
|
|
if (ch == '<')
|
|
{
|
|
ch = this.m_current.ReadChar();
|
|
if (ch == '/' || ch == '!' || ch == '?' || char.IsLetter(ch))
|
|
{
|
|
// Hit a tag, so return XmlNodeType.Text token
|
|
// and remember we partially started a new tag.
|
|
this.m_state = State.PartialTag;
|
|
this.m_partial = ch;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
// not a tag, so just proceed.
|
|
this.m_sb.Append('<');
|
|
this.m_sb.Append(ch);
|
|
ws = false;
|
|
ch = this.m_current.ReadChar();
|
|
}
|
|
}
|
|
else if (ch == '&')
|
|
{
|
|
ExpandEntity(this.m_sb, '<');
|
|
ws = false;
|
|
ch = this.m_current.Lastchar;
|
|
}
|
|
else
|
|
{
|
|
if (!this.m_current.IsWhitespace)
|
|
ws = false;
|
|
this.m_sb.Append(ch);
|
|
ch = this.m_current.ReadChar();
|
|
}
|
|
}
|
|
|
|
string value = this.m_sb.ToString();
|
|
Push(null, XmlNodeType.Text, value);
|
|
return ws;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Consumes and returns a literal block of text, expanding entities as it does so.
|
|
/// </summary>
|
|
/// <param name="sb">The string builder to use.</param>
|
|
/// <param name="quote">The delimiter for the literal.</param>
|
|
/// <returns>The consumed literal.</returns>
|
|
/// <remarks>
|
|
/// This version is slightly different from <see cref="Entity.ScanLiteral"/> in that
|
|
/// it also expands entities.
|
|
/// </remarks>
|
|
private string ScanLiteral(StringBuilder sb, char quote)
|
|
{
|
|
sb.Length = 0;
|
|
char ch = this.m_current.ReadChar();
|
|
while (ch != Entity.EOF && ch != quote && ch != '>')
|
|
{
|
|
if (ch == '&')
|
|
{
|
|
ExpandEntity(sb, quote);
|
|
ch = this.m_current.Lastchar;
|
|
}
|
|
else
|
|
{
|
|
sb.Append(ch);
|
|
ch = this.m_current.ReadChar();
|
|
}
|
|
}
|
|
if (ch == quote)
|
|
{
|
|
this.m_current.ReadChar(); // consume end quote.
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
private bool ParseCData()
|
|
{
|
|
// Like ParseText(), only it doesn't allow elements in the content.
|
|
// It allows comments and processing instructions and text only and
|
|
// text is not returned as text but CDATA (since it may contain angle brackets).
|
|
// And initial whitespace is ignored. It terminates when we hit the
|
|
// end tag for the current CDATA node (e.g. </style>).
|
|
bool ws = this.m_current.IsWhitespace;
|
|
this.m_sb.Length = 0;
|
|
char ch = this.m_current.Lastchar;
|
|
if (this.m_partial != '\0')
|
|
{
|
|
Pop(); // pop the CDATA
|
|
switch (this.m_partial)
|
|
{
|
|
case '!':
|
|
this.m_partial = ' '; // and pop the comment next time around
|
|
return ParseComment();
|
|
case '?':
|
|
this.m_partial = ' '; // and pop the PI next time around
|
|
return ParsePI();
|
|
case '/':
|
|
this.m_state = State.EndTag;
|
|
return true; // we are done!
|
|
case ' ':
|
|
break; // means we just needed to pop the Comment, PI or CDATA.
|
|
}
|
|
}
|
|
|
|
// if this.partial == '!' then parse the comment and return
|
|
// if this.partial == '?' then parse the processing instruction and return.
|
|
while (ch != Entity.EOF)
|
|
{
|
|
if (ch == '<')
|
|
{
|
|
ch = this.m_current.ReadChar();
|
|
if (ch == '!')
|
|
{
|
|
ch = this.m_current.ReadChar();
|
|
if (ch == '-')
|
|
{
|
|
// return what CDATA we have accumulated so far
|
|
// then parse the comment and return to here.
|
|
if (ws)
|
|
{
|
|
this.m_partial = ' '; // pop comment next time through
|
|
return ParseComment();
|
|
}
|
|
else
|
|
{
|
|
// return what we've accumulated so far then come
|
|
// back in and parse the comment.
|
|
this.m_partial = '!';
|
|
break;
|
|
}
|
|
#if FIX
|
|
} else if (ch == '['){
|
|
// We are about to wrap this node as a CDATA block because of it's
|
|
// type in the DTD, but since we found a CDATA block in the input
|
|
// we have to parse it as a CDATA block, otherwise we will attempt
|
|
// to output nested CDATA blocks which of course is illegal.
|
|
if (this.ParseConditionalBlock()){
|
|
this.partial = ' ';
|
|
return true;
|
|
}
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
// not a comment, so ignore it and continue on.
|
|
this.m_sb.Append('<');
|
|
this.m_sb.Append('!');
|
|
this.m_sb.Append(ch);
|
|
ws = false;
|
|
}
|
|
}
|
|
else if (ch == '?')
|
|
{
|
|
// processing instruction.
|
|
this.m_current.ReadChar();// consume the '?' character.
|
|
if (ws)
|
|
{
|
|
this.m_partial = ' '; // pop PI next time through
|
|
return ParsePI();
|
|
}
|
|
else
|
|
{
|
|
this.m_partial = '?';
|
|
break;
|
|
}
|
|
}
|
|
else if (ch == '/')
|
|
{
|
|
// see if this is the end tag for this CDATA node.
|
|
string temp = this.m_sb.ToString();
|
|
if (ParseEndTag() && string.Equals(this.m_endTag, this.m_node.Name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (ws || string.IsNullOrEmpty(temp))
|
|
{
|
|
// we are done!
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// return CDATA text then the end tag
|
|
this.m_partial = '/';
|
|
this.m_sb.Length = 0; // restore buffer!
|
|
this.m_sb.Append(temp);
|
|
this.m_state = State.CData;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// wrong end tag, so continue on.
|
|
this.m_sb.Length = 0; // restore buffer!
|
|
this.m_sb.Append(temp);
|
|
this.m_sb.Append("</" + this.m_endTag + ">");
|
|
ws = false;
|
|
|
|
// NOTE (steveb): we have one character in the buffer that we need to process next
|
|
ch = this.m_current.Lastchar;
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// must be just part of the CDATA block, so proceed.
|
|
this.m_sb.Append('<');
|
|
this.m_sb.Append(ch);
|
|
ws = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!this.m_current.IsWhitespace && ws)
|
|
ws = false;
|
|
this.m_sb.Append(ch);
|
|
}
|
|
|
|
ch = this.m_current.ReadChar();
|
|
}
|
|
|
|
// NOTE (steveb): check if we reached EOF, which means it's over
|
|
if (ch == Entity.EOF)
|
|
{
|
|
this.m_state = State.Eof;
|
|
return false;
|
|
}
|
|
|
|
string value = this.m_sb.ToString();
|
|
|
|
// NOTE (steveb): replace any nested CDATA sections endings
|
|
value = value.Replace("<![CDATA[", string.Empty);
|
|
value = value.Replace("]]>", string.Empty);
|
|
value = value.Replace("/**/", string.Empty);
|
|
|
|
Push(null, XmlNodeType.CDATA, value);
|
|
if (this.m_partial == '\0')
|
|
this.m_partial = ' ';// force it to pop this CDATA next time in.
|
|
|
|
return true;
|
|
}
|
|
|
|
private void ExpandEntity(StringBuilder sb, char terminator)
|
|
{
|
|
char ch = this.m_current.ReadChar();
|
|
if (ch == '#')
|
|
{
|
|
string charent = this.m_current.ExpandCharEntity();
|
|
sb.Append(charent);
|
|
ch = this.m_current.Lastchar;
|
|
}
|
|
else
|
|
{
|
|
this.m_name.Length = 0;
|
|
while (ch != Entity.EOF &&
|
|
(char.IsLetter(ch) || ch == '_' || ch == '-') || ((this.m_name.Length > 0) && char.IsDigit(ch)))
|
|
{
|
|
this.m_name.Append(ch);
|
|
ch = this.m_current.ReadChar();
|
|
}
|
|
string name = this.m_name.ToString();
|
|
|
|
// TODO (steveb): don't lookup amp, gt, lt, quote
|
|
switch (name)
|
|
{
|
|
case "amp":
|
|
sb.Append("&");
|
|
if (ch != terminator && ch != '&' && ch != Entity.EOF)
|
|
ch = this.m_current.ReadChar();
|
|
return;
|
|
case "lt":
|
|
sb.Append("<");
|
|
if (ch != terminator && ch != '&' && ch != Entity.EOF)
|
|
ch = this.m_current.ReadChar();
|
|
return;
|
|
case "gt":
|
|
sb.Append(">");
|
|
if (ch != terminator && ch != '&' && ch != Entity.EOF)
|
|
ch = this.m_current.ReadChar();
|
|
return;
|
|
case "quot":
|
|
sb.Append("\"");
|
|
if (ch != terminator && ch != '&' && ch != Entity.EOF)
|
|
ch = this.m_current.ReadChar();
|
|
return;
|
|
case "apos":
|
|
sb.Append("'");
|
|
if (ch != terminator && ch != '&' && ch != Entity.EOF)
|
|
ch = this.m_current.ReadChar();
|
|
return;
|
|
}
|
|
|
|
if (this.m_dtd != null && !string.IsNullOrEmpty(name))
|
|
{
|
|
Entity e = (Entity)this.m_dtd.FindEntity(name);
|
|
if (e != null)
|
|
{
|
|
if (e.IsInternal)
|
|
{
|
|
sb.Append(e.Literal);
|
|
if (ch != terminator && ch != '&' && ch != Entity.EOF)
|
|
ch = this.m_current.ReadChar();
|
|
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
Entity ex = new Entity(name, e.PublicId, e.Uri, this.m_current.Proxy);
|
|
e.Open(this.m_current, new Uri(e.Uri));
|
|
this.m_current = ex;
|
|
this.m_current.ReadChar();
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log("Undefined entity '{0}'", name);
|
|
}
|
|
}
|
|
// Entity is not defined, so just keep it in with the rest of the
|
|
// text.
|
|
sb.Append("&");
|
|
sb.Append(name);
|
|
if (ch != terminator && ch != '&' && ch != Entity.EOF)
|
|
{
|
|
sb.Append(ch);
|
|
ch = this.m_current.ReadChar();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the reader is positioned at the end of the stream.
|
|
/// </summary>
|
|
/// <value>true if the reader is positioned at the end of the stream; otherwise, false.</value>
|
|
public override bool EOF
|
|
{
|
|
get
|
|
{
|
|
return this.m_state == State.Eof;
|
|
}
|
|
}
|
|
|
|
#if !PORTABLE
|
|
public override void Close()
|
|
{
|
|
((IDisposable)this).Dispose();
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Changes the <see cref="ReadState"/> to Closed.
|
|
/// </summary>
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
base.Dispose(disposing);
|
|
if (!disposing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (this.m_current != null)
|
|
{
|
|
this.m_current.Close();
|
|
this.m_current = null;
|
|
}
|
|
|
|
if (this.m_log != null)
|
|
{
|
|
this.m_log.Dispose();
|
|
this.m_log = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the state of the reader.
|
|
/// </summary>
|
|
/// <value>One of the ReadState values.</value>
|
|
public override ReadState ReadState
|
|
{
|
|
get
|
|
{
|
|
if (this.m_state == State.Initial)
|
|
return ReadState.Initial;
|
|
else if (this.m_state == State.Eof)
|
|
return ReadState.EndOfFile;
|
|
else
|
|
return ReadState.Interactive;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the contents of an element or text node as a string.
|
|
/// </summary>
|
|
/// <returns>The contents of the element or an empty string.</returns>
|
|
#if PORTABLE
|
|
public override string ReadContentAsString()
|
|
#else
|
|
public override string ReadString()
|
|
#endif
|
|
{
|
|
if (this.m_node.NodeType == XmlNodeType.Element)
|
|
{
|
|
this.m_sb.Length = 0;
|
|
while (Read())
|
|
{
|
|
switch (this.NodeType)
|
|
{
|
|
case XmlNodeType.CDATA:
|
|
case XmlNodeType.SignificantWhitespace:
|
|
case XmlNodeType.Whitespace:
|
|
case XmlNodeType.Text:
|
|
this.m_sb.Append(this.m_node.Value);
|
|
break;
|
|
default:
|
|
return this.m_sb.ToString();
|
|
}
|
|
}
|
|
|
|
return this.m_sb.ToString();
|
|
}
|
|
|
|
return this.m_node.Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads all the content, including markup, as a string.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// All the XML content, including markup, in the current node. If the current node has no children,
|
|
/// an empty string is returned. If the current node is neither an element nor attribute, an empty
|
|
/// string is returned.
|
|
/// </returns>
|
|
public override string ReadInnerXml()
|
|
{
|
|
StringWriter sw = new StringWriter(CultureInfo.InvariantCulture);
|
|
XmlWriter xw = XmlWriter.Create(sw, new XmlWriterSettings { Indent = true, IndentChars = " " });
|
|
switch (this.NodeType)
|
|
{
|
|
case XmlNodeType.Element:
|
|
Read();
|
|
while (!this.EOF && this.NodeType != XmlNodeType.EndElement)
|
|
{
|
|
xw.WriteNode(this, true);
|
|
}
|
|
Read(); // consume the end tag
|
|
break;
|
|
case XmlNodeType.Attribute:
|
|
sw.Write(this.Value);
|
|
break;
|
|
default:
|
|
// return empty string according to XmlReader spec.
|
|
break;
|
|
}
|
|
|
|
#if PORTABLE
|
|
xw.Dispose();
|
|
#else
|
|
xw.Close();
|
|
#endif
|
|
return sw.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the content, including markup, representing this node and all its children.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// If the reader is positioned on an element or an attribute node, this method returns all the XML content, including markup, of the current node and all its children; otherwise, it returns an empty string.
|
|
/// </returns>
|
|
public override string ReadOuterXml()
|
|
{
|
|
StringWriter sw = new StringWriter(CultureInfo.InvariantCulture);
|
|
XmlWriter xw = XmlWriter.Create(sw, new XmlWriterSettings { Indent = true, IndentChars = " " });
|
|
xw.WriteNode(this, true);
|
|
#if PORTABLE
|
|
xw.Dispose();
|
|
#else
|
|
xw.Close();
|
|
#endif
|
|
return sw.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the XmlNameTable associated with this implementation.
|
|
/// </summary>
|
|
/// <value>The XmlNameTable enabling you to get the atomized version of a string within the node.</value>
|
|
public override XmlNameTable NameTable
|
|
{
|
|
get
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a namespace prefix in the current element's scope.
|
|
/// </summary>
|
|
/// <param name="prefix">The prefix whose namespace URI you want to resolve. To match the default namespace, pass an empty string.</param>
|
|
/// <returns>The namespace URI to which the prefix maps or a null reference (Nothing in Visual Basic) if no matching prefix is found.</returns>
|
|
public override string LookupNamespace(string prefix)
|
|
{
|
|
return null; // there are no namespaces in SGML.
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves the entity reference for EntityReference nodes.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException">SgmlReader does not resolve or return entities.</exception>
|
|
public override void ResolveEntity()
|
|
{
|
|
// We never return any entity reference nodes, so this should never be called.
|
|
throw new InvalidOperationException("Not on an entity reference.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the attribute value into one or more Text, EntityReference, or EndEntity nodes.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// true if there are nodes to return. false if the reader is not positioned on an attribute node when the initial call is made or if all the
|
|
/// attribute values have been read. An empty attribute, such as, misc="", returns true with a single node with a value of string.Empty.
|
|
/// </returns>
|
|
public override bool ReadAttributeValue()
|
|
{
|
|
if (this.m_state == State.Attr)
|
|
{
|
|
this.m_state = State.AttrValue;
|
|
return true;
|
|
}
|
|
else if (this.m_state == State.AttrValue)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
throw new InvalidOperationException("Not on an attribute.");
|
|
}
|
|
|
|
private void Validate(Node node)
|
|
{
|
|
if (this.m_dtd != null)
|
|
{
|
|
ElementDecl e = this.m_dtd.FindElement(node.Name);
|
|
if (e != null)
|
|
{
|
|
node.DtdType = e;
|
|
if (e.ContentModel.DeclaredContent == DeclaredContent.EMPTY)
|
|
node.IsEmpty = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ValidateAttribute(Node node, Attribute a)
|
|
{
|
|
ElementDecl e = node.DtdType;
|
|
if (e != null)
|
|
{
|
|
AttDef ad = e.FindAttribute(a.Name);
|
|
if (ad != null)
|
|
{
|
|
a.DtdType = ad;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool ValidAttributeName(string name)
|
|
{
|
|
try
|
|
{
|
|
XmlConvert.VerifyNMTOKEN(name);
|
|
int index = name.IndexOf(':');
|
|
if (index >= 0)
|
|
{
|
|
XmlConvert.VerifyNCName(name.Substring(index + 1));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
catch (XmlException)
|
|
{
|
|
return false;
|
|
}
|
|
catch (ArgumentNullException)
|
|
{
|
|
// (steveb) this is probably a bug in XmlConvert.VerifyNCName when passing in an empty string
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void ValidateContent(Node node)
|
|
{
|
|
if (node.NodeType == XmlNodeType.Element)
|
|
{
|
|
if (!VerifyName(node.Name))
|
|
{
|
|
Pop();
|
|
Push(null, XmlNodeType.Text, "<" + node.Name + ">");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (this.m_dtd != null)
|
|
{
|
|
// See if this element is allowed inside the current element.
|
|
// If it isn't, then auto-close elements until we find one
|
|
// that it is allowed to be in.
|
|
string name = node.Name.ToUpperInvariant(); // DTD is in upper case
|
|
int i = 0;
|
|
int top = this.m_stack.Count - 2;
|
|
if (node.DtdType != null)
|
|
{
|
|
// it is a known element, let's see if it's allowed in the
|
|
// current context.
|
|
for (i = top; i > 0; i--)
|
|
{
|
|
Node n = (Node)this.m_stack[i];
|
|
if (n.IsEmpty)
|
|
continue; // we'll have to pop this one
|
|
ElementDecl f = n.DtdType;
|
|
if (f != null)
|
|
{
|
|
if ((i == 2) && string.Equals(f.Name, "BODY", StringComparison.OrdinalIgnoreCase)) // NOTE (steveb): never close the BODY tag too early
|
|
break;
|
|
else if (string.Equals(f.Name, this.m_dtd.Name, StringComparison.OrdinalIgnoreCase))
|
|
break; // can't pop the root element.
|
|
else if (f.CanContain(name, this.m_dtd))
|
|
{
|
|
break;
|
|
}
|
|
else if (!f.EndTagOptional)
|
|
{
|
|
// If the end tag is not optional then we can't
|
|
// auto-close it. We'll just have to live with the
|
|
// junk we've found and move on.
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Since we don't understand this tag anyway,
|
|
// we might as well allow this content!
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (i == 0)
|
|
{
|
|
// Tag was not found or is not allowed anywhere, ignore it and
|
|
// continue on.
|
|
return;
|
|
}
|
|
else if (i < top)
|
|
{
|
|
Node n = (Node)this.m_stack[top];
|
|
if (i == top - 1 && string.Equals(name, n.Name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// e.g. p not allowed inside p, not an interesting error.
|
|
}
|
|
else
|
|
{
|
|
#if DEBUG
|
|
string closing = "";
|
|
for (int k = top; k >= i + 1; k--)
|
|
{
|
|
if (closing != "") closing += ",";
|
|
Node n2 = (Node)this.m_stack[k];
|
|
closing += "<" + n2.Name + ">";
|
|
}
|
|
Log("Element '{0}' not allowed inside '{1}', closing {2}.", name, n.Name, closing);
|
|
#endif
|
|
}
|
|
|
|
this.m_state = State.AutoClose;
|
|
this.m_newnode = node;
|
|
Pop(); // save this new node until we pop the others
|
|
this.m_poptodepth = i + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |