");
sb.AppendLine("");
sb.AppendLine("");
sb.AppendLine($"{title}");
sb.AppendLine("");
sb.AppendLine("");
}
///
/// Appends the three stat cards (total entries, unique permission sets,
/// distinct users/groups) inside a single .stats row.
///
internal static void AppendStatsCards(StringBuilder sb, int totalEntries, int uniquePermSets, int distinctUsers)
{
var T = TranslationSource.Instance;
sb.AppendLine("
");
sb.AppendLine($"
{totalEntries}
{T["report.stat.total_entries"]}
");
sb.AppendLine($"
{uniquePermSets}
{T["report.stat.unique_permission_sets"]}
");
sb.AppendLine($"
{distinctUsers}
{T["report.stat.distinct_users_groups"]}
");
sb.AppendLine("
");
}
/// Appends the live-filter input bound to #permTable.
internal static void AppendFilterInput(StringBuilder sb)
{
var T = TranslationSource.Instance;
sb.AppendLine("
");
sb.AppendLine($" ");
sb.AppendLine("
");
}
/// Appends the inline <script> that powers filter and group toggle.
internal static void AppendInlineJs(StringBuilder sb)
{
sb.AppendLine("");
}
///
/// Renders the user-pill cell content plus any group-member sub-rows for a
/// single permission entry. Callers pass their row colspan so sub-rows
/// span the full table; must be mutated
/// across rows so sub-row IDs stay unique.
///
internal static (string Pills, string MemberSubRows) BuildUserPillsCell(
string userLogins,
string userNames,
string? principalType,
IReadOnlyDictionary>? groupMembers,
int colSpan,
ref int grpMemIdx)
{
var T = TranslationSource.Instance;
var logins = userLogins.Split(';', StringSplitOptions.RemoveEmptyEntries);
var names = userNames.Split(';', StringSplitOptions.RemoveEmptyEntries);
var pills = new StringBuilder();
var subRows = new StringBuilder();
for (int i = 0; i < logins.Length; i++)
{
var login = logins[i].Trim();
var name = i < names.Length ? names[i].Trim() : login;
var isExt = login.Contains("#EXT#", StringComparison.OrdinalIgnoreCase);
bool isExpandable = principalType == "SharePointGroup"
&& groupMembers != null
&& groupMembers.TryGetValue(name, out _);
if (isExpandable && groupMembers != null && groupMembers.TryGetValue(name, out var resolved))
{
var grpId = $"grpmem{grpMemIdx}";
pills.Append($"{HtmlEncode(name)} ▼");
string memberContent;
if (resolved.Count > 0)
{
var parts = resolved.Select(m => $"{HtmlEncode(m.DisplayName)} <{HtmlEncode(m.Login)}>");
memberContent = string.Join(" • ", parts);
}
else
{
memberContent = $"{T["report.text.members_unavailable"]}";
}
subRows.AppendLine($"
{memberContent}
");
grpMemIdx++;
}
else
{
var cls = isExt ? "user-pill external-user" : "user-pill";
pills.Append($"{HtmlEncode(name)}");
}
}
return (pills.ToString(), subRows.ToString());
}
/// Returns the CSS class for the object-type badge.
internal static string ObjectTypeCss(string t) => t switch
{
"Site Collection" => "badge site-coll",
"Site" => "badge site",
"List" => "badge list",
"Folder" => "badge folder",
_ => "badge"
};
/// Minimal HTML encoding for text content and attribute values.
internal static string HtmlEncode(string value)
{
if (string.IsNullOrEmpty(value)) return string.Empty;
return value
.Replace("&", "&")
.Replace("<", "<")
.Replace(">", ">")
.Replace("\"", """)
.Replace("'", "'");
}
}