initial commit

This commit is contained in:
fox
2026-06-23 19:03:31 +01:00
commit 689276ea7d
102 changed files with 8740 additions and 0 deletions
@@ -0,0 +1,6 @@
namespace Solicitors.HtmlParsing.Models;
internal class HtmlAttribute(string name) : IHtmlAttribute
{
public string Name { get; } = name;
}
@@ -0,0 +1,95 @@
using System.Diagnostics.CodeAnalysis;
namespace Solicitors.HtmlParsing.Models;
internal class HtmlNode(string tagName, IHtmlAttribute[] attributes)
: IHtmlNode
{
private readonly IHtmlAttribute[] _attributes = attributes;
private readonly string _tagName = tagName;
public virtual bool TryGetByTagName(
string tagName,
[NotNullWhen(true)] out IHtmlNode? node,
bool noChildren = false)
{
node = null;
if (tagName == _tagName)
node = this;
return node is not null;
}
private string[] Classes
{
get
{
var classAttribute = _attributes
.Where(a => a is HtmlValueAttribute)
.Select(a => (a as HtmlValueAttribute)!)
.FirstOrDefault(a => a.Name.Equals("class", StringComparison.CurrentCultureIgnoreCase));
return classAttribute is not null
? classAttribute.Value.Substring(1, classAttribute.Value.Length - 2).Split(' ')
: [];
}
}
public virtual bool TryGetByClass(
string className,
[NotNullWhen(true)] out IHtmlNode? node,
bool noChildren = false)
{
node = null;
if (Classes.Contains(className, StringComparer.InvariantCultureIgnoreCase))
node = this;
return node is not null;
}
public virtual bool TryGetByTagNameAndClass(
string tagName,
string className,
[NotNullWhen(true)] out IHtmlNode? node,
bool noChildren = false)
{
node = null;
if (tagName == _tagName && Classes.Contains(className, StringComparer.InvariantCultureIgnoreCase))
node = this;
return node is not null;
}
public virtual bool TryGetText([NotNullWhen(true)] out string? text)
{
text = null;
return false;
}
public virtual bool TryGetChildren([NotNullWhen(true)] out IEnumerable<IHtmlNode>? children)
{
children = null;
return false;
}
public bool HasAttribute(string attributeName)
{
return _attributes
.Any(a => a.Name.Equals(attributeName, StringComparison.InvariantCultureIgnoreCase));
}
public bool TryGetAttributeValue(string attributeName, [NotNullWhen(true)] out string? attributeValue)
{
attributeValue = null;
var matchingAttribute = _attributes.FirstOrDefault(
a => a.Name.Equals(attributeName, StringComparison.InvariantCultureIgnoreCase)
);
if (matchingAttribute is HtmlValueAttribute valueAttribute)
attributeValue = valueAttribute.Value;
return attributeValue is not null;
}
}
@@ -0,0 +1,6 @@
namespace Solicitors.HtmlParsing.Models;
internal class HtmlValueAttribute(string name, string value) : HtmlAttribute(name)
{
public string Value { get; } = value;
}
@@ -0,0 +1,6 @@
namespace Solicitors.HtmlParsing.Models;
internal interface IHtmlAttribute
{
string Name { get; }
}
@@ -0,0 +1,64 @@
using System.Diagnostics.CodeAnalysis;
namespace Solicitors.HtmlParsing.Models;
public interface IHtmlNode
{
/// <summary>
/// Searches this node and its direct children for a matching tag name.
/// </summary>
/// <param name="tagName">The tag name to match.</param>
/// <param name="node">If found, the node that matches the tag name.</param>
/// <param name="noChildren">If true, only check this node, not its children.</param>
/// <returns>True if this node, or any of its direct children, have a matching tag name; false otherwise.</returns>
bool TryGetByTagName(
string tagName,
[NotNullWhen(true)] out IHtmlNode? node,
bool noChildren = false);
/// <summary>
/// Searches this node and its direct children for a matching class.
/// </summary>
/// <param name="className">The class name to match.</param>
/// <param name="node">If found, the node that matches the class name.</param>
/// <param name="noChildren">If true, only check this node, not its children.</param>
/// <returns>True if this node, or any of its direct children, have a matching class name; false otherwise.</returns>
bool TryGetByClass(
string className,
[NotNullWhen(true)] out IHtmlNode? node,
bool noChildren = false);
/// <summary>
/// Searches this node and its direct children for a matching tag name and matching class on the same node.
/// </summary>
/// <param name="tagName">The tag name to match.</param>
/// <param name="className">The class name to match.</param>
/// <param name="node">If found, the node that matches the tag name and class name.</param>
/// <param name="noChildren">If true, only check this node, not its children.</param>
/// <returns>True if this node, or any of its direct children, have a matching tag name and class name; false otherwise.</returns>
bool TryGetByTagNameAndClass(
string tagName,
string className,
[NotNullWhen(true)] out IHtmlNode? node,
bool noChildren = false);
/// <summary>
/// Checks this node (and *not* its children) to see if it is text content.
/// </summary>
/// <param name="text">If found, the text content.</param>
/// <returns>True if the node is a text node, false otherwise.</returns>
bool TryGetText([NotNullWhen(true)] out string? text);
/// <summary>
/// Checks this node to see if it has child elements.
/// </summary>
/// <param name="children">If found, the child elements.</param>
/// <returns>True if the node has child elements, false otherwise.</returns>
bool TryGetChildren([NotNullWhen(true)] out IEnumerable<IHtmlNode>? children);
bool HasAttribute(string attributeName);
bool TryGetAttributeValue(
string attributeName,
[NotNullWhen(true)] out string? attributeValue);
}
@@ -0,0 +1,94 @@
using System.Diagnostics.CodeAnalysis;
namespace Solicitors.HtmlParsing.Models;
internal class ParentHtmlNode(string tagName, IHtmlAttribute[] attributes, IHtmlNode[] children)
: HtmlNode(tagName, attributes)
{
private readonly IHtmlNode[] _children = children;
public override bool TryGetByTagName(
string tagName,
[NotNullWhen(true)] out IHtmlNode? node,
bool noChildren = false)
{
node = null;
if (!base.TryGetByTagName(tagName, out node, noChildren) && !noChildren)
{
var nodesToCheck = new Queue<IHtmlNode>(_children);
while (nodesToCheck.TryDequeue(out var child))
{
if (child.TryGetByTagName(tagName, out var childNode, true))
{
node = childNode;
break;
}
if (child.TryGetChildren(out var grandChildren))
foreach (var grandChild in grandChildren)
nodesToCheck.Enqueue(grandChild);
}
}
return node is not null;
}
public override bool TryGetByClass(
string className,
[NotNullWhen(true)] out IHtmlNode? node,
bool noChildren = false)
{
node = null;
if (!base.TryGetByClass(className, out node, noChildren) && !noChildren)
{
var nodesToCheck = new Queue<IHtmlNode>(_children);
while (nodesToCheck.TryDequeue(out var child))
{
if (child.TryGetByClass(className, out var childNode, true))
{
node = childNode;
break;
}
if (child.TryGetChildren(out var grandChildren))
foreach (var grandChild in grandChildren)
nodesToCheck.Enqueue(grandChild);
}
}
return node is not null;
}
public override bool TryGetByTagNameAndClass(
string tagName,
string className,
[NotNullWhen(true)] out IHtmlNode? node,
bool noChildren = false)
{
node = null;
if (!base.TryGetByTagNameAndClass(tagName, className, out node, noChildren) && !noChildren)
{
var nodesToCheck = new Queue<IHtmlNode>(_children);
while (nodesToCheck.TryDequeue(out var child))
{
if (child.TryGetByTagNameAndClass(tagName, className, out var childNode, true))
{
node = childNode;
break;
}
if (child.TryGetChildren(out var grandChildren))
foreach (var grandChild in grandChildren)
nodesToCheck.Enqueue(grandChild);
}
}
return node is not null;
}
public override bool TryGetChildren([NotNullWhen(true)] out IEnumerable<IHtmlNode>? children)
{
children = _children;
return _children.Length != 0;
}
}
@@ -0,0 +1,59 @@
using System.Diagnostics.CodeAnalysis;
namespace Solicitors.HtmlParsing.Models;
internal class StringNode(string content)
: IHtmlNode
{
private readonly string _content = content;
public bool TryGetByTagName(
string tagName,
[NotNullWhen(true)] out IHtmlNode? node,
bool noChildren = false)
{
node = null;
return false;
}
public bool TryGetByClass(
string className,
[NotNullWhen(true)] out IHtmlNode? node,
bool noChildren = false)
{
node = null;
return false;
}
public bool TryGetByTagNameAndClass(
string tagName,
string className,
[NotNullWhen(true)] out IHtmlNode? node,
bool noChildren = false)
{
node = null;
return false;
}
public bool TryGetText([NotNullWhen(true)] out string? text)
{
text = _content;
return !string.IsNullOrEmpty(text);
}
public bool TryGetChildren([NotNullWhen(true)] out IEnumerable<IHtmlNode>? children)
{
children = null;
return false;
}
public bool HasAttribute(string attributeName) => false;
public bool TryGetAttributeValue(
string attributeName,
[NotNullWhen(true)] out string? attributeValue)
{
attributeValue = null;
return false;
}
}