Skip to content

Instantly share code, notes, and snippets.

@scriptingstudio
Last active May 14, 2024 09:18
Show Gist options
  • Save scriptingstudio/2302877441c1eb0d1ce07025562b887a to your computer and use it in GitHub Desktop.
Save scriptingstudio/2302877441c1eb0d1ce07025562b887a to your computer and use it in GitHub Desktop.
Simple and robust PowerShell object to HTML table converter that just works
<#
.SYNOPSIS
Creates HTML table from .NET objects.
.DESCRIPTION
This is basic cmdlet, as a helper, to build HTML tables for complex HTML content.
Converts .NET objects into HTML that can be displayed in a Web browser.
This cmdlet works like "ConvertTo-Html -Fragment" but does not have any of its disadvantages, allowing HTML entities in cells, omiting the first column colons and allowing multi-column tables in the List mode, etc.
Features:
- Parameterset autoresolve. There are no predefined parameter sets
- Input data preprocessor: sorting and filtering
- HTML table format modes: List or Table
- Custom HTML header in the Table mode
- Possibility to generate a headless table
- Multi-column tables in the List mode
- Table identification via HTML attributes "id" and "class"
- Individual cell styling via external user-defined CSS classes
- Table <tfoot> tag enabler
- Mandatory <thead> tag in the Table mode
- Customizable table totals row using <tfoot> tag
- Precontent and postcontent blocks
- Clean HTML layout
.PARAMETER InputObject
Specifies the objects to be represented in HTML. Enter a variable that contains the objects or type a command or expression that gets the objects.
.PARAMETER Select
Specifies the properties to select in InputObject. Wildcards are not permitted. If the input object doesn't have the property named, this property will be removed from the Select.
.PARAMETER Header
Specifies custom property names to use in the HTML, instead of the input object properties.
.PARAMETER PostContent
Specifies text to add after the closing </table> tag. By default, there is no text in that position.
.PARAMETER PreContent
Specifies text to add before the opening <table> tag. By default, there is no text in that position.
.PARAMETER TabClass
Specifies class attribute for the opening <table> tag.
.PARAMETER TabId
Specifies id attribute for the opening <table> tag.
.PARAMETER NoHeader
Indicates that field names should not be put at the top of columns.
.PARAMETER Tfoot
Indicates to insert <tfoot> tag in HTML.
.PARAMETER Sort
Indicates to sort input data by argument before converting to HTML.
.PARAMETER Descending
Indicates that the objects are sorted in descending order. The default is ascending order. Alternatively the descending value can be set in the Sort expression as well.
.PARAMETER Marker
Indicates to insert sorting direction label ("▾"/"▴") after column name. This parameter mostly makes sense when the Sort arguments are strings.
.PARAMETER List
Determines that the object is formatted as a list. The default format is table.
The Table mode generates an HTML table that resembles the PowerShell table format. The header row displays the property names. Each table row represents an object and displays the object's values for each property.
The List mode generates a multi-column HTML table. The first column displays the property name. The others display the property values. The List mode acts as matrix trasposition.
.PARAMETER Colon
Indicates to insert colon char after the first column values in the List mode.
.PARAMETER Totals
Specifies a configuration table to add Totals row.
Configuration format as a hashtable:
@{
_label_ = <name>|blank
<column_name> = @{
action = 'sum'|'avg'|'min'|'max'|'count' # predefined functions
expression = <scriptblock> # expression to prepare/shape/clean data, internal autovariable is $_, example {$_ / 10}
format = <scriptblock> # final shaper, internal autovariable is $args, example {'{0:N2}' -f $args}
}
}
.PARAMETER Style
Specifies a configuration table to style columns and header using CSS classes. This approach overcomes the limited possibilities to style the <col> tag.
Configuration table contains 2 records: for columns and for header to style columns individually.
Configuration format as a hashtable:
@{
column = @{ # column cell configurator; the key word is "column"
col1 = 'class1','class2' # <column_name>=<class_name>[,<class_name>]
col2 = 'class4','class5'
}
header = @{ # header cell configurator; the key word is "header"
col1 = 'hclass1','hclass2' # <column_name>=<class_name>[,<class_name>]
}
}
.EXAMPLE
.EXAMPLE
.EXAMPLE
.INPUTS
You can pipe any object to this cmdlet.
.OUTPUTS
This cmdlet returns an array of strings of HTML representing the converted input objects.
.NOTES
- The Header parameter applies to HTML table in the Table mode only. The Select parameter always applies to input data.
- Sorting mostly makes sense in the Table mode.
- Select, Sort, and Descending parameters apply to the input objects. These are data preprocessor.
#>
function ConvertTo-HtmlTable {
[cmdletbinding()]
[alias('ctht')]
param (
[Parameter(Position=0,Mandatory,ValueFromPipeline)]
[alias('Dataset','Body')]$InputObject,
[alias('Properties','Property')][string[]]$Select,
[alias('HeaderName','HtmlHeader')][string[]]$Header, # table mode
[switch]$NoHeader, # table mode
[alias('Title')][string[]]$PreContent,
[alias('Footer')][string[]]$PostContent,
[string[]]$TabClass, # adds html attribute "class"
[string]$TabId, # adds html attribute "id"
[switch]$Tfoot, # adds tfoot tag
$Sort,
[switch]$Descending,
[switch]$Marker, # inserts sorting direction char after the column name
[alias('Transpose','Aslist')][switch]$List,
[switch]$Colon, # list mode
[hashtable]$Totals, # @{_label_=name|blank; colname=@{action=sum|avg|min|max|count; expression=scriptblock}; format=<scriptblock>}
[hashtable]$Style # @{column=@{colname=class[]}; header=@{colname=class[]}}
)
begin {$collection = [System.Collections.Generic.List[object]]::new()}
process {if ($InputObject) {$collection.addrange(@($InputObject))}}
end {
if (-not $collection.count) {return}
[string[]]$dataHeader = $collection[0].psobject.properties.name
if (-not $dataHeader.count) {return} # not a psobject
# Adjust table structure; resolve data structure dependencies
if ($Noheader -or ($List -and $Header)) {$Header = @()} # prohibition priority
if ($Select) { # junk filter
$Select = $Select | . {end{[System.Collections.Generic.HashSet[object]]::new(@($input))}}
$Select = @($Select).where{$_ -in $dataHeader}
}
if ($Header) {$Header = $Header | . {end{[System.Collections.Generic.HashSet[object]]::new(@($input))}}}
# adjust properties: check and cut selection
if ($Select -and $Select.count -gt ($dataHeader.count + 1)) {
$Select = $Select[0..($dataHeader.count - 1)]
}
if ($Select -and $Header) {
if ($Header.count -gt $Select.count) {
$Header = $Header[0..($Select.count-1)]
} elseif ($Header.count -lt $Select.count) {
$Header = $Select
}
}
elseif ($Header) {
$Select = $Header
if ($Header.count -gt $Select.count) {
$Header = $Header[0..($Select.count-1)]
} elseif ($Header.count -lt $Select.count) {
$Header = $Select
}
$Select = @()
}
# filter data
if ($Select) {$collection = ($collection | Select-Object $Select)}
[string[]]$dataHeader = $collection[0].psobject.properties.name
if (-not $Header -and -not $Noheader) {$Header = $dataHeader}
# Totals row constructor
$TRadd = $false
if ($Totals.count -and -not $list) {
$totalrow = [pscustomobject]@{} | Select-Object $dataHeader
try {
foreach ($p in $dataHeader) {
if ($Totals[$p]) {
$action = $Totals[$p]
$param = @{}; $func = ''
switch ($action.action) {
'Sum' {$param['Sum'] = $true; $func = 'Sum'}
'Avg' {$param['Average'] = $true; $func = 'Average'}
'Min' {$param['Minimum'] = $true; $func = 'Minimum'}
'Max' {$param['Maximum'] = $true; $func = 'Maximum'}
'Count' {$func = 'Count'}
}
if ($func) {
$expr = if ($action.expression) {$action.expression} else {{$_}}
$value = (($collection.$p).foreach($expr) | Measure-Object @param -ErrorAction 0).$func
$totalrow.$p = if ($action.format) {. $action.format $value} else {$value}
$TRadd = $true
}
}
} # header enumerator
} catch {}
if ($TRadd) {
$label = $Totals['_label_']
$totalrow.($dataHeader[0]) = if ($label -eq 'blank') {''} elseif ($label) {$label} else {'Totals'}
$Tfoot = $true
}
} # end totals
# Table row constructor
$colfmt = $headfmt = '' # cell formatter options
if ($style.count) {
if ($style['column'].count) {$colfmt = $style['column']}
if ($style['header'].count) {$headfmt = $style['header']}
}
if ($list) { # list mode
$colgroup = '<colgroup>{0}</colgroup>' -f ('<col/>' * ($collection.count+1))
$c = if ($Colon) {':'}
# TR,TD constructor
$rows = $dataHeader.foreach{
$td = $collection.$_ | . { begin {$i=1} process {
$cclass = $colfmt[$i]
if (-not $cclass) {$cclass = $colfmt["$i"]}
$cclass = if ($cclass) {' class="{0}"' -f "$cclass"}
"<td$cclass>$_</td>"
$i++
}}
$cclass = if ($colfmt.count) {
$cclass = $colfmt[1]
if (-not $cclass) {$cclass = $colfmt["1"]}
$cclass = if ($cclass) {' class="{0}"' -f "$cclass"}
}
"<tr><td>${_}$c$cclass</td>$td</tr>"
} # rows
}
else { # table mode
if ($collection.count -lt 2) {$Sort = $null} # no sense, reset
$sortdir = if ($Sort -and $Marker) {
if ($Descending) {' ▾'} else {' ▴'}
}
$colgroup = '<colgroup>{0}</colgroup>' -f ('<col/>' * $dataHeader.count)
# input data processor scriptblock
$dataexpr = if ($sort) {
$sparam = @{}
if ($Descending) {$sparam['Descending'] = $true}
if ($TRadd) {{@($collection | Sort-Object $sort @sparam) + $totalrow}}
else {{$collection | Sort-Object $sort @sparam}}
} else {
if ($TRadd) {{@($collection) + $totalrow}}
else {{$collection}}
}
# TR,TD constructor
$firstcol = $dataHeader[0]
$rows = . $dataexpr | . { begin {$s=0} process {
$td = foreach ($p in $dataHeader) {
$cclass = $colfmt[$p]
$cclass = if ($cclass) {' class="{0}"' -f "$cclass"}
"<td$cclass>$($_.$p)</td>"
}
"<tr>$td</tr>"
}}
# TH,THEAD constructor
$thead = if (-not $Noheader) {
# extract property names from the Sort expression to build a name list
$columns = @($sort).Where{$_}.ForEach{
$test = if ($_.GetType().name -eq 'hashtable') {$_.Expression} else {$_}
$rx = if ($_.GetType().name -eq 'string') {'(\w+)'} else {'\$_\.(\w+)[ $\}]?'}
[regex]::matches("$test",$rx).groups.where{$_.success -and $_.name -eq '1'}.value
}
#$columns = $sort
$th = $Header.foreach{ # $Header | . { begin {$s=0} process {
$hclass = $headfmt[$_]
$hclass = if ($hclass) {' class="{0}"' -f "$hclass"}
$m = if ($sortdir -and $_ -in $columns) {$sortdir}
"<th$hclass>$_$m</th>"
}
"<thead>","<tr>$th</tr>","</thead>"
}
# footer constructor
$tfooter = if ($tfoot -and
((-not $Noheader -and $rows.count -gt 2) -or
($Noheader -and $rows.count -gt 1))) {
"<tfoot>","$($rows[-1])","</tfoot>"
$rows = $rows[0..($rows.count - 2)]
}
} # table row constructor
# Output
# assemble the final HTML table from its parts
if ($TabId) {$TabId = ' id="{0}"' -f $TabId}
if ($TabClass) {$TabId = '{0} class="{1}"' -f $TabId, "$TabClass"}
if ($PreContent) {$PreContent}
"<table$TabId>"
$colgroup
$thead
"<tbody>"
$rows
"</tbody>"
$tfooter
"</table>"
if ($PostContent) {$PostContent}
} # end
} # END ConvertTo-HtmlTable
# Example 1
$style = @{
column = @{p3='right','yellow'}
header = @{p2='center'}
}
$totals = @{'_label_'='Subtotal'; p2=@{action='sum'}; p3=@{action='avg'; expression={$_ -replace '\D'}}}
ConvertFrom-Csv @"
p1,p2,p3,p4
11,12,13,14
44,45,47,49
<span>888</span>,388,588,788
"@ |
ConvertTo-HtmlTable -TabId tab1 -TabClass class1,class2 -style $style -totals $totals
# output
<table id="tab1" class="class1 class2">
<colgroup><col/><col/><col/></colgroup>
<thead>
<tr><th>p1</th> <th class="center">p2</th> <th>p3</th></tr>
</thead>
<tbody>
<tr><td>11</td> <td>44</td> <td class="right yellow"><span>888</span></td></tr>
<tr><td>12</td> <td>45</td> <td class="right yellow">388</td></tr>
<tr><td>13</td> <td>47</td> <td class="right yellow">588</td></tr>
<tr><td>14</td> <td>48</td> <td class="right yellow">788</td></tr>
</tbody>
<tfoot>
<tr><td>Subtotal</td> <td></td> <td class="right yellow">663</td></tr>
</tfoot>
</table>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment