Public/New-ConfluenceContentTable.ps1
|
function New-ConfluenceContentTable { <# .SYNOPSIS Creates an HTML table for a Confluence page from a collection of objects. .DESCRIPTION New-ConfluenceContentTable converts objects to a table: the property names of the first object become the header row and every object becomes a row. All objects must be of the same type and have the same property names. Hashtables and ordered dictionaries are also accepted as rows; their keys are the columns. Cell values that are collections, dictionaries or objects with several properties are rendered as nested tables, up to three levels deep (deeper values are shown as text). In the data cells (all but the first column) addresses with a scheme (http://, https:// or mailto:) are converted to links; file names such as report.pdf and bare e-mail addresses are not. Header names and cell values are escaped, so the result is always well-formed storage format; use -Raw to insert cell values that already are storage-format markup. An empty collection returns an empty string. .PARAMETER TableData The objects (or hashtables / ordered dictionaries) to show in the table. .PARAMETER TableType The CSS class(es) of the table, for example Wrapped or "Relative Table". .PARAMETER TableTypeStyle A single inline style, for example "Width: 100%" (default), "Width: 600px" or "Border: 1px solid black". .PARAMETER NoHeader Do not add the header row. .PARAMETER HeaderStringFormatting Formatting for the header cells: Bold, Italic, Underline and/or Strikethrough. .PARAMETER HeaderAlignmentFormatting Text alignment of the header cells: Left (default), Center or Right. .PARAMETER VerticalHeader Render the first cell of every row as a row header (<th scope='row'>). .PARAMETER CellStringFormatting Formatting for the data cells (all but the first column): Bold, Italic, Underline and/or Strikethrough. .PARAMETER CellAlignmentFormatting Text alignment of the data cells: Left (default), Center or Right. .PARAMETER FirstCellHeaderFormat Wrap the first cell of each row in a heading: 0 (no heading, default) or 1 to 6. .PARAMETER FirstCellStringFormatting Formatting for the first cell of each row: Bold, Italic, Underline and/or Strikethrough. .PARAMETER FirstCellAlignmentFormatting Text alignment of the first cell of each row. Defaults to -CellAlignmentFormatting. .PARAMETER Raw Insert cell values as given, without escaping them or converting addresses to links. Use it when the values are storage-format fragments, for example links or status lozenges built with the other New-ConfluenceContent* functions. Header names are still escaped. .EXAMPLE New-ConfluenceContentTable -TableData @([ordered]@{ Service = 'web'; Docs = 'https://contoso.com/web' }) Returns a table from an ordered dictionary; the address becomes a link. .EXAMPLE New-ConfluenceContentTable -TableData @([pscustomobject]@{ Name = 'web'; State = (New-ConfluenceContentStatus -Text 'OK' -Colour Green) }) -Raw Returns a table whose State cells contain status lozenges. .EXAMPLE $rows = Get-Process | Select-Object -First 5 Name, Id New-ConfluenceContentTable -TableData $rows -HeaderStringFormatting Bold -VerticalHeader Returns a table with a bold header row and the process names as row headers. .OUTPUTS System.String #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Only builds a storage-format string in memory; nothing outside the session is changed.')] [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory, HelpMessage = 'The table data to be converted.')] [AllowEmptyCollection()] [AllowNull()] [array]$TableData, [Parameter(HelpMessage = 'The type of table to be created.')] [ValidateSet('Wrapped', 'Wrapped Relative Table', 'Relative Table', 'Relative Table with Header', 'Wrapped Relative Table with Header', 'Wrapped Relative Table with Header and Vertical Header')] [string]$TableType = 'Wrapped', [Parameter(HelpMessage = 'The style to be applied to the table.')] [ValidatePattern('^(Width: \d+%|Height: \d+%|Width: \d+px|Height: \d+px|Margin: \d+px|Padding: \d+px|Border: \d+px solid [a-zA-Z]+)$')] [string]$TableTypeStyle = 'Width: 100%', [Parameter(HelpMessage = 'Do not add a header row.')] [switch]$NoHeader, [Parameter(HelpMessage = 'The text formatting to be applied to the table header.')] [ValidateSet('Bold', 'Italic', 'Underline', 'Strikethrough')] [string[]]$HeaderStringFormatting, [Parameter(HelpMessage = 'The text alignment to be applied to the table header.')] [ValidateSet('Left', 'Center', 'Right')] [string]$HeaderAlignmentFormatting = 'Left', [Parameter(HelpMessage = 'Render the first cell of each row as a row header.')] [switch]$VerticalHeader, [Parameter(HelpMessage = 'The text formatting to be applied to the table cells.')] [ValidateSet('Bold', 'Italic', 'Underline', 'Strikethrough')] [string[]]$CellStringFormatting, [Parameter(HelpMessage = 'The text alignment to be applied to the table cells.')] [ValidateSet('Left', 'Center', 'Right')] [string]$CellAlignmentFormatting = 'Left', [Parameter(HelpMessage = "The header format to be applied to the table first cell.`n0 Normal Paragraph`n1 Heading 1 (Largest)`n2 Heading 2`n3 Heading 3`n4 Heading 4`n5 Heading 5 (Smallest)`n6 Heading 6 (Quote)")] [ValidateSet('0', '1', '2', '3', '4', '5', '6')] [string]$FirstCellHeaderFormat = '0', [Parameter(HelpMessage = 'The text formatting to be applied to the first cell.')] [ValidateSet('Bold', 'Italic', 'Underline', 'Strikethrough')] [string[]]$FirstCellStringFormatting, [Parameter(HelpMessage = 'The text alignment to be applied to the first cell.')] [ValidateSet('Left', 'Center', 'Right')] [string]$FirstCellAlignmentFormatting, [Parameter(HelpMessage = 'Insert cell values as storage-format markup without escaping them.')] [switch]$Raw ) $TelemetryArgs = @{ ModuleName = $MyInvocation.MyCommand.Module.Name ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version CommandName = $MyInvocation.MyCommand.Name ExecutionID = [guid]::NewGuid().ToString() } Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer $telemetryFailed = $false try { if ($null -eq $TableData -or $TableData.Count -eq 0) { return '' } # Rows are objects (columns = property names) or dictionaries (columns = keys) $getColumns = { param($Row) if ($Row -is [System.Collections.IDictionary]) { return ($Row.Keys | ForEach-Object { [string]$_ }) } return $Row.PSObject.Properties.Name } $getValue = { param($Row, [string]$Column) if ($Row -is [System.Collections.IDictionary]) { return , $Row[$Column] } return , $Row.PSObject.Properties[$Column].Value } $firstRow = $TableData[0] if ($null -eq $firstRow) { Write-Error 'Table rows must not be null.' return } $isDictionary = $firstRow -is [System.Collections.IDictionary] $firstRowType = $firstRow.GetType() $firstRowProperties = @(& $getColumns $firstRow) foreach ($row in $TableData) { if ($null -eq $row) { Write-Error 'Table rows must not be null.' return } if ($isDictionary) { if ($row -isnot [System.Collections.IDictionary]) { Write-Error 'All table rows must be of the same type.' return } $rowColumns = @(& $getColumns $row) if ($rowColumns.Count -ne $firstRowProperties.Count -or @($rowColumns | Where-Object { $firstRowProperties -notcontains $_ }).Count -gt 0) { Write-Error 'All table rows must have the same column names.' return } continue } if ($row.GetType() -ne $firstRowType) { Write-Error 'All table rows must be of the same type.' return } if (Compare-Object -ReferenceObject $firstRowProperties -DifferenceObject @(& $getColumns $row) -SyncWindow 0) { Write-Error 'All table rows must have the same column names.' return } } if ([string]::IsNullOrEmpty($FirstCellAlignmentFormatting)) { $FirstCellAlignmentFormatting = $CellAlignmentFormatting } # Renders one cell value: nested table for collections/complex objects, escaped text otherwise $renderValue = { param($Value, [bool]$DetectLinks) if ($null -eq $Value) { return '' } $isScalar = ($Value -is [string]) -or ($Value -is [ValueType]) # Nested tables stop at three levels, so self-referencing objects cannot recurse forever if (-not $isScalar -and $script:ConfluenceTableNesting -lt 3) { $nestedRows = $null if ($Value -is [System.Collections.IDictionary]) { $nestedRows = @(, $Value) } elseif ($Value -is [System.Collections.IEnumerable] -and @($Value).Count -ge 1) { $nestedRows = @($Value) } elseif (@($Value.PSObject.Properties).Count -gt 1) { $nestedRows = @($Value) } if ($null -ne $nestedRows) { $script:ConfluenceTableNesting++ try { return (New-ConfluenceContentTable -TableData $nestedRows -Raw:$Raw) } finally { $script:ConfluenceTableNesting-- } } } $text = $Value.ToString() if ($Raw) { return $text } if ($DetectLinks) { return (ConvertTo-ConfluenceLinkedText -Text $text) } return (ConvertTo-ConfluenceXmlText -Text $text) } $TableHtml = "<table class='$TableType' style='$TableTypeStyle'>" if (-not $NoHeader) { $headerOpen = Get-HtmlFormatTag -Format $HeaderStringFormatting $headerClose = Get-HtmlFormatTag -Format $HeaderStringFormatting -Close $TableHtml += '<thead><tr>' foreach ($header in $firstRowProperties) { $TableHtml += "<th style='text-align: $HeaderAlignmentFormatting;' scope='col'>$headerOpen$(ConvertTo-ConfluenceXmlText -Text $header)$headerClose</th>" } $TableHtml += '</tr></thead>' } $firstOpen = Get-HtmlFormatTag -Format $FirstCellStringFormatting $firstClose = Get-HtmlFormatTag -Format $FirstCellStringFormatting -Close $cellOpen = Get-HtmlFormatTag -Format $CellStringFormatting $cellClose = Get-HtmlFormatTag -Format $CellStringFormatting -Close $useHeading = $FirstCellHeaderFormat -match '^[1-6]$' $TableHtml += '<tbody>' foreach ($row in $TableData) { $TableHtml += '<tr>' $isFirstCell = $true foreach ($column in $firstRowProperties) { $cellValue = & $getValue $row $column if ($isFirstCell) { $cellTag = if ($VerticalHeader) { 'th' } else { 'td' } $scope = if ($VerticalHeader) { " scope='row'" } else { '' } $wrapOpen = if ($useHeading) { "<h$FirstCellHeaderFormat>" } else { '<span>' } $wrapClose = if ($useHeading) { "</h$FirstCellHeaderFormat>" } else { '</span>' } $value = & $renderValue $cellValue $false $TableHtml += "<$cellTag$scope style='text-align: $FirstCellAlignmentFormatting;'>$wrapOpen$firstOpen$value$firstClose$wrapClose</$cellTag>" $isFirstCell = $false } else { $value = & $renderValue $cellValue $true $TableHtml += "<td style='text-align: $CellAlignmentFormatting;'>$cellOpen$value$cellClose</td>" } } $TableHtml += '</tr>' } $TableHtml += '</tbody></table>' return $TableHtml } catch { $telemetryFailed = $true Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_ throw } finally { if (-not $telemetryFailed) { Invoke-TelemetryCollection @TelemetryArgs -Stage End } } } |