using System.Globalization;
using System.Text;
namespace Sunfish.Web;
/// <summary>
/// Construye HTML escapando todo texto. En modo clasico la pagina es ISO-8859-1 y solo contiene
/// ASCII: los caracteres Latin-1 van como referencias numericas (las entienden hasta los
/// navegadores de HTML 2.0) y el resto se translitera o se cambia por '?'. En modo moderno, UTF-8.
/// </summary>
internal sealed class HtmlBuilder(bool classic)
{
private readonly StringBuilder _sb = new();
public bool Classic { get; } = classic;
public string Charset => Classic ? "iso-8859-1" : "utf-8";
public string ContentType => "text/html; charset=" + Charset;
/// <summary>HTML literal (debe ser ASCII).</summary>
public HtmlBuilder Raw(string html)
{
_sb.Append(html);
return this;
}
/// <summary>Texto escapado.</summary>
public HtmlBuilder Text(string text)
{
_sb.Append(Escape(text, Classic));
return this;
}
public byte[] ToBytes() =>
Classic ? Encoding.Latin1.GetBytes(_sb.ToString()) : Encoding.UTF8.GetBytes(_sb.ToString());
public override string ToString() => _sb.ToString();
public static string Escape(string text, bool classic)
{
var sb = new StringBuilder(text.Length + 16);
foreach (char c in text)
{
switch (c)
{
case '&': sb.Append("&"); break;
case '<': sb.Append("<"); break;
case '>': sb.Append(">"); break;
case '"': sb.Append("""); break;
case '\'': sb.Append("'"); break;
default:
if (c < 0x20)
sb.Append('?');
else if (!classic || c < 0x80)
sb.Append(c);
else if (c is >= ' ' and <= 'ÿ')
sb.Append("&#").Append(((int)c).ToString(CultureInfo.InvariantCulture)).Append(';');
else
sb.Append(Transliterate(c));
break;
}
}
return sb.ToString();
}
/// <summary>Quita los diacriticos ("ő" -> "o"); si no queda un caracter Latin-1, '?'.</summary>
internal static string Transliterate(char c)
{
if (char.IsSurrogate(c))
return "?";
string decomposed = c.ToString().Normalize(NormalizationForm.FormD);
foreach (char d in decomposed)
{
if (CharUnicodeInfo.GetUnicodeCategory(d) == UnicodeCategory.NonSpacingMark)
continue;
if (d < 0x80)
return d.ToString();
if (d is >= ' ' and <= 'ÿ')
return "&#" + ((int)d).ToString(CultureInfo.InvariantCulture) + ";";
break;
}
return "?";
}
/// <summary>
/// Un componente de URL codificado como %XX de sus bytes UTF-8: solo ASCII, valido en
/// cualquier pagina; el navegador devuelve los mismos bytes al seguir el enlace.
/// </summary>
public static string UrlSegment(string name) => Uri.EscapeDataString(name);
}