using System.Text; using AngleSharp.Dom; namespace XFESystem.Browser; internal static class HtmlTextRenderer { private static readonly HashSet 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(); } }