Private/HTML/ConvertTo-EnhancedHTMLFragment.ps1
<#
The MIT License (MIT) Copyright (c) 2015 Objectivity Bespoke Software Specialists Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #> function ConvertTo-EnhancedHTMLFragment { <# .SYNOPSIS Creates an HTML fragment. .DESCRIPTION This is a modified version of the cmdlet created by Don Jones and described in the book 'Creating HTML Reports in PowerShell' (http://powershell.org/wp/ebooks/). Following modifications have been made: - removed $EvenRowCssClass / $OddRowCssClass - not needed when using DataTables - added $IsTotalRow to enable generating table footer (needed for totals in DataTables) - extended properties with 'format' to enable decimal formatting - refactored the code for more clarity, used StringBuilder instead of string joining .PARAMETER InputObject The object to be converted to HTML. You cannot select properties using this command; precede this command with Select-Object if you need a subset of the objects' properties. .PARAMETER TableCssID Optional. The CSS ID name applied to the <TABLE> tag. .PARAMETER DivCssID Optional. The CSS ID name applied to the <DIV> tag which is wrapped around the table. .PARAMETER TableCssClass Optional. The CSS class name to apply to the <TABLE> tag. .PARAMETER DivCssClass Optional. The CSS class name to apply to the wrapping <DIV> tag. .PARAMETER As Must be 'List' or 'Table.' Defaults to Table. Actually produces an HTML table either way; with Table the output is a grid-like display. With List the output is a two-column table with properties in the left column and values in the right column. .PARAMETER Properties A comma-separated list of properties to include in the HTML fragment. This can be * (which is the default) to include all properties of the piped-in object(s). In addition to property names, you can also use a hashtable similar to that used with Select-Object. For example: Get-Process | ConvertTo-EnhancedHTMLFragment -As Table ` -Properties Name,ID,@{n='VM'; e={$_.VM}; css={if ($_.VM -gt 100) { 'red' } else { 'green' }}} This will create table cell rows with the calculated CSS class names. E.g., for a process with a VM greater than 100, you'd get: <TD class="red">475858</TD> You can use this feature to specify a CSS class for each table cell based upon the contents of that cell. Valid keys in the hashtable are: n, name, l, or label: The table column header e or expression: The table cell contents css or csslcass: The CSS class name to apply to the <TD> tag Another example: @{n='Free(MB)'; e={$_.FreeSpace / 1MB -as [int]}; css={ if ($_.FreeSpace -lt 100) { 'red' } else { 'blue' }} This example creates a column titled "Free(MB)". It will contain the input object's FreeSpace property, divided by 1MB and cast as a whole number (integer). If the value is less than 100, the table cell will be given the CSS class "red." If not, the table cell will be given the CSS class "blue." The supplied cascading style sheet must define ".red" and ".blue" for those to have any effect. .PARAMETER PreContent Raw HTML content to be placed before the wrapping <DIV> tag. For example: -PreContent "<h2>Section A</h2>" .PARAMETER PostContent Raw HTML content to be placed after the wrapping <DIV> tag. For example: -PostContent "<hr />" .PARAMETER MakeHiddenSection Used in conjunction with -PreContent. Adding this switch, which needs no value, turns your -PreContent into clickable report section header. The section will be hidden by default, and clicking the header will toggle its visibility. When using this parameter, consider adding a symbol to your -PreContent that helps indicate this is an expandable section. For example: -PreContent '<h2>♦ My Section</h2>' If you use -MakeHiddenSection, you MUST provide -PreContent also, or the hidden section will not have a section header and will not be visible. .PARAMETER MakeTableDynamic When using "-As Table", makes the table dynamic. Will be ignored if you use "-As List". Dynamic tables are sortable, searchable, and are paginated. You should not use even/odd styling with tables that are made dynamic. Dynamic tables automatically have their own even/odd styling. You can apply CSS classes named ".odd" and ".even" in your CSS to style the even/odd in a dynamic table. .PARAMETER IsTotalRow Defines when the row is treated as 'total' and rendered in the table footer. For example 'IsTotalRow'={ $_.nameColumn -eq 'TOTAL' } will render the row with column 'nameColumn' = 'TOTAL' in the footer. .EXAMPLE $fragment = Get-WmiObject -Class Win32_LogicalDisk | Select-Object -Property PSComputerName,DeviceID,FreeSpace,Size | ConvertTo-EnhancedHTMLFragment -EvenRowClass 'even' ` -OddRowClass 'odd' ` -PreContent '<h2>Disk Report</h2>' ` -MakeHiddenSection ` -MakeTableDynamic You will usually save fragments to a variable, so that multiple fragments (each in its own variable) can be passed to ConvertTo-EnhancedHTML. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory=$True,ValueFromPipeline=$True)] [object[]]$InputObject, [string]$TableCssID, [string]$DivCssID, [string]$DivCssClass, [string]$TableCssClass = "display", [ValidateSet('List','Table')] [string]$As = 'Table', [object[]]$Properties = '*', [string]$PreContent, [switch]$MakeHiddenSection, [switch]$MakeTableDynamic, [string]$PostContent, [scriptblock]$IsTotalRow ) BEGIN { <# Accumulate output in a variable so that we don't produce an array of strings to the pipeline, but instead produce a single string. #> $out = New-Object System.Text.StringBuilder <# Add the section header (pre-content). If asked to make this section of the report hidden, set the appropriate code on the section header to toggle the underlying table. Note that we generate a GUID to use as an additional ID on the <div>, so that we can uniquely refer to it without relying on the user supplying us with a unique ID. #> Write-Verbose "Precontent" if ($PSBoundParameters.ContainsKey('PreContent')) { if ($PSBoundParameters.ContainsKey('MakeHiddenSection')) { [string]$tempid = [System.Guid]::NewGuid() [void]$out.Append("<span class=`"sectionheader`" onclick=`"`$('#$tempid').toggle(500);`">$PreContent</span>`n") } else { [void]$out.Append($PreContent) $tempid = '' } } <# The table will be wrapped in a <div> tag for styling purposes. Note that THIS, not the table per se, is what we hide for -MakeHiddenSection. So we will hide the section if asked to do so. #> Write-Verbose "DIV" if ($PSBoundParameters.ContainsKey('DivCSSClass')) { $temp = " class=`"$DivCSSClass`"" } else { $temp = "" } if ($PSBoundParameters.ContainsKey('MakeHiddenSection')) { $temp += " id=`"$tempid`" style=`"display:none;`"" } else { $tempid = '' } if ($PSBoundParameters.ContainsKey('DivCSSID')) { $temp += " id=`"$DivCSSID`"" } [void]$out.Append("<div $temp>") <# Create the table header. If asked to make the table dynamic, we add the CSS style that ConvertTo-EnhancedHTML will look for to dynamic-ize tables. #> Write-Verbose "TABLE" $_TableCssClass = '' if ($PSBoundParameters.ContainsKey('MakeTableDynamic') -and $As -eq 'Table') { $_TableCssClass += 'enhancedhtml-dynamic-table ' } if ($TableCssClass) { $_TableCssClass += $TableCssClass } if ($_TableCssClass -ne '') { $css = "class=`"$_TableCSSClass`"" } else { $css = "" } if ($PSBoundParameters.ContainsKey('TableCSSID')) { $css += "id=`"$TableCSSID`"" } else { if ($tempid -ne '') { $css += "id=`"$tempid`"" } } [void]$out.Append("<table $css>") <# We're now setting up to run through our input objects and create the table rows #> $wrote_first_line = $false $wrote_total = $false if ($properties -eq '*') { $all_properties = $true } else { $all_properties = $false } $rowNumber = 1 } PROCESS { foreach ($object in $inputobject) { Write-Verbose "Processing object" <# If asked to include all object properties, get them. #> if ($all_properties) { $properties = $object | Get-Member -MemberType Properties | Select -ExpandProperty Name } if ($PSBoundParameters.ContainsKey('IsTotalRow') -and ($Object | Where-Object $IsTotalRow)) { $isTotal = $true } $columns = Get-EnhancedHTMLProperties -Properties $properties -Object $object -RowNumber $rowNumber -IsTotal:$isTotal $rowNumber++ <# Write the table header, if we're doing a table. #> if (-not $wrote_first_line -and $as -eq 'Table') { Write-Verbose "Writing header row" $headerrow = $columns | Foreach { "<th>$($_.name)</th>" } [void]$out.Append("<thead><tr>$headerrow</tr></thead><tbody>") $wrote_first_line = $true } if ($isTotal) { [void]$out.Append("</tbody><tfoot>") $tableElement = "th" } else { $tableElement = "td" } <# When constructing a table, we have to remember the property names so that we can build the table header. In a list, it's easier - we output the property name and the value at the same time, since they both live on the same row of the output. #> if ($As -eq 'table') { $datarow = $columns | Foreach { "<${tableElement}$(if ($_.css -ne '') { ' class="'+ $_.css +'"' })>$($_.value)</${tableElement}>" } [void]$out.Append("<tr>$datarow</tr>") } else { $wrote_first_line = $true $datarow = $columns | Foreach { "<${tableElement}$(if ($_.css -ne '') { ' class="'+ $_.css +'"' })>$($_.name) :</${tableElement}><${tableElement}$(if ($_.css -ne '') { ' class="'+ $_.css +'"' })>$($_.value)</${tableElement}>" } [void]$out.Append("<tr>$datarow</tr>") } if ($isTotal) { [void]$out.Append("</tfoot>") $wrote_total = $true } } } END { <# Finally, post-content code, the end of the table, the end of the <div>, and write the final string. #> Write-Verbose "PostContent" if ($PSBoundParameters.ContainsKey('PostContent')) { [void]$out.Append("`n$PostContent") } Write-Verbose "Done" if (!$wrote_total) { [void]$out.Append("</tbody>") } [void]$out.Append("</table></div>") Write-Output $out.ToString() } } |