RaUI/Source/MyDb/HtmlAgilityPack.Shared/HtmlNode.cs
鑫Intel a1d6dce946 ### 2021-08-03更新
------
#### MyDbV4    V3.0.2108.0301
- *.[新增]新增内置HtmlAgilityPack组件。
2021-08-30 19:47:56 +08:00

2700 lines
67 KiB
C#

// Description: Html Agility Pack - HTML Parsers, selectors, traversors, manupulators.
// Website & Documentation: http://html-agility-pack.net
// Forum & Issues: https://github.com/zzzprojects/html-agility-pack
// License: https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE
// More projects: http://www.zzzprojects.com/
// Copyright ?ZZZ Projects Inc. 2014 - 2017. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
// ReSharper disable InconsistentNaming
namespace HtmlAgilityPack
{
/// <summary>
/// Represents an HTML node.
/// </summary>
[DebuggerDisplay("Name: {OriginalName}")]
public partial class HtmlNode
{
#region Consts
internal const string DepthLevelExceptionMessage = "The document is too complex to parse";
#endregion
#region Fields
internal HtmlAttributeCollection _attributes;
internal HtmlNodeCollection _childnodes;
internal HtmlNode _endnode;
private bool _changed;
internal string _innerhtml;
internal int _innerlength;
internal int _innerstartindex;
internal int _line;
internal int _lineposition;
private string _name;
internal int _namelength;
internal int _namestartindex;
internal HtmlNode _nextnode;
internal HtmlNodeType _nodetype;
internal string _outerhtml;
internal int _outerlength;
internal int _outerstartindex;
private string _optimizedName;
internal HtmlDocument _ownerdocument;
internal HtmlNode _parentnode;
internal HtmlNode _prevnode;
internal HtmlNode _prevwithsamename;
internal bool _starttag;
internal int _streamposition;
internal bool _isImplicitEnd;
internal bool _isHideInnerText;
#endregion
#region Static Members
/// <summary>
/// Gets the name of a comment node. It is actually defined as '#comment'.
/// </summary>
public static readonly string HtmlNodeTypeNameComment = "#comment";
/// <summary>
/// Gets the name of the document node. It is actually defined as '#document'.
/// </summary>
public static readonly string HtmlNodeTypeNameDocument = "#document";
/// <summary>
/// Gets the name of a text node. It is actually defined as '#text'.
/// </summary>
public static readonly string HtmlNodeTypeNameText = "#text";
/// <summary>
/// Gets a collection of flags that define specific behaviors for specific element nodes.
/// The table contains a DictionaryEntry list with the lowercase tag name as the Key, and a combination of HtmlElementFlags as the Value.
/// </summary>
public static Dictionary<string, HtmlElementFlag> ElementsFlags;
#endregion
#region Constructors
/// <summary>
/// Initialize HtmlNode. Builds a list of all tags that have special allowances
/// </summary>
static HtmlNode()
{
// tags whose content may be anything
ElementsFlags = new Dictionary<string, HtmlElementFlag>(StringComparer.OrdinalIgnoreCase);
ElementsFlags.Add("script", HtmlElementFlag.CData);
ElementsFlags.Add("style", HtmlElementFlag.CData);
ElementsFlags.Add("noxhtml", HtmlElementFlag.CData); // can't found.
ElementsFlags.Add("textarea", HtmlElementFlag.CData);
ElementsFlags.Add("title", HtmlElementFlag.CData);
// tags that can not contain other tags
ElementsFlags.Add("base", HtmlElementFlag.Empty);
ElementsFlags.Add("link", HtmlElementFlag.Empty);
ElementsFlags.Add("meta", HtmlElementFlag.Empty);
ElementsFlags.Add("isindex", HtmlElementFlag.Empty);
ElementsFlags.Add("hr", HtmlElementFlag.Empty);
ElementsFlags.Add("col", HtmlElementFlag.Empty);
ElementsFlags.Add("img", HtmlElementFlag.Empty);
ElementsFlags.Add("param", HtmlElementFlag.Empty);
ElementsFlags.Add("embed", HtmlElementFlag.Empty);
ElementsFlags.Add("frame", HtmlElementFlag.Empty);
ElementsFlags.Add("wbr", HtmlElementFlag.Empty);
ElementsFlags.Add("bgsound", HtmlElementFlag.Empty);
ElementsFlags.Add("spacer", HtmlElementFlag.Empty);
ElementsFlags.Add("keygen", HtmlElementFlag.Empty);
ElementsFlags.Add("area", HtmlElementFlag.Empty);
ElementsFlags.Add("input", HtmlElementFlag.Empty);
ElementsFlags.Add("basefont", HtmlElementFlag.Empty);
ElementsFlags.Add("source", HtmlElementFlag.Empty);
ElementsFlags.Add("form", HtmlElementFlag.CanOverlap);
//// they sometimes contain, and sometimes they don 't...
//ElementsFlags.Add("option", HtmlElementFlag.Empty);
// tag whose closing tag is equivalent to open tag:
// <p>bla</p>bla will be transformed into <p>bla</p>bla
// <p>bla<p>bla will be transformed into <p>bla<p>bla and not <p>bla></p><p>bla</p> or <p>bla<p>bla</p></p>
//<br> see above
ElementsFlags.Add("br", HtmlElementFlag.Empty | HtmlElementFlag.Closed);
if (!HtmlDocument.DisableBehaviorTagP)
{
ElementsFlags.Add("p", HtmlElementFlag.Empty | HtmlElementFlag.Closed);
}
}
/// <summary>
/// Initializes HtmlNode, providing type, owner and where it exists in a collection
/// </summary>
/// <param name="type"></param>
/// <param name="ownerdocument"></param>
/// <param name="index"></param>
public HtmlNode(HtmlNodeType type, HtmlDocument ownerdocument, int index)
{
_nodetype = type;
_ownerdocument = ownerdocument;
_outerstartindex = index;
switch (type)
{
case HtmlNodeType.Comment:
Name = HtmlNodeTypeNameComment;
_endnode = this;
break;
case HtmlNodeType.Document:
Name = HtmlNodeTypeNameDocument;
_endnode = this;
break;
case HtmlNodeType.Text:
Name = HtmlNodeTypeNameText;
_endnode = this;
break;
}
if (_ownerdocument.Openednodes != null)
{
if (!Closed)
{
// we use the index as the key
// -1 means the node comes from public
if (-1 != index)
{
_ownerdocument.Openednodes.Add(index, this);
}
}
}
if ((-1 != index) || (type == HtmlNodeType.Comment) || (type == HtmlNodeType.Text)) return;
// innerhtml and outerhtml must be calculated
SetChanged();
}
#endregion
#region Properties
/// <summary>
/// Gets the collection of HTML attributes for this node. May not be null.
/// </summary>
public HtmlAttributeCollection Attributes
{
get
{
if (!HasAttributes)
{
_attributes = new HtmlAttributeCollection(this);
}
return _attributes;
}
internal set { _attributes = value; }
}
/// <summary>
/// Gets all the children of the node.
/// </summary>
public HtmlNodeCollection ChildNodes
{
get { return _childnodes ?? (_childnodes = new HtmlNodeCollection(this)); }
internal set { _childnodes = value; }
}
/// <summary>
/// Gets a value indicating if this node has been closed or not.
/// </summary>
public bool Closed
{
get { return (_endnode != null); }
}
/// <summary>
/// Gets the collection of HTML attributes for the closing tag. May not be null.
/// </summary>
public HtmlAttributeCollection ClosingAttributes
{
get { return !HasClosingAttributes ? new HtmlAttributeCollection(this) : _endnode.Attributes; }
}
/// <summary>
/// Gets the closing tag of the node, null if the node is self-closing.
/// </summary>
public HtmlNode EndNode
{
get { return _endnode; }
}
/// <summary>
/// Gets the first child of the node.
/// </summary>
public HtmlNode FirstChild
{
get { return !HasChildNodes ? null : _childnodes[0]; }
}
/// <summary>
/// Gets a value indicating whether the current node has any attributes.
/// </summary>
public bool HasAttributes
{
get
{
if (_attributes == null)
{
return false;
}
if (_attributes.Count <= 0)
{
return false;
}
return true;
}
}
/// <summary>
/// Gets a value indicating whether this node has any child nodes.
/// </summary>
public bool HasChildNodes
{
get
{
if (_childnodes == null)
{
return false;
}
if (_childnodes.Count <= 0)
{
return false;
}
return true;
}
}
/// <summary>
/// Gets a value indicating whether the current node has any attributes on the closing tag.
/// </summary>
public bool HasClosingAttributes
{
get
{
if ((_endnode == null) || (_endnode == this))
{
return false;
}
if (_endnode._attributes == null)
{
return false;
}
if (_endnode._attributes.Count <= 0)
{
return false;
}
return true;
}
}
/// <summary>
/// Gets or sets the value of the 'id' HTML attribute. The document must have been parsed using the OptionUseIdAttribute set to true.
/// </summary>
public string Id
{
get
{
if (_ownerdocument.Nodesid == null)
throw new Exception(HtmlDocument.HtmlExceptionUseIdAttributeFalse);
return GetId();
}
set
{
if (_ownerdocument.Nodesid == null)
throw new Exception(HtmlDocument.HtmlExceptionUseIdAttributeFalse);
if (value == null)
throw new ArgumentNullException("value");
SetId(value);
}
}
/// <summary>
/// Gets or Sets the HTML between the start and end tags of the object.
/// </summary>
public virtual string InnerHtml
{
get
{
if (_changed)
{
UpdateHtml();
return _innerhtml;
}
if (_innerhtml != null)
return _innerhtml;
if (_innerstartindex < 0 || _innerlength < 0)
return string.Empty;
return _ownerdocument.Text.Substring(_innerstartindex, _innerlength);
}
set
{
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(value);
RemoveAllChildren();
AppendChildren(doc.DocumentNode.ChildNodes);
}
}
/// <summary>
/// Gets the text between the start and end tags of the object.
/// </summary>
public virtual string InnerText
{
get
{
var sb = new StringBuilder();
int depthLevel = 0;
string name = this.Name;
if (name != null)
{
name = name.ToLowerInvariant();
bool isDisplayScriptingText = (name == "head" || name == "script" || name == "style");
InternalInnerText(sb, isDisplayScriptingText, depthLevel);
}
else
{
InternalInnerText(sb, false, depthLevel);
}
return sb.ToString();
}
}
internal virtual void InternalInnerText(StringBuilder sb, bool isDisplayScriptingText, int depthLevel)
{
depthLevel++;
if (depthLevel > HtmlDocument.MaxDepthLevel)
{
throw new Exception($"Maximum deep level reached: {HtmlDocument.MaxDepthLevel}");
}
if (!_ownerdocument.BackwardCompatibility)
{
if (HasChildNodes)
{
AppendInnerText(sb, isDisplayScriptingText);
return;
}
sb.Append(GetCurrentNodeText());
return;
}
if (_nodetype == HtmlNodeType.Text)
{
sb.Append(((HtmlTextNode) this).Text);
return;
}
// Don't display comment or comment child nodes
if (_nodetype == HtmlNodeType.Comment)
{
return;
}
// note: right now, this method is *slow*, because we recompute everything.
// it could be optimized like innerhtml
if (!HasChildNodes || (_isHideInnerText && !isDisplayScriptingText))
{
return;
}
foreach (HtmlNode node in ChildNodes)
node.InternalInnerText(sb, isDisplayScriptingText, depthLevel);
}
/// <summary>Gets direct inner text.</summary>
/// <returns>The direct inner text.</returns>
public virtual string GetDirectInnerText()
{
if (!_ownerdocument.BackwardCompatibility)
{
if (HasChildNodes)
{
StringBuilder sb = new StringBuilder();
AppendDirectInnerText(sb);
return sb.ToString();
}
return GetCurrentNodeText();
}
if (_nodetype == HtmlNodeType.Text)
return ((HtmlTextNode)this).Text;
// Don't display comment or comment child nodes
if (_nodetype == HtmlNodeType.Comment)
return "";
if (!HasChildNodes)
return string.Empty;
var s = new StringBuilder();
foreach (HtmlNode node in ChildNodes)
{
if (node._nodetype == HtmlNodeType.Text)
{
s.Append(((HtmlTextNode)node).Text);
}
}
return s.ToString();
}
internal string GetCurrentNodeText()
{
if (_nodetype == HtmlNodeType.Text)
{
string s = ((HtmlTextNode) this).Text;
if (ParentNode.Name != "pre")
{
// Make some test...
s = s.Replace("\n", "").Replace("\r", "").Replace("\t", "");
}
return s;
}
return "";
}
internal void AppendDirectInnerText(StringBuilder sb)
{
if (_nodetype == HtmlNodeType.Text)
{
sb.Append(GetCurrentNodeText());
}
if (!HasChildNodes) return;
foreach (HtmlNode node in ChildNodes)
{
sb.Append(node.GetCurrentNodeText());
}
return;
}
internal void AppendInnerText(StringBuilder sb, bool isShowHideInnerText)
{
if (_nodetype == HtmlNodeType.Text)
{
sb.Append(GetCurrentNodeText());
}
if (!HasChildNodes || (_isHideInnerText && !isShowHideInnerText)) return;
foreach (HtmlNode node in ChildNodes)
{
node.AppendInnerText(sb, isShowHideInnerText);
}
}
/// <summary>
/// Gets the last child of the node.
/// </summary>
public HtmlNode LastChild
{
get { return !HasChildNodes ? null : _childnodes[_childnodes.Count - 1]; }
}
/// <summary>
/// Gets the line number of this node in the document.
/// </summary>
public int Line
{
get { return _line; }
internal set { _line = value; }
}
/// <summary>
/// Gets the column number of this node in the document.
/// </summary>
public int LinePosition
{
get { return _lineposition; }
internal set { _lineposition = value; }
}
/// <summary>
/// Gets the stream position of the area between the opening and closing tag of the node, relative to the start of the document.
/// </summary>
public int InnerStartIndex
{
get { return _innerstartindex; }
}
/// <summary>
/// Gets the stream position of the area of the beginning of the tag, relative to the start of the document.
/// </summary>
public int OuterStartIndex
{
get { return _outerstartindex; }
}
/// <summary>
/// Gets the length of the area between the opening and closing tag of the node.
/// </summary>
public int InnerLength
{
get { return InnerHtml.Length; }
}
/// <summary>
/// Gets the length of the entire node, opening and closing tag included.
/// </summary>
public int OuterLength
{
get { return OuterHtml.Length; }
}
/// <summary>
/// Gets or sets this node's name.
/// </summary>
public string Name
{
get
{
if (_optimizedName == null)
{
if (_name == null)
Name = _ownerdocument.Text.Substring(_namestartindex, _namelength);
if (_name == null)
_optimizedName = string.Empty;
else
_optimizedName = _name.ToLowerInvariant();
}
return _optimizedName;
}
set
{
_name = value;
_optimizedName = null;
}
}
/// <summary>
/// Gets the HTML node immediately following this element.
/// </summary>
public HtmlNode NextSibling
{
get { return _nextnode; }
internal set { _nextnode = value; }
}
/// <summary>
/// Gets the type of this node.
/// </summary>
public HtmlNodeType NodeType
{
get { return _nodetype; }
internal set { _nodetype = value; }
}
/// <summary>
/// The original unaltered name of the tag
/// </summary>
public string OriginalName
{
get { return _name; }
}
/// <summary>
/// Gets or Sets the object and its content in HTML.
/// </summary>
public virtual string OuterHtml
{
get
{
if (_changed)
{
UpdateHtml();
return _outerhtml;
}
if (_outerhtml != null)
{
return _outerhtml;
}
if (_outerstartindex < 0 || _outerlength < 0)
{
return string.Empty;
}
return _ownerdocument.Text.Substring(_outerstartindex, _outerlength);
}
}
/// <summary>
/// Gets the <see cref="HtmlDocument"/> to which this node belongs.
/// </summary>
public HtmlDocument OwnerDocument
{
get { return _ownerdocument; }
internal set { _ownerdocument = value; }
}
/// <summary>
/// Gets the parent of this node (for nodes that can have parents).
/// </summary>
public HtmlNode ParentNode
{
get { return _parentnode; }
internal set { _parentnode = value; }
}
/// <summary>
/// Gets the node immediately preceding this node.
/// </summary>
public HtmlNode PreviousSibling
{
get { return _prevnode; }
internal set { _prevnode = value; }
}
/// <summary>
/// Gets the stream position of this node in the document, relative to the start of the document.
/// </summary>
public int StreamPosition
{
get { return _streamposition; }
}
/// <summary>
/// Gets a valid XPath string that points to this node
/// </summary>
public string XPath
{
get
{
string basePath = (ParentNode == null || ParentNode.NodeType == HtmlNodeType.Document)
? "/"
: ParentNode.XPath + "/";
return basePath + GetRelativeXpath();
}
}
/// <summary>
/// The depth of the node relative to the opening root html element. This value is used to determine if a document has to many nested html nodes which can cause stack overflows
/// </summary>
public int Depth { get; set; }
#endregion
#region Public Methods
/// <summary>
/// Determines if an element node can be kept overlapped.
/// </summary>
/// <param name="name">The name of the element node to check. May not be <c>null</c>.</param>
/// <returns>true if the name is the name of an element node that can be kept overlapped, <c>false</c> otherwise.</returns>
public static bool CanOverlapElement(string name)
{
if (name == null)
{
throw new ArgumentNullException("name");
}
HtmlElementFlag flag;
if (!ElementsFlags.TryGetValue(name, out flag))
{
return false;
}
return (flag & HtmlElementFlag.CanOverlap) != 0;
}
/// <summary>
/// Creates an HTML node from a string representing literal HTML.
/// </summary>
/// <param name="html">The HTML text.</param>
/// <returns>The newly created node instance.</returns>
public static HtmlNode CreateNode(string html)
{
return CreateNode(html, null);
}
/// <summary>
/// Creates an HTML node from a string representing literal HTML.
/// </summary>
/// <param name="html">The HTML text.</param>
/// <param name="htmlDocumentBuilder">The HTML Document builder.</param>
/// <returns>The newly created node instance.</returns>
public static HtmlNode CreateNode(string html, Action<HtmlDocument> htmlDocumentBuilder)
{
// REVIEW: this is *not* optimum...
HtmlDocument doc = new HtmlDocument();
if (htmlDocumentBuilder != null)
{
htmlDocumentBuilder(doc);
}
doc.LoadHtml(html);
if (!doc.DocumentNode.IsSingleElementNode())
{
throw new Exception("Multiple node elments can't be created.");
}
var element = doc.DocumentNode.FirstChild;
while (element != null)
{
if (element.NodeType == HtmlNodeType.Element && element.OuterHtml != "\r\n")
return element;
element = element.NextSibling;
}
return doc.DocumentNode.FirstChild;
}
/// <summary>
/// Determines if an element node is a CDATA element node.
/// </summary>
/// <param name="name">The name of the element node to check. May not be null.</param>
/// <returns>true if the name is the name of a CDATA element node, false otherwise.</returns>
public static bool IsCDataElement(string name)
{
if (name == null)
{
throw new ArgumentNullException("name");
}
HtmlElementFlag flag;
if (!ElementsFlags.TryGetValue(name, out flag))
{
return false;
}
return (flag & HtmlElementFlag.CData) != 0;
}
/// <summary>
/// Determines if an element node is closed.
/// </summary>
/// <param name="name">The name of the element node to check. May not be null.</param>
/// <returns>true if the name is the name of a closed element node, false otherwise.</returns>
public static bool IsClosedElement(string name)
{
if (name == null)
{
throw new ArgumentNullException("name");
}
HtmlElementFlag flag;
if (!ElementsFlags.TryGetValue(name, out flag))
{
return false;
}
return (flag & HtmlElementFlag.Closed) != 0;
}
/// <summary>
/// Determines if an element node is defined as empty.
/// </summary>
/// <param name="name">The name of the element node to check. May not be null.</param>
/// <returns>true if the name is the name of an empty element node, false otherwise.</returns>
public static bool IsEmptyElement(string name)
{
if (name == null)
{
throw new ArgumentNullException("name");
}
if (name.Length == 0)
{
return true;
}
// <!DOCTYPE ...
if ('!' == name[0])
{
return true;
}
// <?xml ...
if ('?' == name[0])
{
return true;
}
HtmlElementFlag flag;
if (!ElementsFlags.TryGetValue(name, out flag))
{
return false;
}
return (flag & HtmlElementFlag.Empty) != 0;
}
/// <summary>
/// Determines if a text corresponds to the closing tag of an node that can be kept overlapped.
/// </summary>
/// <param name="text">The text to check. May not be null.</param>
/// <returns>true or false.</returns>
public static bool IsOverlappedClosingElement(string text)
{
if (text == null)
{
throw new ArgumentNullException("text");
}
// min is </x>: 4
if (text.Length <= 4)
return false;
if ((text[0] != '<') ||
(text[text.Length - 1] != '>') ||
(text[1] != '/'))
return false;
string name = text.Substring(2, text.Length - 3);
return CanOverlapElement(name);
}
/// <summary>
/// Returns a collection of all ancestor nodes of this element.
/// </summary>
/// <returns></returns>
public IEnumerable<HtmlNode> Ancestors()
{
HtmlNode node = ParentNode;
if (node != null)
{
yield return node; //return the immediate parent node
//now look at it's parent and walk up the tree of parents
while (node.ParentNode != null)
{
yield return node.ParentNode;
node = node.ParentNode;
}
}
}
/// <summary>
/// Get Ancestors with matching name
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public IEnumerable<HtmlNode> Ancestors(string name)
{
for (HtmlNode n = ParentNode; n != null; n = n.ParentNode)
if (n.Name == name)
yield return n;
}
/// <summary>
/// Returns a collection of all ancestor nodes of this element.
/// </summary>
/// <returns></returns>
public IEnumerable<HtmlNode> AncestorsAndSelf()
{
for (HtmlNode n = this; n != null; n = n.ParentNode)
yield return n;
}
/// <summary>
/// Gets all anscestor nodes and the current node
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public IEnumerable<HtmlNode> AncestorsAndSelf(string name)
{
for (HtmlNode n = this; n != null; n = n.ParentNode)
if (n.Name == name)
yield return n;
}
/// <summary>
/// Adds the specified node to the end of the list of children of this node.
/// </summary>
/// <param name="newChild">The node to add. May not be null.</param>
/// <returns>The node added.</returns>
public HtmlNode AppendChild(HtmlNode newChild)
{
if (newChild == null)
{
throw new ArgumentNullException("newChild");
}
ChildNodes.Append(newChild);
_ownerdocument.SetIdForNode(newChild, newChild.GetId());
SetChildNodesId(newChild);
SetChanged();
return newChild;
}
/// <summary>Sets child nodes identifier.</summary>
/// <param name="chilNode">The chil node.</param>
public void SetChildNodesId(HtmlNode chilNode)
{
foreach (HtmlNode child in chilNode.ChildNodes)
{
_ownerdocument.SetIdForNode(child, child.GetId());
SetChildNodesId(child);
}
}
/// <summary>
/// Adds the specified node to the end of the list of children of this node.
/// </summary>
/// <param name="newChildren">The node list to add. May not be null.</param>
public void AppendChildren(HtmlNodeCollection newChildren)
{
if (newChildren == null)
throw new ArgumentNullException("newChildren");
foreach (HtmlNode newChild in newChildren)
{
AppendChild(newChild);
}
}
/// <summary>
/// Gets all Attributes with name
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public IEnumerable<HtmlAttribute> ChildAttributes(string name)
{
return Attributes.AttributesWithName(name);
}
/// <summary>
/// Creates a duplicate of the node
/// </summary>
/// <returns></returns>
public HtmlNode Clone()
{
return CloneNode(true);
}
/// <summary>
/// Creates a duplicate of the node and changes its name at the same time.
/// </summary>
/// <param name="newName">The new name of the cloned node. May not be <c>null</c>.</param>
/// <returns>The cloned node.</returns>
public HtmlNode CloneNode(string newName)
{
return CloneNode(newName, true);
}
/// <summary>
/// Creates a duplicate of the node and changes its name at the same time.
/// </summary>
/// <param name="newName">The new name of the cloned node. May not be null.</param>
/// <param name="deep">true to recursively clone the subtree under the specified node; false to clone only the node itself.</param>
/// <returns>The cloned node.</returns>
public HtmlNode CloneNode(string newName, bool deep)
{
if (newName == null)
{
throw new ArgumentNullException("newName");
}
HtmlNode node = CloneNode(deep);
node.Name = newName;
return node;
}
/// <summary>
/// Creates a duplicate of the node.
/// </summary>
/// <param name="deep">true to recursively clone the subtree under the specified node; false to clone only the node itself.</param>
/// <returns>The cloned node.</returns>
public HtmlNode CloneNode(bool deep)
{
HtmlNode node = _ownerdocument.CreateNode(_nodetype);
node.Name = OriginalName;
switch (_nodetype)
{
case HtmlNodeType.Comment:
((HtmlCommentNode) node).Comment = ((HtmlCommentNode) this).Comment;
return node;
case HtmlNodeType.Text:
((HtmlTextNode) node).Text = ((HtmlTextNode) this).Text;
return node;
}
// attributes
if (HasAttributes)
{
foreach (HtmlAttribute att in _attributes)
{
HtmlAttribute newatt = att.Clone();
node.Attributes.Append(newatt);
}
}
// closing attributes
if (HasClosingAttributes)
{
node._endnode = _endnode.CloneNode(false);
foreach (HtmlAttribute att in _endnode._attributes)
{
HtmlAttribute newatt = att.Clone();
node._endnode._attributes.Append(newatt);
}
}
if (!deep)
{
return node;
}
if (!HasChildNodes)
{
return node;
}
// child nodes
foreach (HtmlNode child in _childnodes)
{
HtmlNode newchild = child.CloneNode(deep);
node.AppendChild(newchild);
}
return node;
}
/// <summary>
/// Creates a duplicate of the node and the subtree under it.
/// </summary>
/// <param name="node">The node to duplicate. May not be <c>null</c>.</param>
public void CopyFrom(HtmlNode node)
{
CopyFrom(node, true);
}
/// <summary>
/// Creates a duplicate of the node.
/// </summary>
/// <param name="node">The node to duplicate. May not be <c>null</c>.</param>
/// <param name="deep">true to recursively clone the subtree under the specified node, false to clone only the node itself.</param>
public void CopyFrom(HtmlNode node, bool deep)
{
if (node == null)
{
throw new ArgumentNullException("node");
}
Attributes.RemoveAll();
if (node.HasAttributes)
{
foreach (HtmlAttribute att in node.Attributes)
{
HtmlAttribute newatt = att.Clone();
Attributes.Append(newatt);
}
}
if (deep)
{
RemoveAllChildren();
if (node.HasChildNodes)
{
foreach (HtmlNode child in node.ChildNodes)
{
AppendChild(child.CloneNode(true));
}
}
}
}
/// <summary>
/// Gets all Descendant nodes for this node and each of child nodes
/// </summary>
/// <param name="level">The depth level of the node to parse in the html tree</param>
/// <returns>the current element as an HtmlNode</returns>
[Obsolete("Use Descendants() instead, the results of this function will change in a future version")]
public IEnumerable<HtmlNode> DescendantNodes(int level = 0)
{
if (level > HtmlDocument.MaxDepthLevel)
{
throw new ArgumentException(HtmlNode.DepthLevelExceptionMessage);
}
foreach (HtmlNode node in ChildNodes)
{
yield return node;
foreach (HtmlNode descendant in node.DescendantNodes(level + 1))
{
yield return descendant;
}
}
}
/// <summary>
/// Returns a collection of all descendant nodes of this element, in document order
/// </summary>
/// <returns></returns>
[Obsolete("Use DescendantsAndSelf() instead, the results of this function will change in a future version")]
public IEnumerable<HtmlNode> DescendantNodesAndSelf()
{
return DescendantsAndSelf();
}
/// <summary>
/// Gets all Descendant nodes in enumerated list
/// </summary>
/// <returns></returns>
public IEnumerable<HtmlNode> Descendants()
{
// DO NOT REMOVE, the empty method is required for Fizzler third party library
return Descendants(0);
}
/// <summary>
/// Gets all Descendant nodes in enumerated list
/// </summary>
/// <returns></returns>
public IEnumerable<HtmlNode> Descendants(int level)
{
if (level > HtmlDocument.MaxDepthLevel)
{
throw new ArgumentException(HtmlNode.DepthLevelExceptionMessage);
}
foreach (HtmlNode node in ChildNodes)
{
yield return node;
foreach (HtmlNode descendant in node.Descendants(level + 1))
{
yield return descendant;
}
}
}
/// <summary>
/// Get all descendant nodes with matching name
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public IEnumerable<HtmlNode> Descendants(string name)
{
foreach (HtmlNode node in Descendants())
if (String.Equals(node.Name, name, StringComparison.OrdinalIgnoreCase))
yield return node;
}
/// <summary>
/// Returns a collection of all descendant nodes of this element, in document order
/// </summary>
/// <returns></returns>
public IEnumerable<HtmlNode> DescendantsAndSelf()
{
yield return this;
foreach (HtmlNode n in Descendants())
{
HtmlNode el = n;
if (el != null)
yield return el;
}
}
/// <summary>
/// Gets all descendant nodes including this node
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public IEnumerable<HtmlNode> DescendantsAndSelf(string name)
{
yield return this;
foreach (HtmlNode node in Descendants())
if (node.Name == name)
yield return node;
}
/// <summary>
/// Gets first generation child node matching name
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public HtmlNode Element(string name)
{
foreach (HtmlNode node in ChildNodes)
if (node.Name == name)
return node;
return null;
}
/// <summary>
/// Gets matching first generation child nodes matching name
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public IEnumerable<HtmlNode> Elements(string name)
{
foreach (HtmlNode node in ChildNodes)
if (node.Name == name)
yield return node;
}
/// <summary>Gets data attribute.</summary>
/// <param name="key">The key.</param>
/// <returns>The data attribute.</returns>
public HtmlAttribute GetDataAttribute(string key)
{
return Attributes.Hashitems.SingleOrDefault(x => x.Key.Equals("data-" + key, StringComparison.OrdinalIgnoreCase)).Value;
}
/// <summary>Gets the data attributes in this collection.</summary>
/// <returns>
/// An enumerator that allows foreach to be used to process the data attributes in this
/// collection.
/// </returns>
public IEnumerable<HtmlAttribute> GetDataAttributes()
{
return Attributes.Hashitems.Where(x => x.Key.StartsWith("data-", StringComparison.OrdinalIgnoreCase)).Select(x => x.Value).ToList();
}
/// <summary>Gets the attributes in this collection.</summary>
/// <returns>
/// An enumerator that allows foreach to be used to process the attributes in this collection.
/// </returns>
public IEnumerable<HtmlAttribute> GetAttributes()
{
return Attributes.items;
}
/// <summary>Gets the attributes in this collection.</summary>
/// <param name="attributeNames">A variable-length parameters list containing attribute names.</param>
/// <returns>
/// An enumerator that allows foreach to be used to process the attributes in this collection.
/// </returns>
public IEnumerable<HtmlAttribute> GetAttributes(params string[] attributeNames)
{
List<HtmlAttribute> list = new List<HtmlAttribute>();
foreach(var name in attributeNames)
{
list.Add(Attributes[name]);
}
return list;
}
/// <summary>
/// Helper method to get the value of an attribute of this node. If the attribute is not found, the default value will be returned.
/// </summary>
/// <param name="name">The name of the attribute to get. May not be <c>null</c>.</param>
/// <param name="def">The default value to return if not found.</param>
/// <returns>The value of the attribute if found, the default value if not found.</returns>
public string GetAttributeValue(string name, string def)
{
#if METRO || NETSTANDARD1_3 || NETSTANDARD1_6
if (name == null)
{
throw new ArgumentNullException("name");
}
if (!HasAttributes)
{
return def;
}
HtmlAttribute att = Attributes[name];
if (att == null)
{
return def;
}
return att.Value;
#else
return GetAttributeValue<string>(name, def);
#endif
}
/// <summary>
/// Helper method to get the value of an attribute of this node. If the attribute is not found, the default value will be returned.
/// </summary>
/// <param name="name">The name of the attribute to get. May not be <c>null</c>.</param>
/// <param name="def">The default value to return if not found.</param>
/// <returns>The value of the attribute if found, the default value if not found.</returns>
public int GetAttributeValue(string name, int def)
{
#if METRO || NETSTANDARD1_3 || NETSTANDARD1_6
if (name == null)
{
throw new ArgumentNullException("name");
}
if (!HasAttributes)
{
return def;
}
HtmlAttribute att = Attributes[name];
if (att == null)
{
return def;
}
try
{
return Convert.ToInt32(att.Value);
}
catch
{
return def;
}
#else
return GetAttributeValue<int>(name, def);
#endif
}
/// <summary>
/// Helper method to get the value of an attribute of this node. If the attribute is not found, the default value will be returned.
/// </summary>
/// <param name="name">The name of the attribute to get. May not be <c>null</c>.</param>
/// <param name="def">The default value to return if not found.</param>
/// <returns>The value of the attribute if found, the default value if not found.</returns>
public bool GetAttributeValue(string name, bool def)
{
#if METRO || NETSTANDARD1_3 || NETSTANDARD1_6
if (name == null)
{
throw new ArgumentNullException("name");
}
if (!HasAttributes)
{
return def;
}
HtmlAttribute att = Attributes[name];
if (att == null)
{
return def;
}
try
{
return Convert.ToBoolean(att.Value);
}
catch
{
return def;
}
#else
return GetAttributeValue<bool>(name, def);
#endif
}
#if !(METRO || NETSTANDARD1_3 || NETSTANDARD1_6)
/// <summary>
/// Helper method to get the value of an attribute of this node. If the attribute is not found,
/// the default value will be returned.
/// </summary>
/// <param name="name">The name of the attribute to get. May not be <c>null</c>.</param>
/// <param name="def">The default value to return if not found.</param>
/// <returns>The value of the attribute if found, the default value if not found.</returns>
public T GetAttributeValue<T>(string name, T def)
{
if (name == null)
{
throw new ArgumentNullException("name");
}
if (!HasAttributes)
{
return def;
}
HtmlAttribute att = Attributes[name];
if (att == null)
{
return def;
}
try
{
return (T)att.Value.To(typeof(T));
}
catch
{
return def;
}
}
#endif
/// <summary>
/// Inserts the specified node immediately after the specified reference node.
/// </summary>
/// <param name="newChild">The node to insert. May not be <c>null</c>.</param>
/// <param name="refChild">The node that is the reference node. The newNode is placed after the refNode.</param>
/// <returns>The node being inserted.</returns>
public HtmlNode InsertAfter(HtmlNode newChild, HtmlNode refChild)
{
if (newChild == null)
{
throw new ArgumentNullException("newChild");
}
if (refChild == null)
{
return PrependChild(newChild);
}
if (newChild == refChild)
{
return newChild;
}
int index = -1;
if (_childnodes != null)
{
index = _childnodes[refChild];
}
if (index == -1)
{
throw new ArgumentException(HtmlDocument.HtmlExceptionRefNotChild);
}
if (_childnodes != null) _childnodes.Insert(index + 1, newChild);
_ownerdocument.SetIdForNode(newChild, newChild.GetId());
SetChildNodesId(newChild);
SetChanged();
return newChild;
}
/// <summary>
/// Inserts the specified node immediately before the specified reference node.
/// </summary>
/// <param name="newChild">The node to insert. May not be <c>null</c>.</param>
/// <param name="refChild">The node that is the reference node. The newChild is placed before this node.</param>
/// <returns>The node being inserted.</returns>
public HtmlNode InsertBefore(HtmlNode newChild, HtmlNode refChild)
{
if (newChild == null)
{
throw new ArgumentNullException("newChild");
}
if (refChild == null)
{
return AppendChild(newChild);
}
if (newChild == refChild)
{
return newChild;
}
int index = -1;
if (_childnodes != null)
{
index = _childnodes[refChild];
}
if (index == -1)
{
throw new ArgumentException(HtmlDocument.HtmlExceptionRefNotChild);
}
if (_childnodes != null) _childnodes.Insert(index, newChild);
_ownerdocument.SetIdForNode(newChild, newChild.GetId());
SetChildNodesId(newChild);
SetChanged();
return newChild;
}
/// <summary>
/// Adds the specified node to the beginning of the list of children of this node.
/// </summary>
/// <param name="newChild">The node to add. May not be <c>null</c>.</param>
/// <returns>The node added.</returns>
public HtmlNode PrependChild(HtmlNode newChild)
{
if (newChild == null)
{
throw new ArgumentNullException("newChild");
}
ChildNodes.Prepend(newChild);
_ownerdocument.SetIdForNode(newChild, newChild.GetId());
SetChildNodesId(newChild);
SetChanged();
return newChild;
}
/// <summary>
/// Adds the specified node list to the beginning of the list of children of this node.
/// </summary>
/// <param name="newChildren">The node list to add. May not be <c>null</c>.</param>
public void PrependChildren(HtmlNodeCollection newChildren)
{
if (newChildren == null)
{
throw new ArgumentNullException("newChildren");
}
for (int i = newChildren.Count - 1; i >= 0; i--)
{
PrependChild(newChildren[i]);
}
}
/// <summary>
/// Removes node from parent collection
/// </summary>
public void Remove()
{
if (ParentNode != null)
{
ParentNode.ChildNodes.Remove(this);
}
}
/// <summary>
/// Removes all the children and/or attributes of the current node.
/// </summary>
public void RemoveAll()
{
RemoveAllChildren();
if (HasAttributes)
{
_attributes.Clear();
}
if ((_endnode != null) && (_endnode != this))
{
if (_endnode._attributes != null)
{
_endnode._attributes.Clear();
}
}
SetChanged();
}
/// <summary>
/// Removes all the children of the current node.
/// </summary>
public void RemoveAllChildren()
{
if (!HasChildNodes)
{
return;
}
if (_ownerdocument.OptionUseIdAttribute)
{
// remove nodes from id list
foreach (HtmlNode node in _childnodes)
{
_ownerdocument.SetIdForNode(null, node.GetId());
RemoveAllIDforNode(node);
}
}
_childnodes.Clear();
SetChanged();
}
/// <summary>Removes all id for node described by node.</summary>
/// <param name="node">The node.</param>
public void RemoveAllIDforNode(HtmlNode node)
{
foreach (HtmlNode nodeChildNode in node.ChildNodes)
{
_ownerdocument.SetIdForNode(null, nodeChildNode.GetId());
RemoveAllIDforNode(nodeChildNode);
}
}
/// <summary>Move a node already associated and append it to this node instead.</summary>
/// <param name="child">The child node to move.</param>
public void MoveChild(HtmlNode child)
{
if (child == null)
{
throw new ArgumentNullException($"Oops! the '{nameof(child)}' parameter cannot be null.");
}
var oldParent = child.ParentNode;
AppendChild(child);
if (oldParent != null)
{
oldParent.RemoveChild(child);
}
}
/// <summary>Move a children collection already associated and append it to this node instead.</summary>
/// <param name="children">The children collection already associated to move to another node.</param>
public void MoveChildren(HtmlNodeCollection children)
{
if (children == null)
{
throw new ArgumentNullException($"Oops! the '{nameof(children)}' parameter cannot be null.");
}
var oldParent = children.ParentNode;
AppendChildren(children);
if (oldParent != null)
{
oldParent.RemoveChildren(children);
}
}
/// <summary>Removes the children collection for this node.</summary>
/// <param name="oldChildren">The old children collection to remove.</param>
public void RemoveChildren(HtmlNodeCollection oldChildren)
{
if (oldChildren == null)
{
throw new ArgumentNullException($"Oops! the '{nameof(oldChildren)}' parameter cannot be null.");
}
var list = oldChildren.ToList();
foreach (HtmlNode newChild in list)
{
RemoveChild(newChild);
}
}
/// <summary>
/// Removes the specified child node.
/// </summary>
/// <param name="oldChild">The node being removed. May not be <c>null</c>.</param>
/// <returns>The node removed.</returns>
public HtmlNode RemoveChild(HtmlNode oldChild)
{
if (oldChild == null)
{
throw new ArgumentNullException("oldChild");
}
int index = -1;
if (_childnodes != null)
{
index = _childnodes[oldChild];
}
if (index == -1)
{
throw new ArgumentException(HtmlDocument.HtmlExceptionRefNotChild);
}
if (_childnodes != null)
_childnodes.Remove(index);
_ownerdocument.SetIdForNode(null, oldChild.GetId());
RemoveAllIDforNode(oldChild);
SetChanged();
return oldChild;
}
/// <summary>
/// Removes the specified child node.
/// </summary>
/// <param name="oldChild">The node being removed. May not be <c>null</c>.</param>
/// <param name="keepGrandChildren">true to keep grand children of the node, false otherwise.</param>
/// <returns>The node removed.</returns>
public HtmlNode RemoveChild(HtmlNode oldChild, bool keepGrandChildren)
{
if (oldChild == null)
{
throw new ArgumentNullException("oldChild");
}
if ((oldChild._childnodes != null) && keepGrandChildren)
{
// get prev sibling
HtmlNode prev = oldChild.PreviousSibling;
// reroute grand children to ourselves
foreach (HtmlNode grandchild in oldChild._childnodes)
{
prev = InsertAfter(grandchild, prev);
}
}
RemoveChild(oldChild);
SetChanged();
return oldChild;
}
/// <summary>
/// Replaces the child node oldChild with newChild node.
/// </summary>
/// <param name="newChild">The new node to put in the child list.</param>
/// <param name="oldChild">The node being replaced in the list.</param>
/// <returns>The node replaced.</returns>
public HtmlNode ReplaceChild(HtmlNode newChild, HtmlNode oldChild)
{
if (newChild == null)
{
return RemoveChild(oldChild);
}
if (oldChild == null)
{
return AppendChild(newChild);
}
int index = -1;
if (_childnodes != null)
{
index = _childnodes[oldChild];
}
if (index == -1)
{
throw new ArgumentException(HtmlDocument.HtmlExceptionRefNotChild);
}
if (_childnodes != null) _childnodes.Replace(index, newChild);
_ownerdocument.SetIdForNode(null, oldChild.GetId());
RemoveAllIDforNode(oldChild);
_ownerdocument.SetIdForNode(newChild, newChild.GetId());
SetChildNodesId(newChild);
SetChanged();
return newChild;
}
/// <summary>
/// Helper method to set the value of an attribute of this node. If the attribute is not found, it will be created automatically.
/// </summary>
/// <param name="name">The name of the attribute to set. May not be null.</param>
/// <param name="value">The value for the attribute.</param>
/// <returns>The corresponding attribute instance.</returns>
public HtmlAttribute SetAttributeValue(string name, string value)
{
if (name == null)
{
throw new ArgumentNullException("name");
}
HtmlAttribute att = Attributes[name];
if (att == null)
{
return Attributes.Append(_ownerdocument.CreateAttribute(name, value));
}
att.Value = value;
return att;
}
/// <summary>
/// Saves all the children of the node to the specified TextWriter.
/// </summary>
/// <param name="outText">The TextWriter to which you want to save.</param>
/// <param name="level">Identifies the level we are in starting at root with 0</param>
public void WriteContentTo(TextWriter outText, int level = 0)
{
if (level > HtmlDocument.MaxDepthLevel)
{
throw new ArgumentException(HtmlNode.DepthLevelExceptionMessage);
}
if (_childnodes == null)
{
return;
}
foreach (HtmlNode node in _childnodes)
{
node.WriteTo(outText, level + 1);
}
}
/// <summary>
/// Saves all the children of the node to a string.
/// </summary>
/// <returns>The saved string.</returns>
public string WriteContentTo()
{
StringWriter sw = new StringWriter();
WriteContentTo(sw);
sw.Flush();
return sw.ToString();
}
/// <summary>
/// Saves the current node to the specified TextWriter.
/// </summary>
/// <param name="outText">The TextWriter to which you want to save.</param>
/// <param name="level">identifies the level we are in starting at root with 0</param>
public virtual void WriteTo(TextWriter outText, int level = 0)
{
string html;
switch (_nodetype)
{
case HtmlNodeType.Comment:
html = ((HtmlCommentNode) this).Comment;
if (_ownerdocument.OptionOutputAsXml)
{
var commentNode = (HtmlCommentNode) this;
if (!_ownerdocument.BackwardCompatibility && commentNode.Comment.ToLowerInvariant().StartsWith("<!doctype"))
{
outText.Write(commentNode.Comment);
}
else
{
if (OwnerDocument.OptionXmlForceOriginalComment)
{
outText.Write(commentNode.Comment);
}
else
{
outText.Write("<!--" + GetXmlComment(commentNode) + " -->");
}
}
}
else
outText.Write(html);
break;
case HtmlNodeType.Document:
if (_ownerdocument.OptionOutputAsXml)
{
#if SILVERLIGHT || PocketPC || METRO || NETSTANDARD1_3 || NETSTANDARD1_6
outText.Write("<?xml version=\"1.0\" encoding=\"" + _ownerdocument.GetOutEncoding().WebName + "\"?>");
#else
outText.Write("<?xml version=\"1.0\" encoding=\"" + _ownerdocument.GetOutEncoding().BodyName + "\"?>");
#endif
// check there is a root element
if (_ownerdocument.DocumentNode.HasChildNodes)
{
int rootnodes = _ownerdocument.DocumentNode._childnodes.Count;
if (rootnodes > 0)
{
HtmlNode xml = _ownerdocument.GetXmlDeclaration();
if (xml != null)
rootnodes--;
if (rootnodes > 1)
{
if (!_ownerdocument.BackwardCompatibility)
{
WriteContentTo(outText, level);
}
else
{
if (_ownerdocument.OptionOutputUpperCase)
{
outText.Write("<SPAN>");
WriteContentTo(outText, level);
outText.Write("</SPAN>");
}
else
{
outText.Write("<span>");
WriteContentTo(outText, level);
outText.Write("</span>");
}
}
break;
}
}
}
}
WriteContentTo(outText, level);
break;
case HtmlNodeType.Text:
html = ((HtmlTextNode) this).Text;
outText.Write(_ownerdocument.OptionOutputAsXml ? HtmlDocument.HtmlEncodeWithCompatibility(html, _ownerdocument.BackwardCompatibility) : html);
break;
case HtmlNodeType.Element:
string name = _ownerdocument.OptionOutputUpperCase ? Name.ToUpperInvariant() : Name;
if (_ownerdocument.OptionOutputOriginalCase)
name = OriginalName;
if (_ownerdocument.OptionOutputAsXml)
{
if (name.Length > 0)
{
if (name[0] == '?')
// forget this one, it's been done at the document level
break;
if (name.Trim().Length == 0)
break;
name = HtmlDocument.GetXmlName(name, false, _ownerdocument.OptionPreserveXmlNamespaces);
}
else
break;
}
outText.Write("<" + name);
WriteAttributes(outText, false);
if (HasChildNodes)
{
outText.Write(">");
bool cdata = false;
if (_ownerdocument.OptionOutputAsXml && IsCDataElement(Name))
{
// this code and the following tries to output things as nicely as possible for old browsers.
cdata = true;
outText.Write("\r\n//<![CDATA[\r\n");
}
if (cdata)
{
if (HasChildNodes)
// child must be a text
ChildNodes[0].WriteTo(outText, level);
outText.Write("\r\n//]]>//\r\n");
}
else
WriteContentTo(outText, level);
if (_ownerdocument.OptionOutputAsXml || !_isImplicitEnd)
{
outText.Write("</" + name);
if (!_ownerdocument.OptionOutputAsXml)
WriteAttributes(outText, true);
outText.Write(">");
}
}
else
{
if (IsEmptyElement(Name))
{
if ((_ownerdocument.OptionWriteEmptyNodes) || (_ownerdocument.OptionOutputAsXml))
outText.Write(" />");
else
{
if (Name.Length > 0 && Name[0] == '?')
outText.Write("?");
outText.Write(">");
}
}
else
{
if (!_isImplicitEnd)
{
outText.Write("></" + name + ">");
}
else
{
outText.Write(">");
}
}
}
break;
}
}
/// <summary>
/// Saves the current node to the specified XmlWriter.
/// </summary>
/// <param name="writer">The XmlWriter to which you want to save.</param>
public void WriteTo(XmlWriter writer)
{
switch (_nodetype)
{
case HtmlNodeType.Comment:
writer.WriteComment(GetXmlComment((HtmlCommentNode) this));
break;
case HtmlNodeType.Document:
#if SILVERLIGHT || PocketPC || METRO || NETSTANDARD1_3 || NETSTANDARD1_6
writer.WriteProcessingInstruction("xml",
"version=\"1.0\" encoding=\"" +
_ownerdocument.GetOutEncoding().WebName + "\"");
#else
writer.WriteProcessingInstruction("xml",
"version=\"1.0\" encoding=\"" +
_ownerdocument.GetOutEncoding().BodyName + "\"");
#endif
if (HasChildNodes)
{
foreach (HtmlNode subnode in ChildNodes)
{
subnode.WriteTo(writer);
}
}
break;
case HtmlNodeType.Text:
string html = ((HtmlTextNode) this).Text;
writer.WriteString(html);
break;
case HtmlNodeType.Element:
string name = _ownerdocument.OptionOutputUpperCase ? Name.ToUpperInvariant() : Name;
if (_ownerdocument.OptionOutputOriginalCase)
name = OriginalName;
writer.WriteStartElement(name);
WriteAttributes(writer, this);
if (HasChildNodes)
{
foreach (HtmlNode subnode in ChildNodes)
{
subnode.WriteTo(writer);
}
}
writer.WriteEndElement();
break;
}
}
/// <summary>
/// Saves the current node to a string.
/// </summary>
/// <returns>The saved string.</returns>
public string WriteTo()
{
using (StringWriter sw = new StringWriter())
{
WriteTo(sw);
sw.Flush();
return sw.ToString();
}
}
/// <summary>
/// Sets the parent Html node and properly determines the current node's depth using the parent node's depth.
/// </summary>
public void SetParent(HtmlNode parent)
{
if (parent == null)
return;
ParentNode = parent;
if (OwnerDocument.OptionMaxNestedChildNodes > 0)
{
Depth = parent.Depth + 1;
if (Depth > OwnerDocument.OptionMaxNestedChildNodes)
throw new Exception(string.Format("Document has more than {0} nested tags. This is likely due to the page not closing tags properly.", OwnerDocument.OptionMaxNestedChildNodes));
}
}
#endregion
#region Internal Methods
internal void SetChanged()
{
_changed = true;
if (ParentNode != null)
{
ParentNode.SetChanged();
}
}
private void UpdateHtml()
{
_innerhtml = WriteContentTo();
_outerhtml = WriteTo();
_changed = false;
}
internal static string GetXmlComment(HtmlCommentNode comment)
{
string s = comment.Comment;
s = s.Substring(4, s.Length - 7).Replace("--", " - -");
return s;
}
internal static void WriteAttributes(XmlWriter writer, HtmlNode node)
{
if (!node.HasAttributes)
{
return;
}
// we use Hashitems to make sure attributes are written only once
foreach (HtmlAttribute att in node.Attributes.Hashitems.Values)
{
writer.WriteAttributeString(att.XmlName, att.Value);
}
}
internal void UpdateLastNode()
{
HtmlNode newLast = null;
if (_prevwithsamename == null || !_prevwithsamename._starttag)
{
if (_ownerdocument.Openednodes != null)
{
foreach (var openNode in _ownerdocument.Openednodes)
{
if ((openNode.Key < _outerstartindex || openNode.Key > (_outerstartindex + _outerlength)) && openNode.Value._name == _name)
{
if (newLast == null && openNode.Value._starttag)
{
newLast = openNode.Value;
}
else if (newLast != null && newLast.InnerStartIndex < openNode.Key && openNode.Value._starttag)
{
newLast = openNode.Value;
}
}
}
}
}
else
{
newLast = _prevwithsamename;
}
if (newLast != null)
{
_ownerdocument.Lastnodes[newLast.Name] = newLast;
}
}
internal void CloseNode(HtmlNode endnode, int level = 0)
{
if (level > HtmlDocument.MaxDepthLevel)
{
throw new ArgumentException(HtmlNode.DepthLevelExceptionMessage);
}
if (!_ownerdocument.OptionAutoCloseOnEnd)
{
// close all children
if (_childnodes != null)
{
foreach (HtmlNode child in _childnodes)
{
if (child.Closed)
continue;
// create a fake closer node
HtmlNode close = new HtmlNode(NodeType, _ownerdocument, -1);
close._endnode = close;
child.CloseNode(close, level + 1);
}
}
}
if (!Closed)
{
_endnode = endnode;
if (_ownerdocument.Openednodes != null)
_ownerdocument.Openednodes.Remove(_outerstartindex);
HtmlNode self = Utilities.GetDictionaryValueOrDefault(_ownerdocument.Lastnodes, Name);
if (self == this)
{
_ownerdocument.Lastnodes.Remove(Name);
_ownerdocument.UpdateLastParentNode();
if (_starttag && !String.IsNullOrEmpty(Name))
{
UpdateLastNode();
}
}
if (endnode == this)
return;
// create an inner section
_innerstartindex = _outerstartindex + _outerlength;
_innerlength = endnode._outerstartindex - _innerstartindex;
// update full length
_outerlength = (endnode._outerstartindex + endnode._outerlength) - _outerstartindex;
}
}
internal string GetId()
{
HtmlAttribute att = Attributes["id"];
return att == null ? string.Empty : att.Value;
}
internal void SetId(string id)
{
HtmlAttribute att = Attributes["id"] ?? _ownerdocument.CreateAttribute("id");
att.Value = id;
_ownerdocument.SetIdForNode(this, att.Value);
Attributes["id"] = att;
SetChanged();
}
internal void WriteAttribute(TextWriter outText, HtmlAttribute att)
{
if (att.Value == null)
{
// null value attribute are not written
return;
}
var quoteType = OwnerDocument.GlobalAttributeValueQuote ?? att.QuoteType;
if (quoteType == AttributeValueQuote.Initial)
{
quoteType = att.InternalQuoteType;
}
string name;
string quote = quoteType == AttributeValueQuote.DoubleQuote ? "\"" : quoteType == AttributeValueQuote.SingleQuote ? "'" : "";
if (_ownerdocument.OptionOutputAsXml)
{
name = _ownerdocument.OptionOutputUpperCase ? att.XmlName.ToUpperInvariant(): att.XmlName;
if (_ownerdocument.OptionOutputOriginalCase)
name = att.OriginalName;
outText.Write(" " + name + "=" + quote + HtmlDocument.HtmlEncodeWithCompatibility(att.XmlValue, _ownerdocument.BackwardCompatibility) + quote);
}
else
{
name = _ownerdocument.OptionOutputUpperCase ? att.Name.ToUpperInvariant() : att.Name;
if (_ownerdocument.OptionOutputOriginalCase)
name = att.OriginalName;
if (att.Name.Length >= 4)
{
if ((att.Name[0] == '<') && (att.Name[1] == '%') &&
(att.Name[att.Name.Length - 1] == '>') && (att.Name[att.Name.Length - 2] == '%'))
{
outText.Write(" " + name);
return;
}
}
var value = quoteType == AttributeValueQuote.DoubleQuote ? !att.Value.StartsWith("@") ? att.Value.Replace("\"", "&quot;") :
att.Value : quoteType == AttributeValueQuote.SingleQuote ? att.Value.Replace("'", "&#39;") : att.Value;
if (_ownerdocument.OptionOutputOptimizeAttributeValues)
if (att.Value.IndexOfAny(new char[] {(char) 10, (char) 13, (char) 9, ' '}) < 0)
outText.Write(" " + name + "=" + att.Value);
else
outText.Write(" " + name + "=" + quote + value + quote);
else
outText.Write(" " + name + "=" + quote + value + quote);
}
}
internal void WriteAttributes(TextWriter outText, bool closing)
{
if (_ownerdocument.OptionOutputAsXml)
{
if (_attributes == null)
{
return;
}
// we use Hashitems to make sure attributes are written only once
foreach (HtmlAttribute att in _attributes.Hashitems.Values)
{
WriteAttribute(outText, att);
}
return;
}
if (!closing)
{
if (_attributes != null)
foreach (HtmlAttribute att in _attributes)
WriteAttribute(outText, att);
if (!_ownerdocument.OptionAddDebuggingAttributes) return;
WriteAttribute(outText, _ownerdocument.CreateAttribute("_closed", Closed.ToString()));
WriteAttribute(outText, _ownerdocument.CreateAttribute("_children", ChildNodes.Count.ToString()));
int i = 0;
foreach (HtmlNode n in ChildNodes)
{
WriteAttribute(outText, _ownerdocument.CreateAttribute("_child_" + i,
n.Name));
i++;
}
}
else
{
if (_endnode == null || _endnode._attributes == null || _endnode == this)
return;
foreach (HtmlAttribute att in _endnode._attributes)
WriteAttribute(outText, att);
if (!_ownerdocument.OptionAddDebuggingAttributes) return;
WriteAttribute(outText, _ownerdocument.CreateAttribute("_closed", Closed.ToString()));
WriteAttribute(outText, _ownerdocument.CreateAttribute("_children", ChildNodes.Count.ToString()));
}
}
#endregion
#region Private Methods
private string GetRelativeXpath()
{
if (ParentNode == null)
return Name;
if (NodeType == HtmlNodeType.Document)
return string.Empty;
int i = 1;
foreach (HtmlNode node in ParentNode.ChildNodes)
{
if (node.Name != Name) continue;
if (node == this)
break;
i++;
}
return Name + "[" + i + "]";
}
private bool IsSingleElementNode()
{
int count = 0;
var element = FirstChild;
while (element != null)
{
if (element.NodeType == HtmlNodeType.Element && element.OuterHtml != "\r\n")
count++;
element = element.NextSibling;
}
return count <= 1 ? true : false;
}
#endregion
#region Class Helper
/// <summary>
/// Adds one or more classes to this node.
/// </summary>
/// <param name="name">The node list to add. May not be null.</param>
public void AddClass(string name)
{
AddClass(name, false);
}
/// <summary>
/// Adds one or more classes to this node.
/// </summary>
/// <param name="name">The node list to add. May not be null.</param>
/// <param name="throwError">true to throw Error if class name exists, false otherwise.</param>
public void AddClass(string name, bool throwError)
{
var classAttributes = Attributes.AttributesWithName("class");
if (!IsEmpty(classAttributes))
{
foreach (HtmlAttribute att in classAttributes)
{
// Check class solo, check class in First with other class, check Class no first.
if (att.Value != null && att.Value.Split(' ').ToList().Any(x => x.Equals(name)))
{
if (throwError)
{
throw new Exception(HtmlDocument.HtmlExceptionClassExists);
}
}
else
{
SetAttributeValue(att.Name, att.Value + " " + name);
}
}
}
else
{
HtmlAttribute attribute = _ownerdocument.CreateAttribute("class", name);
Attributes.Append(attribute);
}
}
/// <summary>
/// Removes the class attribute from the node.
/// </summary>
public void RemoveClass()
{
RemoveClass(false);
}
/// <summary>
/// Removes the class attribute from the node.
/// </summary>
/// <param name="throwError">true to throw Error if class name doesn't exist, false otherwise.</param>
public void RemoveClass(bool throwError)
{
var classAttributes = Attributes.AttributesWithName("class");
if (IsEmpty(classAttributes) && throwError)
{
throw new Exception(HtmlDocument.HtmlExceptionClassDoesNotExist);
}
foreach (var att in classAttributes)
{
Attributes.Remove(att);
}
}
/// <summary>
/// Removes the specified class from the node.
/// </summary>
/// <param name="name">The class being removed. May not be <c>null</c>.</param>
public void RemoveClass(string name)
{
RemoveClass(name, false);
}
/// <summary>
/// Removes the specified class from the node.
/// </summary>
/// <param name="name">The class being removed. May not be <c>null</c>.</param>
/// <param name="throwError">true to throw Error if class name doesn't exist, false otherwise.</param>
public void RemoveClass(string name, bool throwError)
{
var classAttributes = Attributes.AttributesWithName("class");
if (IsEmpty(classAttributes) && throwError)
{
throw new Exception(HtmlDocument.HtmlExceptionClassDoesNotExist);
}
else
{
foreach (var att in classAttributes)
{
if (att.Value == null)
{
continue;
}
if (att.Value.Equals(name))
{
Attributes.Remove(att);
}
else if (att.Value != null && att.Value.Split(' ').ToList().Any(x => x.Equals(name)))
{
string[] classNames = att.Value.Split(' ');
string newClassNames = "";
foreach (string item in classNames)
{
if (!item.Equals(name))
newClassNames += item + " ";
}
newClassNames = newClassNames.Trim();
SetAttributeValue(att.Name, newClassNames);
}
else
{
if (throwError)
{
throw new Exception(HtmlDocument.HtmlExceptionClassDoesNotExist);
}
}
if (string.IsNullOrEmpty(att.Value))
{
Attributes.Remove(att);
}
}
}
}
/// <summary>
/// Replaces the class name oldClass with newClass name.
/// </summary>
/// <param name="newClass">The new class name.</param>
/// <param name="oldClass">The class being replaced.</param>
public void ReplaceClass(string newClass, string oldClass)
{
ReplaceClass(newClass, oldClass, false);
}
/// <summary>
/// Replaces the class name oldClass with newClass name.
/// </summary>
/// <param name="newClass">The new class name.</param>
/// <param name="oldClass">The class being replaced.</param>
/// <param name="throwError">true to throw Error if class name doesn't exist, false otherwise.</param>
public void ReplaceClass(string newClass, string oldClass, bool throwError)
{
if (string.IsNullOrEmpty(newClass))
{
RemoveClass(oldClass);
}
if (string.IsNullOrEmpty(oldClass))
{
AddClass(newClass);
}
var classAttributes = Attributes.AttributesWithName("class");
if (IsEmpty(classAttributes) && throwError)
{
throw new Exception(HtmlDocument.HtmlExceptionClassDoesNotExist);
}
foreach (var att in classAttributes)
{
if (att.Value == null)
{
continue;
}
if (att.Value.Equals(oldClass) || att.Value.Contains(oldClass))
{
string newClassNames = att.Value.Replace(oldClass, newClass);
SetAttributeValue(att.Name, newClassNames);
}
else if (throwError)
{
throw new Exception(HtmlDocument.HtmlExceptionClassDoesNotExist);
}
}
}
/// <summary>Gets the CSS Class from the node.</summary>
/// <returns>
/// The CSS Class from the node
/// </returns>
public IEnumerable<string> GetClasses()
{
var classAttributes = Attributes.AttributesWithName("class");
foreach (var att in classAttributes)
{
var classNames = att.Value.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
foreach (var className in classNames)
{
yield return className;
}
}
}
/// <summary>Check if the node class has the parameter class.</summary>
/// <param name="className">The class.</param>
/// <returns>True if node class has the parameter class, false if not.</returns>
public bool HasClass(string className)
{
var classes = GetClasses();
foreach (var @class in classes)
{
var classNames = @class.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
foreach (var theClassName in classNames)
{
if (theClassName == className)
{
return true;
}
}
}
return false;
}
private bool IsEmpty(IEnumerable en)
{
foreach (var c in en)
{
return false;
}
return true;
}
#endregion
}
}