namespace Solicitors.HtmlParsing;
using Models;
internal class HtmlParser : IHtmlParser
{
private readonly string[] _voidElements =
[
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"source",
"track",
"wbr"
];
public IEnumerable ParseHtml(string html)
{
// wanted to do this in one pass but was quite time-consuming
// so taking a little lesson from compiler development
// and lex the text into tokens before parsing :)
// deferred execution might help it still be "one pass"?
var stringEnumerator = html.GetEnumerator();
var tokens = LexHtml(stringEnumerator);
var tokenEnumerator = tokens.GetEnumerator();
return ParseHtml(tokenEnumerator);
}
private IEnumerable LexHtml(IEnumerator content)
{
var work = "";
while (content.MoveNext())
{
if (content.Current == '<' && !string.IsNullOrWhiteSpace(work))
{
yield return work.Trim();
work = "";
}
work += content.Current;
if (content.Current == '>' && !string.IsNullOrWhiteSpace(work))
{
yield return work.Trim();
work = "";
}
}
}
private IEnumerable ParseHtml(IEnumerator tokens)
{
while (tokens.MoveNext())
{
var token = tokens.Current;
if (!token.StartsWith('<'))
{
yield return new StringNode(token);
continue;
}
var noBraces = token.Substring(1, token.Length - 2);
var split = noBraces.Split(' ');
IHtmlAttribute[] attributes = [];
if (split.Length > 1)
{
attributes = ParseAttributes(noBraces).ToArray();
}
if (IsVoidOrSelfClosingElement(token))
yield return new HtmlNode(split.First(), attributes);
else if (token.StartsWith(""))
yield break;
else
{
var children = ParseHtml(tokens).ToArray();
yield return new ParentHtmlNode(split.First(), attributes, children);
}
}
}
bool IsVoidOrSelfClosingElement(string token)
{
if (token.StartsWith(""))
return true;
var tagName = token.Split(' ').First().Substring(1);
if (tagName.EndsWith('>'))
tagName = tagName.Substring(0, tagName.Length - 1);
return _voidElements.Contains(tagName);
}
private IEnumerable ParseAttributes(string tokenNoBraces)
{
var attributesString = string.Join(' ', tokenNoBraces.Split(' ').Skip(1));
var attString = "";
bool inQuote = false;
foreach (char c in attributesString)
{
if (!inQuote && c == ' ')
{
yield return ParseAttribute(attString);
attString = "";
}
else
{
attString += c;
if (c == '\"')
inQuote = !inQuote;
}
}
yield return ParseAttribute(attString);
}
private IHtmlAttribute ParseAttribute(string attString)
{
var split = attString.Split('=');
if (split.Length > 1)
return new HtmlValueAttribute(split[0], string.Join('=', split.Skip(1)));
else
return new HtmlAttribute(attString);
}
}