| 1 | using System.Net; |
| 2 | using System.Text; |
| 3 | |
| 4 | namespace AgentForge.Plugin.View; |
| 5 | |
| 6 | /// <summary>Composable, escaped HTML fragments for server-rendered view pages.</summary> |
| 7 | public static partial class ForgeHtml |
| 8 | { |
| 9 | public static string H(string? value) => WebUtility.HtmlEncode(value ?? ""); |
| 10 | |
| 11 | public static string Fragment(params ReadOnlySpan<string> parts) |
| 12 | { |
| 13 | if (parts.Length == 0) |
| 14 | return ""; |
| 15 | if (parts.Length == 1) |
| 16 | return parts[0]; |
| 17 | var sb = new StringBuilder(); |
| 18 | foreach (var part in parts) |
| 19 | sb.Append(part); |
| 20 | return sb.ToString(); |
| 21 | } |
| 22 | |
| 23 | public static void Write(StringBuilder target, string html) => target.Append(html); |
| 24 | |
| 25 | public static void Write(StringBuilder target, params ReadOnlySpan<string> parts) |
| 26 | { |
| 27 | foreach (var part in parts) |
| 28 | target.Append(part); |
| 29 | } |
| 30 | |
| 31 | public static string Raw(string html) => html; |
| 32 | |
| 33 | public static string Text(string? text) => H(text); |
| 34 | |
| 35 | public static string Tag(string name, string? cssClass, ReadOnlySpan<string> inner) |
| 36 | { |
| 37 | var sb = new StringBuilder(); |
| 38 | sb.Append('<').Append(name); |
| 39 | if (!string.IsNullOrEmpty(cssClass)) |
| 40 | sb.Append(" class=\"").Append(H(cssClass)).Append('"'); |
| 41 | sb.Append('>'); |
| 42 | foreach (var part in inner) |
| 43 | sb.Append(part); |
| 44 | sb.Append("</").Append(name).Append('>'); |
| 45 | return sb.ToString(); |
| 46 | } |
| 47 | |
| 48 | public static string Tag(string name, string? cssClass, string inner) => |
| 49 | Tag(name, cssClass, [inner]); |
| 50 | |
| 51 | public readonly record struct ForgeHtmlAttr(string Name, string? Value = null); |
| 52 | |
| 53 | public static string Tag( |
| 54 | string name, |
| 55 | string? cssClass, |
| 56 | ReadOnlySpan<ForgeHtmlAttr> attrs, |
| 57 | ReadOnlySpan<string> inner) |
| 58 | { |
| 59 | var sb = new StringBuilder(); |
| 60 | sb.Append('<').Append(name); |
| 61 | if (!string.IsNullOrEmpty(cssClass)) |
| 62 | sb.Append(" class=\"").Append(H(cssClass)).Append('"'); |
| 63 | foreach (var attr in attrs) |
| 64 | { |
| 65 | if (attr.Value is null) |
| 66 | sb.Append(' ').Append(attr.Name); |
| 67 | else |
| 68 | sb.Append(' ').Append(attr.Name).Append("=\"").Append(H(attr.Value)).Append('"'); |
| 69 | } |
| 70 | |
| 71 | sb.Append('>'); |
| 72 | foreach (var part in inner) |
| 73 | sb.Append(part); |
| 74 | sb.Append("</").Append(name).Append('>'); |
| 75 | return sb.ToString(); |
| 76 | } |
| 77 | |
| 78 | public static string Div(string? cssClass, string inner) => Tag("div", cssClass, inner); |
| 79 | |
| 80 | public static string P(string? cssClass, string inner) => Tag("p", cssClass, inner); |
| 81 | |
| 82 | public static string Li(string? cssClass, string inner) => Tag("li", cssClass, inner); |
| 83 | |
| 84 | public static string Ul(string cssClass, string inner) => Tag("ul", cssClass, inner); |
| 85 | |
| 86 | public static string H1(string text) => Tag("h1", null, Text(text)); |
| 87 | |
| 88 | public static string H1(params ReadOnlySpan<string> inner) => Tag("h1", null, inner); |
| 89 | |
| 90 | public static string H2(string text, string? cssClass = null) => Tag("h2", cssClass, Text(text)); |
| 91 | |
| 92 | public static string H3(string text, string? cssClass = null) => Tag("h3", cssClass, Text(text)); |
| 93 | |
| 94 | public static string Header(string? cssClass, params ReadOnlySpan<string> inner) => |
| 95 | Tag("header", cssClass, inner); |
| 96 | |
| 97 | public static string Aside(string? cssClass, params ReadOnlySpan<string> inner) => |
| 98 | Tag("aside", cssClass, inner); |
| 99 | |
| 100 | public static string P(string? cssClass, params ReadOnlySpan<string> inner) => |
| 101 | Tag("p", cssClass, inner); |
| 102 | |
| 103 | public static string Div(string? cssClass, params ReadOnlySpan<string> inner) => |
| 104 | Tag("div", cssClass, inner); |
| 105 | |
| 106 | public static string Span(string? cssClass, params ReadOnlySpan<string> inner) => |
| 107 | Tag("span", cssClass, inner); |
| 108 | |
| 109 | public static string SpanData(string? cssClass, string? dataForgeUri, params ReadOnlySpan<string> inner) |
| 110 | { |
| 111 | var sb = new StringBuilder("<span"); |
| 112 | if (!string.IsNullOrEmpty(cssClass)) |
| 113 | sb.Append(" class=\"").Append(H(cssClass)).Append('"'); |
| 114 | if (!string.IsNullOrEmpty(dataForgeUri)) |
| 115 | sb.Append(" data-forge-uri=\"").Append(H(dataForgeUri)).Append('"'); |
| 116 | sb.Append('>'); |
| 117 | foreach (var part in inner) |
| 118 | sb.Append(part); |
| 119 | sb.Append("</span>"); |
| 120 | return sb.ToString(); |
| 121 | } |
| 122 | |
| 123 | public static string Strong(string text) => Tag("strong", null, Text(text)); |
| 124 | |
| 125 | public static string Strong(string? cssClass, params ReadOnlySpan<string> inner) => |
| 126 | Tag("strong", cssClass, inner); |
| 127 | |
| 128 | public static string Em(string text) => Tag("em", null, Text(text)); |
| 129 | |
| 130 | public static string Code(string text) => Tag("code", null, Text(text)); |
| 131 | |
| 132 | public static string Badge(string text) => Span("badge", Text(text)); |
| 133 | |
| 134 | public static string Mark(string cssClass, string symbol, bool ariaHidden = true) |
| 135 | { |
| 136 | var sb = new StringBuilder("<span class=\"").Append(H(cssClass)).Append('"'); |
| 137 | if (ariaHidden) |
| 138 | sb.Append(" aria-hidden=\"true\""); |
| 139 | sb.Append('>').Append(H(symbol)).Append("</span>"); |
| 140 | return sb.ToString(); |
| 141 | } |
| 142 | |
| 143 | public static string A(string href, string label, string? cssClass = null) |
| 144 | { |
| 145 | var sb = new StringBuilder(); |
| 146 | sb.Append("<a href=\"").Append(H(href)).Append('"'); |
| 147 | if (!string.IsNullOrEmpty(cssClass)) |
| 148 | sb.Append(" class=\"").Append(H(cssClass)).Append('"'); |
| 149 | sb.Append('>').Append(H(label)).Append("</a>"); |
| 150 | return sb.ToString(); |
| 151 | } |
| 152 | |
| 153 | public static string AExternal(string href, string label, string? cssClass = null) |
| 154 | { |
| 155 | var sb = new StringBuilder(); |
| 156 | sb.Append("<a href=\"").Append(H(href)).Append('"'); |
| 157 | sb.Append(" target=\"_blank\" rel=\"noopener noreferrer\""); |
| 158 | if (!string.IsNullOrEmpty(cssClass)) |
| 159 | sb.Append(" class=\"").Append(H(cssClass)).Append('"'); |
| 160 | sb.Append('>').Append(H(label)).Append("</a>"); |
| 161 | return sb.ToString(); |
| 162 | } |
| 163 | |
| 164 | public static string ARaw(string href, params ReadOnlySpan<string> inner) |
| 165 | { |
| 166 | var sb = new StringBuilder(); |
| 167 | sb.Append("<a href=\"").Append(H(href)).Append("\">"); |
| 168 | foreach (var part in inner) |
| 169 | sb.Append(part); |
| 170 | sb.Append("</a>"); |
| 171 | return sb.ToString(); |
| 172 | } |
| 173 | |
| 174 | public static string Li(string? cssClass, params ReadOnlySpan<string> inner) => |
| 175 | Tag("li", cssClass, inner); |
| 176 | |
| 177 | public static string Ul(string cssClass, params ReadOnlySpan<string> items) => |
| 178 | Tag("ul", cssClass, items); |
| 179 | |
| 180 | public static string Nav(string cssClass, string ariaLabel, params ReadOnlySpan<string> inner) |
| 181 | { |
| 182 | var sb = new StringBuilder("<nav class=\"").Append(H(cssClass)).Append("\" aria-label=\"").Append(H(ariaLabel)).Append("\">"); |
| 183 | foreach (var part in inner) |
| 184 | sb.Append(part); |
| 185 | sb.Append("</nav>"); |
| 186 | return sb.ToString(); |
| 187 | } |
| 188 | |
| 189 | public static string Table(string? cssClass, params ReadOnlySpan<string> inner) => |
| 190 | Tag("table", cssClass, inner); |
| 191 | |
| 192 | public static string Thead(params ReadOnlySpan<string> inner) => Tag("thead", null, inner); |
| 193 | |
| 194 | public static string Tbody(params ReadOnlySpan<string> inner) => Tag("tbody", null, inner); |
| 195 | |
| 196 | public static string Tr(params ReadOnlySpan<string> inner) => Tag("tr", null, inner); |
| 197 | |
| 198 | public static string Tr(string? cssClass, string? id, params ReadOnlySpan<string> inner) |
| 199 | { |
| 200 | var sb = new StringBuilder("<tr"); |
| 201 | if (!string.IsNullOrEmpty(cssClass)) |
| 202 | sb.Append(" class=\"").Append(H(cssClass)).Append('"'); |
| 203 | if (!string.IsNullOrEmpty(id)) |
| 204 | sb.Append(" id=\"").Append(H(id)).Append('"'); |
| 205 | sb.Append('>'); |
| 206 | foreach (var part in inner) |
| 207 | sb.Append(part); |
| 208 | sb.Append("</tr>"); |
| 209 | return sb.ToString(); |
| 210 | } |
| 211 | |
| 212 | public static string Td(string? cssClass, params ReadOnlySpan<string> inner) |
| 213 | { |
| 214 | var sb = new StringBuilder("<td"); |
| 215 | if (!string.IsNullOrEmpty(cssClass)) |
| 216 | sb.Append(" class=\"").Append(H(cssClass)).Append('"'); |
| 217 | sb.Append('>'); |
| 218 | foreach (var part in inner) |
| 219 | sb.Append(part); |
| 220 | sb.Append("</td>"); |
| 221 | return sb.ToString(); |
| 222 | } |
| 223 | |
| 224 | public static string TdColspan(int colspan, string? cssClass, params ReadOnlySpan<string> inner) |
| 225 | { |
| 226 | var sb = new StringBuilder("<td colspan=\"").Append(colspan).Append('"'); |
| 227 | if (!string.IsNullOrEmpty(cssClass)) |
| 228 | sb.Append(" class=\"").Append(H(cssClass)).Append('"'); |
| 229 | sb.Append('>'); |
| 230 | foreach (var part in inner) |
| 231 | sb.Append(part); |
| 232 | sb.Append("</td>"); |
| 233 | return sb.ToString(); |
| 234 | } |
| 235 | |
| 236 | public static string Th(string text) => Tag("th", null, Text(text)); |
| 237 | |
| 238 | public static string Td(params ReadOnlySpan<string> inner) => Tag("td", null, inner); |
| 239 | |
| 240 | public static string Td(string inner) => Tag("td", null, inner); |
| 241 | |
| 242 | public static string Code(string text, string? cssClass, string? title) |
| 243 | { |
| 244 | var sb = new StringBuilder("<code"); |
| 245 | if (!string.IsNullOrEmpty(cssClass)) |
| 246 | sb.Append(" class=\"").Append(H(cssClass)).Append('"'); |
| 247 | if (!string.IsNullOrEmpty(title)) |
| 248 | sb.Append(" title=\"").Append(H(title)).Append('"'); |
| 249 | sb.Append('>').Append(H(text)).Append("</code>"); |
| 250 | return sb.ToString(); |
| 251 | } |
| 252 | |
| 253 | public static string Article(string? cssClass, params ReadOnlySpan<string> inner) => |
| 254 | Tag("article", cssClass, inner); |
| 255 | |
| 256 | public static string Option(string value, string label, bool selected) |
| 257 | { |
| 258 | var sb = new StringBuilder(); |
| 259 | sb.Append("<option value=\"").Append(H(value)).Append('"'); |
| 260 | if (selected) |
| 261 | sb.Append(" selected"); |
| 262 | sb.Append('>').Append(H(label)).Append("</option>"); |
| 263 | return sb.ToString(); |
| 264 | } |
| 265 | } |
| 266 | |