NotebookOutput.ps1

<#
  Thank you to James O'Neill for contributing this script.
  Some really cool functions that can take your PowerShell .NET Interative notebooks to the next level.
#>


using namespace "Microsoft.DotNet.Interactive"

function Write-Notebook {
  <#
      .SYNOPSIS
        Writes to the output part of the current cell (a streamlined version of Out-Display)
 
      .PARAMETER Html
        Output to be sent as Hmtl
 
      .PARAMETER Text
        Output to be sent as plain text
 
      .PARAMETER PassThru
        If specified returns the output object, allowing it to be updated.
 
      .EXAMPLE
        > $statusMsg = Write-Notebook -PassThru -text "Step 1"
        > ...
        > $statusmsg.update("Step2")
 
        Displays and updates text in the current cell output
 
      .EXAMPLE
        > $PSVersionTable | ConvertTo-Html -Fragment | Write-Notebook
 
        Converts $psversionTable to a table and displays it. Without Write-Notebook the HTML markup would appear.
    #>

  [cmdletbinding(DefaultParameterSetName = 'Html')]
  param   (
    [parameter(Mandatory = $true, ParameterSetName = 'Html', ValueFromPipeline = $true, Position = 1 )]
    $Html,

    [parameter(Mandatory = $true, ParameterSetName = 'Text')]
    $Text,

    [Alias('PT')]
    [switch]$PassThru
  )
  begin { $htmlbody = @() }
  process { if ($html) { $htmlbody += $Html } }
  end {
    if ($htmlbody.count -gt 0) { $result = [Kernel]::display([Kernel]::HTML($htmlbody), 'text/html') }
    if ($Text) { $result = [Kernel]::display($Text, 'text/plain') }
    if ($PassThru) { return $result }
  }
}

function ConvertTo-Grid {
  param   (
    [parameter(ValueFromPipeline = $true, Position = 0, Mandatory = $true)]
    $InputObject,

    # .Net formatting string to apply to floating point numbers; default is N2 i.e. #,###.00
    [string]$FloatFormat = 'N2',

    # .Net formatting string to apply to integers; default is N0 i.e. #,###
    [string]$IntFormat = 'N0',

    #A title to appear above the grid, centred in H1 style
    [string]$TitleText,

    #css for the grid background and padding: default is middle grey and 5 pixels padding
    [string]$GridStyle = 'background-color: #7f7f7f; padding: 5px;',

    #css for alternating light and dark rows. to remove dark rows use an empty string
    [string]$DarkRowStyle = 'background-color: rgba(255, 255, 255, 0.8);',

    #css for normal cells in the grid
    [string]$ItemStyle = 'text-align: center; background-color: white; border: 1px solid rgba(0, 0, 0, 0.8); padding: 8px; font-size: 16px;' ,

    #css for normal cells in the grid, default is left aligned, bold white text on black background,
    [string]$ColumnHeadingStyle = 'text-align: left; background-color: black; color: white; font-weight: bold;',

    #css for normal cells in the grid
    [string]$RowLablelStyle = 'text-align: left; background-color: black; color: white; font-weight: bold;',

    #Displays the grid in the output of the notebook cell, instead of returning the html
    [switch]$Display,

    #Specifies the properties to select. Wildcards are permitted.
    [Parameter(Position = 1)]
    $Property,

    #Specifies the properties that the selection process excludes from the operation. Wildcards are permitted.
    $ExcludeProperty,

    #Specifies that if a subset of the input objects has identical properties and values, only a single member of the subset will be selected. Unique selects values after other filtering parameters are applied.
    [switch]$Unique,

    #Specifies the number of objects to select from the end of an array of input objects.
    [int32]$Last,

    #Specifies the number of objects to select from the beginning of an array of input objects.
    [int32]$First,

    #Skips (does not select) the specified number of items. By default, the Skip parameter counts from the beginning of the array or list of objects, but if the command uses the Last parameter, it counts from the end of the list or array.
    [int32]$Skip
  )
  begin {
    $rows = @()
    $html = @('<style>' , " .grid-container {display: grid; grid-template-columns: auto auto auto; $gridStyle}" ,
      " .grid-item {$ItemStyle}", '</style>') -join "`r`n"
  }
  process { $rows += $InputObject }
  end {
    if ($rows.count -eq 0) { return }

    $selectParams = @{}
    foreach ($param in @( 'Property', 'ExcludeProperty', 'Unique', 'Last', 'First', 'Skip')) {
      if ($PSBoundParameters.ContainsKey($param)) { $selectParams[$param] = $PSBoundParameters[$param] }
    }
    if ($selectParams.Count -ge 1) { $rows = $rows | Select-Object @selectParams }

    $properties = $rows[0].psobject.Properties.name

    if ($TitleText) {
      $html += "`r`n<center><h1>$TitleText</h1></center>"
    }
    $html += "`r`n<div class=`"grid-container`" style=`"grid-template-columns:$(' auto' * $properties.Count);`" >"
    foreach ($p in $properties) {
      $html += "`r`n <div class=`"grid-item`" style=`"$ColumnHeadingStyle`">$p</div>"
    }
    $html += "`r`n"
    $rowcount = 0
    foreach ($r in $rows) {
      $rowstyle = $rowcount ++ % 2 ? $DarkRowStyle : ""
      foreach ($p in $properties) {
        if ($null -eq $r.$p) {
          $html += "`r`n <div class=`"grid-item`" style=`"$rowstyle`"></div>"
          continue
        }
        if ($r.$p.GetType().name -match 'int') {
          $itemStyle = $rowstyle + "text-align: right;"; $itemText = $r.$p.tostring($IntFormat)
        }
        elseif ($r.$p -is [Single] -or
          $r.$p -is [double]) { $itemStyle = $rowstyle + "text-align: right;"; $itemText = $r.$p.tostring($FloatFormat) }
        elseif ($r.$p -is [boolean]) { $itemStyle = $rowstyle + "text-align: center;"; $itemText = $r.$p.tostring() }
        elseif ($r.$p -is [enum]) { $itemStyle = $rowstyle + "text-align: left;"; $itemText = $r.$p.tostring() }
        elseif ($r.$p -is [ValueType]) { $itemStyle = $rowstyle + "text-align: right;"; $itemText = $r.$p.tostring() }
        elseif ($r.$p -is [string]) { $itemStyle = $rowstyle + "text-align: left;"; $itemText = $r.$p }
        elseif ($r.$p -is [string]) { $itemStyle = $rowstyle + "text-align: left;"; $itemText = $r.$p.toString() }
        $html += "`r`n <div class=`"grid-item`" style=`"$itemStyle`">$itemText</div>"
      }
      $html += "`r`n"
    }
    $html += "</div>"
    if ($Display) { Write-Notebook -Html $html }
    else { $html }
  }
}

function Out-Cell {
  <#
      .SYNOPSIS
        Outputs a notebook cell - takes a script block, or html or objects to format as a list/table
 
      .DESCRIPTION
        This command has four ways of working. It can
        * Take HTML (via the pipeline or as a parameter) and output it into a cell
        * Make an HTML grid, table or list from objects, selecting properties, first, last etc.
        * Render objects as text (using Out-String)
        * Take a script block to generate html or objects to transform to a list/table
 
      .PARAMETER InputObject
        Specifies the objects to format, or HTML or a script block to excecute to generate the rquired output
 
      .PARAMETER AsGrid
        Specifies the object sould be output as an HTML grid (this uses ConvertTo-Grid )
 
      .PARAMETER GridOption
        Specifies additional options for ConvertTo-Grid when using -AsGrid
 
      .PARAMETER AsList
        Specifies the object sould be output as an HTML list (this uses ConvertTo-Html -Fragment -As "Grid" internaly)
 
      .PARAMETER AsTable
        Specifies the object sould be output as an HTML table (this uses ConvertTo-Html -Fragment -As Table internaly)
 
      .PARAMETER AsText
        Specifies the object sould be output as text (Similar to using Out-String, optionally combined with select-object)
 
      .PARAMETER Property
        Specifies the properties to select. Wildcards are permitted. Property, ExcludeProperty, Unique, Last, First and Skip work as they do with Select-Object (the function calls Select-object with these parameters),
 
      .PARAMETER ExcludeProperty
        Specifies the properties that the selection process excludes from the operation. Wildcards are permitted.
 
      .PARAMETER Unique
        Specifies that if a subset of the input objects has identical properties and values, only a single member of the subset will be selected. Unique selects values after other filtering parameters are applied.
 
      .PARAMETER Last
        Specifies the number of objects to select from the end of an array of input objects.
 
      .PARAMETER First
        Specifies the number of objects to select from the beginning of an array of input objects.
 
      .PARAMETER Skip
        Skips (does not select) the specified number of items. By default, the Skip parameter counts from the beginning of the array or list of objects, but if the command uses the Last parameter, it counts from the end of the list or array.
 
 
      .EXAMPLE
        > Get-command -Module Microsoft.DotNet.Interactive.PowerShell | Out-Cell -AsTable -Property name,version
 
        Takes CommandInfo objects and displays their name and version as an HTML Table
        ---------------------------
        | Name | Version |
        ---------------------------
        | Enter-AzShell | 0.1.0 |
        | Out-Display | 0.1.0 |
        ---------------------------
 
 
      .EXAMPLE
        > Get-command -Module Microsoft.DotNet.Interactive.PowerShell | Out-Cell -AsText -Property name,version
 
        Renders the output as console-style text
            Name Version
            ---- -------
            Enter-AzShell 0.1.0
            Out-Display 0.1.0
 
      .EXAMPLE
        > Cell { Get-command -Module Microsoft.DotNet.Interactive.PowerShell } -AsList name,version
 
        Similar to the previous object this uses the alias "cell" and uses a script block to create
        the objects and formats their name and version as an HTML List
        ----------------------------
        | Name: | Enter-AzShell |
        | Version: | 0.1.0 |
        ----------------------------
        | Name: | Out-Display |
        | Version: | 0.1.0 |
        ----------------------------
 
      .EXAMPLE
        > cell {plot_pipeline FollowOn.pipeline.yml -DestinationPath "" }
 
        In this example, `plot_pipeline` reads a yml file and draws a simple SVG graph.
        Out-cell will assume the script block is creating HTML because it has not been told as a list, table or grid
    #>

  [cmdletbinding(DefaultParameterSetName = "Default")]
  [alias("cell")]
  param   (
    [Parameter(ParameterSetName = 'Default', Mandatory = $true, Position = 0, ValueFromPipeline = $true )]
    [Parameter(ParameterSetName = 'List', Mandatory = $true, Position = 0, ValueFromPipeline = $true )]
    [Parameter(ParameterSetName = 'Table', Mandatory = $true, Position = 0, ValueFromPipeline = $true )]
    [Parameter(ParameterSetName = 'Grid', Mandatory = $true, Position = 0, ValueFromPipeline = $true )]
    [Parameter(ParameterSetName = 'Text', Mandatory = $true, Position = 0, ValueFromPipeline = $true )]
    $InputObject,

    [Parameter(ParameterSetName = 'List', Mandatory = $true)]
    [Alias('List')]
    [switch]$AsList,

    [Parameter(ParameterSetName = 'Table', Mandatory = $true)]
    [Alias('Table')]
    [switch]$AsTable,

    [Parameter(ParameterSetName = 'Grid', Mandatory = $true)]
    [switch]$AsGrid,

    [Parameter(ParameterSetName = 'Grid')]
    [hashtable]$GridOptions,

    [Parameter(ParameterSetName = 'Text', Mandatory = $true)]
    [Alias('Text')]
    [switch]$AsText,

    [Parameter(ParameterSetName = 'Grid' , Position = 1)]
    [Parameter(ParameterSetName = 'List' , Position = 1)]
    [Parameter(ParameterSetName = 'Table', Position = 1)]
    [Parameter(ParameterSetName = 'Text' , Position = 1)]
    $Property,

        [Parameter(ParameterSetName='Grid')]
        [Parameter(ParameterSetName='List')]
        [Parameter(ParameterSetName='Table')]
        [Parameter(ParameterSetName='Text')]
        [string[]]$ExcludeProperty,

        [Parameter(ParameterSetName='Grid')]
        [Parameter(ParameterSetName='List')]
        [Parameter(ParameterSetName='Table')]
        [Parameter(ParameterSetName='Text')]
        [int32]$First,

        [Parameter(ParameterSetName='Grid')]
        [Parameter(ParameterSetName='List')]
        [Parameter(ParameterSetName='Table')]
        [Parameter(ParameterSetName='Text')]
        [int32]$Last,

        [Parameter(ParameterSetName='Grid')]
        [Parameter(ParameterSetName='List')]
        [Parameter(ParameterSetName='Table')]
        [Parameter(ParameterSetName='Text')]
        [int32]$Skip,

        [switch]$PassThru
    )
    begin   {
            $data = @()
    }
    process {
        if ($InputObject -is [scriptblock]) {
                $data += Invoke-Command -ScriptBlock $InputObject
        }
        else {  $data += $InputObject }
    }
    end     {
        # if we're not told to render as something assume we've recieved HTML
        if (-not ($AsGrid -or $AsList -or $AsTable -or $AsText)) {
                $html = $data -join ""
        }
        else {
            $selectParameterNames = @("First","Last","Skip","Property","ExcludeProperty")
            if ($PSBoundParameters[$selectParameterNames]) {
                $selectParams = @{}
                foreach ($p in $selectParameterNames.Where({$PSBoundParameters.ContainsKey($_)})) {
                    $selectParams[$p]=$PSBoundParameters[$p]
                }
                $data = $data | Select-Object @selectParams
            }
            if ($GridOptions) {$html = $data | ConvertTo-Grid @GridOptions}
            elseif  ($AsGrid) {$html = $data | ConvertTo-Grid }
            elseif  ($AsList) {$html = $data | ConvertTo-Html -Fragment -As "List"}
            elseif ($AsTable) {$html = $data | ConvertTo-Html -Fragment -As "Table"}
            elseif  ($AsText) {
                $t = $data | Out-String
                Write-Notebook -Text $t.Trim() -PassThru:$PassThru
                return
            }
        }
        Write-Notebook -Html $html -PassThru:$PassThru
    }
}

function Write-Progress {
    <#
      .SYNOPSIS
        Notebook friendly replacement for the Write-Progress cmdlet. Similar to the "Minimal" view implemented in PowerShell 7.2
    #>

    param (
        [Parameter(Mandatory=$true,position = 0)]
        [string]$Activity,
        [Parameter(position = 1)]
        [string]$Status,
        [string]$CurrentOperation,
        [int]$PercentComplete,
        [int]$SecondsRemaining,
        [switch]$Completed
    )
    if ($status)           {$bar  = "{0,-100}"  -f $Status}
    else                   {$bar  = "{0,-100}"  -f $CurrentOperation} #even if it is empty!
    if ($PercentComplete)  {$bar  = $PSStyle.Background.blue + $PSStyle.Foreground.BrightWhite +
                                    ($bar -replace "(?<=^.{$percentComplete})", ($PSStyle.Reset + $PSStyle.Foreground.blue))
    }
    if ($SecondsRemaining) {$bar += $SecondsRemaining.tostring("0s") }
    if ($Completed)        {$bar  = ' '}
    else                   {$bar  = $PSStyle.Foreground.blue + $Activity +  "[" + $bar + "]" + $PSStyle.Reset}

    if ($global:ProgressBar -and $global:contextID -eq  [KernelInvocationContext]::Current.Command.Properties.id) {
        $global:ProgressBar.Update($bar)
    }
    else {
        $global:ProgressBar =  Write-Notebook -Text $bar -PassThru
        $global:contextID   =  [KernelInvocationContext]::Current.Command.Properties.id
    }
}

function Out-Mermaid    {
    <#
      .DESCRIPTION
        Accepts a mermaid chart definition as a parameter (example with the definition) or from the pipeline
        and outputs the minimum correct HTML / Javascript but **depends on the kernel extension being loaded**
 
        For examples see the mermaid home page at https://mermaid-js.github.io/mermaid/#/
        Has an alias of `Mermaid` it can be called in a more dsl-y style);
 
        .EXAMPLE
        ps >Mermaid @'
        sequenceDiagram
            participant Alice
            participant Bob
            Alice->>John: Hello John, how are you?
            loop Healthcheck
                John->>John: Fight against hypochondria
            end
            Note right of John: Rational thoughts <br/>prevail!
            John-->>Alice: Great!
            John->>Bob: How about you?
            Bob-->>John: Jolly good!
        '@
 
        Outputs a sample diagram found on the mermaid home page
    #>

    [alias('Mermaid')]
    param   (
        [parameter(ValueFromPipeline=$true,Mandatory=$true,Position=0)]
        $Text
    )
    begin   {
        $mermaid = ""
        $guid    = ([guid]::NewGuid().ToString() -replace '\W','')
        $html    = @"
<div style="background-color:white;"><script type="text/javascript">
loadMermaid_$guid = () => {(require.config({ 'paths': { 'context': '1.0.252001', 'mermaidUri' : 'https://colombod.github.io/dotnet-interactive-cdn/extensionlab/1.0.252001/mermaid/mermaidapi', 'urlArgs': 'cacheBuster=7de2aec4927849b5a989d2305cf957bc' }}) || require)(['mermaidUri'], (mermaid) => {let renderTarget = document.getElementById('$guid'); mermaid.render( 'mermaid_$guid', ``~~Mermaid~~``, g => {renderTarget.innerHTML = g });}, (error) => {console.log(error);});}
if ((typeof(require) !== typeof(Function)) || (typeof(require.config) !== typeof(Function))) {
    let require_script = document.createElement('script');
    require_script.setAttribute('src', 'https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js');
    require_script.setAttribute('type', 'text/javascript');
    require_script.onload = function() {loadMermaid_$guid();};
    document.getElementsByTagName('head')[0].appendChild(require_script);
}
else {loadMermaid_$guid();}
</script><div id="$guid"></div></div>
"@
  }
    process {$Mermaid +=  ("`r`n" + $Text -replace '^[\r\n]+','' -replace '[\r\n]+$','') }
    end     {Write-Notebook -Html  ($html -replace  '~~Mermaid~~',$mermaid ) }
}

function Out-TreeView   {
    <#
      .SYNOPSIS
        Outputs a treeview to a notebook.
 
      .DESCRIPTION
        C# can show JSON as a tree view; this takes an input in a similar way to Format-Table and Creates the same HTML
 
      .PARAMETER InputObject
        Specifies the objects to format. Enter a variable that contains the objects, or type a command or expression that gets the objects.
 
      .PARAMETER Property
        Specifies the properties to select. Wildcards are permitted. Property, ExcludeProperty, Unique, Last, First and Skip work as they do with Select-Object (the function calls Select-object with these parameters),
 
      .PARAMETER ExcludeProperty
        Specifies the properties that the selection process excludes from the operation. Wildcards are permitted.
 
      .PARAMETER Display
        If specified the tree view will be displayed, otherwise it will will be returned as a HTML
 
      .PARAMETER Unique
        Specifies that if a subset of the input objects has identical properties and values, only a single member of the subset will be selected. Unique selects values after other filtering parameters are applied.
 
      .PARAMETER Last
        Specifies the number of objects to select from the end of an array of input objects.
 
      .PARAMETER First
        Specifies the number of objects to select from the beginning of an array of input objects.
 
      .PARAMETER Skip
        Skips (does not select) the specified number of items. By default, the Skip parameter counts from the beginning of the array or list of objects, but if the command uses the Last parameter, it counts from the end of the list or array.
 
      .PARAMETER TreeviewCss
        Replaces the default style sheet for formatting
 
      .PARAMETER TitleHtml
        The title to be included - formatted as HTML
 
      .EXAMPLE
        > $Sales = Import-Csv .\SalesByEmployee.csv
        > Out-TreeView $sales.where({$_.name -eq 'Jim'}) -TitleHtml '<b>Jim</b>' -ExcludeProperty Name -Display
 
        Selects the sales for a single employee and displays
        > Jim
        Clicking the arrow expands this to
        \/ Jim
            ---------------------------------------------
            | Date | Customer | Revenue | Expenses |
            ---------------------------------------------
            | 1/1/20 | PaperGenius | 1864 | 1305 |
            | 2/2/20 | Money Corp. | 8278 | 462 |
            ---------------------------------------------
 
      .EXAMPLE
          > $groupby = 'Name'
          > $groupedSales = Import-Csv .\SalesByEmployee.csv | Group-object -Property $groupby
          > $subtrees = foreach ($g in $groupedSales) {
          > [pscustomobject]@{$groupBy = (Out-TreeView $g.Group -TitleHtml $g.Name)}
          > }
          > $subtrees | Out-TreeView -Title Sales -Display
 
           Here the sales are grouped and the for loop greates a subtree for each Name
           The subtrees are gathered together in a parent tree labelled sales.
           By specifying that each tree is "name" property of an object. (Which might have have other properties)
           When the Sales tree expands we have a column "Name" which contains trees like the ones in the first example
           Initiallly it displays
           > Sales
 
           Clicking on the arrow expands this to
           \/ Sales
                  Name
               > Alex
               > Jim
               > Phil
 
            Clicking one of the Name arrows expands to
           \/ Sales
                  Name
               > Alex
               \/ Jim
                    ---------------------------------------------
                    | Date | Customer | Revenue | Expenses |
                    ---------------------------------------------
                    | 1/1/20 | PaperGenius | 1864 | 1305 |
                    | 2/2/20 | Money Corp. | 8278 | 462 |
                    ---------------------------------------------
               > Phil
 
    #>

    param   (
        [Parameter(Position=0, ValueFromPipeline=$true, Mandatory=$true)]
        $InputObject,
        [string]$TitleHtml ='Tree view',
        [Parameter(Position=1)]
        $Property,
        $ExcludeProperty,
        [switch]$Display,
        [switch]$Unique,
        [int32]$Last,
        [int32]$First,
        [int32]$Skip,
        [string]$TreeviewCss
    )
    begin   {
        if ($ExcludeProperty -and -not $Property) {$Property = '*'}
        $rows = @()
        if (-not $PSBoundParameters.ContainsKey('$TreeviewCss')) { $treeViewCss = @'
<style id="dni-styles-JsonElement">
    .dni-code-hint { font-style: italic; overflow: hidden; white-space: nowrap;}
    .dni-treeview { white-space: nowrap; }
    .dni-treeview td { vertical-align: top;}
    details.dni-treeview {padding-left: 1em;}
</style>
'@
 }}
    process { $rows += $InputObject}
    end     {
        $selectParams = @{}
        foreach ($param in @( 'Property', 'ExcludeProperty', 'Unique', 'Last', 'First', 'Skip')){
            if ($PSBoundParameters.ContainsKey($param)) {$selectParams[$param] = $PSBoundParameters[$param]}
        }
        if     ($selectParams.Count -ge 1) {$rows = $rows | Select-Object @selectParams }
        #using psobject.Properties we get them in the correct order, not alphabetically.
        $propNames   = $rows[0].psobject.Properties.name
        $outputHtml  = '<details class="dni-treeview"><summary><span class="dni-code-hint">{0}</span></summary><div><table>' -f $TitleHtml
        $outputHtml += "`r`n <thead>`r`n <tr>"
        foreach ($p in $propNames) {$outputHtml += "<td>$p</td>"}
        $outputHtml += "</tr>`r`n </thead>`r`n <tbody>"
        foreach ($r in $rows) {
            $outputHtml += "`r`n <tr>"
            foreach ($p in $propNames) {
                $outputHtml += '<td><span><div class="dni-plaintext">{0}</div></span></td>' -f $r.$p
            }
            $outputHtml += "</tr>"
        }
        $outputHtml += "`r`n </tbody>`r`n</table></div></details>`r`n"
        if ($Display) {Write-Notebook -Html ($outputHtml + $treeViewCss) }
        else          {[Kernel]::HTML( $outputHtml)  }
    }
}