using System.Text;
using AngleSharp.Dom;
namespace XFESystem.Browser;
internal static class HtmlTextRenderer
{
private static readonly HashSet<string> BlockTags = new(StringComparer.Ordinal)
{
"address", "article", "aside", "blockquote", "div", "dl", "dt", "dd", "fieldset", "figcaption",
"figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "li", "main",
"nav", "ol", "p", "pre", "section", "table", "tr", "ul"
};
public static string Render(IDocument document, Uri address, BrowserLimits limits)
{
var output = new StringBuilder();
var links = new List<(string Label, Uri Address)>();
var pending = new Stack<(INode Node, bool End, bool Pre)>();
if (document.Body is not null) pending.Push((document.Body, false, false));
var whitespacePending = false;
void Append(string text)
{
if (text.Length > limits.MaximumOutputCharacters - output.Length)
throw new BrowserLimitException("The rendered page exceeds its character limit.");
output.Append(text);
}
void NewLine()
{
whitespacePending = false;
if (output.Length != 0 && output[^1] != '\n') Append("\n");
}
while (pending.TryPop(out var entry))
{
if (entry.Node.NodeType == NodeType.Text)
{
foreach (var character in entry.Node.TextContent)
{
if (entry.Pre)
{
if (!char.IsControl(character) || character is '\n' or '\t') Append(character.ToString());
}
else if (char.IsWhiteSpace(character)) whitespacePending = true;
else if (!char.IsControl(character))
{
if (whitespacePending && output.Length != 0 && output[^1] != '\n') Append(" ");
whitespacePending = false;
Append(character.ToString());
}
}
continue;
}
if (entry.Node is not IElement element) continue;
var tag = element.LocalName;
if (tag is "script" or "style" or "head" or "template" or "noscript" || element.HasAttribute("hidden")) continue;
if (entry.End)
{
if (BlockTags.Contains(tag)) NewLine();
if (tag == "a" && Uri.TryCreate(address, element.GetAttribute("href"), out var target) &&
target.Scheme is "http" or "https" && string.IsNullOrEmpty(target.UserInfo))
{
if (links.Count >= 256) throw new BrowserLimitException("The page exceeds its link limit.");
links.Add((BoundedText.Safe(element.TextContent.Trim(), 256), target));
Append($" [{links.Count}]");
}
continue;
}
if (tag is "br" or "hr") { NewLine(); continue; }
if (BlockTags.Contains(tag)) NewLine();
if (tag == "li") Append("• ");
if (tag == "img" && element.GetAttribute("alt") is { Length: > 0 } alt) Append("[" + BoundedText.Safe(alt, 512) + "]");
pending.Push((element, true, entry.Pre));
for (var index = element.ChildNodes.Length - 1; index >= 0; index--)
pending.Push((element.ChildNodes[index], false, entry.Pre || tag == "pre"));
}
if (links.Count != 0)
{
NewLine();
Append("\nLinks:\n");
for (var index = 0; index < links.Count; index++)
Append($"[{index + 1}] {links[index].Label} — {links[index].Address.AbsoluteUri}\n");
}
return output.ToString().TrimEnd();
}
}
using System.Text;
using AngleSharp.Dom;
namespace XFESystem.Browser;
internal static class HtmlTextRenderer
{
private static readonly HashSet<string> BlockTags = new(StringComparer.Ordinal)
{
"address", "article", "aside", "blockquote", "div", "dl", "dt", "dd", "fieldset", "figcaption",
"figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "li", "main",
"nav", "ol", "p", "pre", "section", "table", "tr", "ul"
};
public static string Render(IDocument document, Uri address, BrowserLimits limits)
{
var output = new StringBuilder();
var links = new List<(string Label, Uri Address)>();
var pending = new Stack<(INode Node, bool End, bool Pre)>();
if (document.Body is not null) pending.Push((document.Body, false, false));
var whitespacePending = false;
void Append(string text)
{
if (text.Length > limits.MaximumOutputCharacters - output.Length)
throw new BrowserLimitException("The rendered page exceeds its character limit.");
output.Append(text);
}
void NewLine()
{
whitespacePending = false;
if (output.Length != 0 && output[^1] != '\n') Append("\n");
}
while (pending.TryPop(out var entry))
{
if (entry.Node.NodeType == NodeType.Text)
{
foreach (var character in entry.Node.TextContent)
{
if (entry.Pre)
{
if (!char.IsControl(character) || character is '\n' or '\t') Append(character.ToString());
}
else if (char.IsWhiteSpace(character)) whitespacePending = true;
else if (!char.IsControl(character))
{
if (whitespacePending && output.Length != 0 && output[^1] != '\n') Append(" ");
whitespacePending = false;
Append(character.ToString());
}
}
continue;
}
if (entry.Node is not IElement element) continue;
var tag = element.LocalName;
if (tag is "script" or "style" or "head" or "template" or "noscript" || element.HasAttribute("hidden")) continue;
if (entry.End)
{
if (BlockTags.Contains(tag)) NewLine();
if (tag == "a" && Uri.TryCreate(address, element.GetAttribute("href"), out var target) &&
target.Scheme is "http" or "https" && string.IsNullOrEmpty(target.UserInfo))
{
if (links.Count >= 256) throw new BrowserLimitException("The page exceeds its link limit.");
links.Add((BoundedText.Safe(element.TextContent.Trim(), 256), target));
Append($" [{links.Count}]");
}
continue;
}
if (tag is "br" or "hr") { NewLine(); continue; }
if (BlockTags.Contains(tag)) NewLine();
if (tag == "li") Append("• ");
if (tag == "img" && element.GetAttribute("alt") is { Length: > 0 } alt) Append("[" + BoundedText.Safe(alt, 512) + "]");
pending.Push((element, true, entry.Pre));
for (var index = element.ChildNodes.Length - 1; index >= 0; index--)
pending.Push((element.ChildNodes[index], false, entry.Pre || tag == "pre"));
}
if (links.Count != 0)
{
NewLine();
Append("\nLinks:\n");
for (var index = 0; index < links.Count; index++)
Append($"[{index + 1}] {links[index].Label} — {links[index].Address.AbsoluteUri}\n");
}
return output.ToString().TrimEnd();
}
}