Private/Format-NSPFortiGateHtmlTable.ps1
|
function Format-NSPFortiGateHtmlTable { <# .SYNOPSIS Renders rows as an HTML table, merging repeated group cells. .DESCRIPTION Rows are hashtables keyed by column name; a '_class' key sets the row class. -SpanColumns merge repeated cells (rowspan), nested in the order given: a cell merges while its value and every earlier span column's repeat, so a group's name and "used by" print once beside its members. Values are HTML-encoded; line breaks become <br>. -NoWrapColumns keep short values (IDs, status) on one line while long names wrap. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)][string[]]$Columns, [AllowEmptyCollection()][object[]]$Rows = @(), [string[]]$SpanColumns = @(), [string[]]$NoWrapColumns = @(), [hashtable]$ColumnWidth = @{}, [string]$Empty = 'None referenced.' ) if ($Rows.Count -eq 0) { return "<p class=""empty"">$([System.Net.WebUtility]::HtmlEncode($Empty))</p>" } $encode = { param($Value) [System.Net.WebUtility]::HtmlEncode([string]$Value) -replace '\r?\n', '<br>' } # Cells: offer line breaks after _ - . / so long object names (VPN_Branch_Servers_All) # wrap at their natural joints instead of mid-word, and columns size to those pieces. $encodeCell = { param($Value) ([System.Net.WebUtility]::HtmlEncode([string]$Value) -replace '([_./-])', '$1<wbr>') -replace '\r?\n', '<br>' } $html = New-Object System.Text.StringBuilder [void]$html.Append('<table><thead><tr>') foreach ($column in $Columns) { $nowrap = if ($NoWrapColumns -contains $column) { ' class="nw"' } else { '' } # A width hint keeps one long-text column from squeezing the rest under automatic table layout. $width = if ($ColumnWidth.ContainsKey($column)) { " style=""width: $($ColumnWidth[$column])""" } else { '' } [void]$html.Append("<th$nowrap$width>$(& $encode $column)</th>") } [void]$html.Append('</tr></thead><tbody>') # Span columns nest in the order given: a cell merges while its own value and those of the span # columns before it repeat, so a policy spans its VSAs and each VSA spans its firewall rules. $spanKey = @(for ($i = 0; $i -lt $Rows.Count; $i++) { $parts = @(foreach ($column in $SpanColumns) { [string]$Rows[$i][$column] }) , @(for ($level = 0; $level -lt $parts.Count; $level++) { $parts[0..$level] -join "`t" }) }) for ($i = 0; $i -lt $Rows.Count; $i++) { $row = $Rows[$i] $startsGroup = $SpanColumns.Count -eq 0 -or $i -eq 0 -or $spanKey[$i - 1][0] -ne $spanKey[$i][0] $class = if ($row['_class']) { " class=""$($row['_class'])""" } elseif ($startsGroup -and $i -gt 0 -and $SpanColumns.Count) { ' class="group-start"' } else { '' } [void]$html.Append("<tr$class>") foreach ($column in $Columns) { $level = if ($SpanColumns.Count) { [array]::IndexOf($SpanColumns, $column) } else { -1 } if ($level -ge 0) { if ($i -gt 0 -and $spanKey[$i - 1][$level] -eq $spanKey[$i][$level]) { continue } $span = 1 while ($i + $span -lt $Rows.Count -and $spanKey[$i + $span][$level] -eq $spanKey[$i][$level]) { $span++ } $rowspan = if ($span -gt 1) { " rowspan=""$span""" } else { '' } # A merged cell sits in its first row; 'mixed' keeps it from taking that row's styling # (such as a disabled rule) when the rows it spans differ. $mixed = if (@($Rows[$i..($i + $span - 1)] | ForEach-Object { [string]$_['_class'] } | Select-Object -Unique).Count -gt 1) { ' mixed' } else { '' } if ($NoWrapColumns -contains $column) { [void]$html.Append("<td class=""group nw$mixed""$rowspan>$(& $encode $row[$column])</td>") } else { [void]$html.Append("<td class=""group$mixed""$rowspan>$(& $encodeCell $row[$column])</td>") } } else { # Browsers still break at <wbr> inside white-space: nowrap, so no-wrap cells get none. if ($NoWrapColumns -contains $column) { [void]$html.Append("<td class=""nw"">$(& $encode $row[$column])</td>") } else { [void]$html.Append("<td>$(& $encodeCell $row[$column])</td>") } } } [void]$html.Append('</tr>') } [void]$html.Append('</tbody></table>') $html.ToString() } |