Private/Console/TableRenderer.psm1

using namespace System
using namespace System.Collections.Generic

using module ..\Enums.psm1
using module ..\Abstracts.psm1
using module .\Colors.psm1
using module .\Rendering.psm1
using module .\Widgets.psm1
using module .\Tables.psm1

class TableRendererContext {
  [object]$Table
  [RenderOptions]$Options
  [List[TableRow]]$Rows
  [int]$TableWidth
  [int]$MaxWidth

  [TableBorder]$Border
  [Style]$BorderStyle
  [bool]$ShowHeaders
  [bool]$ShowRowSeparators
  [bool]$ShowFooters
  [bool]$Expand
  [bool]$PadRightCell
  [bool]$IsGrid
  [bool]$ShowBorder
  [bool]$HideBorder
  [bool]$HasRows
  [bool]$HasFooters
  [List[TableColumn]]$Columns
  [TableTitle]$Title
  [TableTitle]$Caption

  TableRendererContext([object]$table, [RenderOptions]$options, [List[TableRow]]$rows, [int]$tableWidth, [int]$maxWidth) {
    $this.Table = $table
    $this.Options = $options
    $this.Rows = $rows
    $this.TableWidth = $tableWidth
    $this.MaxWidth = $maxWidth

    $this.Border = $table.Border
    $this.BorderStyle = if ($null -ne $table.BorderStyle) { $table.BorderStyle } else { [Style]::Plain }
    $this.ShowHeaders = $table.ShowHeaders
    $this.ShowRowSeparators = $table.ShowRowSeparators
    $this.ShowFooters = $table.ShowFooters
    $this.Expand = $table.Expand
    $this.PadRightCell = $table.PadRightCell
    $this.IsGrid = $table.IsGrid
    $this.ShowBorder = $table.Border.get_Visible()
    $this.HideBorder = !$this.ShowBorder
    $this.HasRows = $rows.Count -gt 0
    $this.HasFooters = $false
    foreach ($c in $table.Columns) { if ($null -ne $c.Footer) { $this.HasFooters = $true; break } }
    $this.Columns = $table.Columns
    $this.Title = $table.Title
    $this.Caption = $table.Caption
  }
}

class TableRenderer {
  static hidden [Style]$_defaultHeadingStyle = [Color]::Silver
  static hidden [Style]$_defaultCaptionStyle = [Color]::Grey

  static [List[Segment]] Render([TableRendererContext]$context, [List[int]]$columnWidths) {
    $badWidth = $false
    foreach ($w in $columnWidths) { if ($w -lt 0) { $badWidth = $true; break } }
    if ($context.TableWidth -le 0 -or $context.TableWidth -gt $context.MaxWidth -or $badWidth) {
      $ret = [List[Segment]]::new()
      $ret.Add([Segment]::new("…", $context.BorderStyle))
      return $ret
    }

    $result = [List[Segment]]::new()
    $result.AddRange([TableRenderer]::RenderAnnotation($context, $context.Title, [TableRenderer]::_defaultHeadingStyle))

    for ($index = 0; $index -lt $context.Rows.Count; $index++) {
      $row = $context.Rows[$index]
      $isFirstRow = ($index -eq 0)
      $isLastRow = ($index -eq $context.Rows.Count - 1)

      $cellHeight = 1
      # Array of custom objects holding state
      $cells = [List[object]]::new()
      $columnIndex = 0

      foreach ($item in $row.Items) {
        $cellRenderable = $item
        $span = 1
        if ($item -is [TableCell]) {
          $cellRenderable = $item.Content
          $span = $item.ColumnSpan
        }

        $cellWidth = $columnWidths[$columnIndex]
        if ($span -gt 1) {
          for ($i = 1; $i -lt $span; $i++) {
            if (($columnIndex + $i) -lt $columnWidths.Count) {
              if ($context.ShowBorder) { $cellWidth += 1 }
              $cellWidth += $columnWidths[$columnIndex + $i]
              if (($context.ShowBorder -and $context.Border.get_UsePadding()) -or $context.IsGrid) {
                $cellWidth += $context.Columns[$columnIndex + $i].Padding.GetLeftSafe()
                $cellWidth += $context.Columns[$columnIndex + $i].Padding.GetRightSafe()
              }
            }
          }
        }

        $align = $context.Columns[$columnIndex].Alignment
        $childContext = [RenderOptions]::new()
        $childContext.Ansi = $context.Options.Ansi
        $childContext.ColorSystem = $context.Options.ColorSystem
        $childContext.Justification = $align
        $childContext.Unicode = $context.Options.Unicode

        $segs = $cellRenderable.Render($childContext, $cellWidth)
        $lines = [Segment]::SplitLines($segs, $cellWidth)
        $cellHeight = [Math]::Max($cellHeight, $lines.Count)

        $cells.Add(@{ Lines = $lines; Width = $cellWidth; ColumnIndex = $columnIndex; Span = $span; IsNull = $false })

        for ($i = 1; $i -lt $span; $i++) {
          $cells.Add(@{ Lines = $null; Width = 0; ColumnIndex = $columnIndex + $i; Span = 0; IsNull = $true })
        }
        $columnIndex += $span
      }

      if ($isFirstRow -and $context.ShowBorder) {
        $sep = $context.Border.GetColumnRow([TablePart]::Top, $columnWidths, $context.Columns)
        if (-not [string]::IsNullOrEmpty($sep)) {
          $result.Add([Segment]::new($sep, $context.BorderStyle))
          $result.Add([Segment]::LineBreak)
        }
      }

      if ($context.ShowFooters -and $isLastRow -and $context.ShowBorder -and $context.HasFooters) {
        $tb = $context.Border.GetColumnRow([TablePart]::FooterSeparator, $columnWidths, $context.Columns)
        if (-not [string]::IsNullOrEmpty($tb)) {
          $result.Add([Segment]::new($tb, $context.BorderStyle))
          $result.Add([Segment]::LineBreak)
        }
      }

      foreach ($c in $cells) {
        if (!$c.IsNull -and $c.Lines.Count -lt $cellHeight) {
          while ($c.Lines.Count -lt $cellHeight) {
            $c.Lines.Add([SegmentLine]::new())
          }
        }
      }

      $firstNonNull = -1; $lastNonNull = -1
      for ($i = 0; $i -lt $cells.Count; $i++) {
        if (!$cells[$i].IsNull) {
          if ($firstNonNull -eq -1) { $firstNonNull = $i }
          $lastNonNull = $i
        }
      }

      for ($cellRowIndex = 0; $cellRowIndex -lt $cellHeight; $cellRowIndex++) {
        $rowResult = [List[Segment]]::new()

        for ($cellIndex = 0; $cellIndex -lt $cells.Count; $cellIndex++) {
          $cData = $cells[$cellIndex]
          if ($cData.IsNull) { continue }

          $isFirstCell = ($cellIndex -eq $firstNonNull)
          $isLastCell = ($cellIndex -eq $lastNonNull)
          $actualCol = $cData.ColumnIndex
          $cellLines = $cData.Lines
          $cellW = $cData.Width
          $cellSpan = $cData.Span

          if ($isFirstCell -and $context.ShowBorder) {
            $part = if ($isFirstRow -and $context.ShowHeaders) { [TableBorderPart]::HeaderLeft } else { [TableBorderPart]::CellLeft }
            $rowResult.Add([Segment]::new($context.Border.GetPart($part), $context.BorderStyle))
          }

          if (($context.ShowBorder -and $context.Border.get_UsePadding()) -or $context.IsGrid) {
            $lPad = $context.Columns[$actualCol].Padding.GetLeftSafe()
            if ($lPad -gt 0) { $rowResult.Add([Segment]::Padding($lPad)) }
          }

          $rowResult.AddRange($cellLines[$cellRowIndex].Segments)

          $len = $cellLines[$cellRowIndex].CellCount()
          if ($len -lt $cellW) {
            $rowResult.Add([Segment]::Padding($cellW - $len))
          }

          $rightCol = $actualCol + $cellSpan - 1
          if (($context.ShowBorder -and $context.Border.get_UsePadding()) -or ($context.HideBorder -and !$isLastCell) -or ($context.HideBorder -and $isLastCell -and $context.IsGrid -and $context.PadRightCell)) {
            $rPad = $context.Columns[$rightCol].Padding.GetRightSafe()
            if ($rPad -gt 0) { $rowResult.Add([Segment]::Padding($rPad)) }
          }

          if ($isLastCell -and $context.ShowBorder) {
            $part = if ($isFirstRow -and $context.ShowHeaders) { [TableBorderPart]::HeaderRight } else { [TableBorderPart]::CellRight }
            $rowResult.Add([Segment]::new($context.Border.GetPart($part), $context.BorderStyle))
          } elseif ($context.ShowBorder) {
            $part = if ($isFirstRow -and $context.ShowHeaders) { [TableBorderPart]::HeaderSeparator } else { [TableBorderPart]::CellSeparator }
            $rowResult.Add([Segment]::new($context.Border.GetPart($part), $context.BorderStyle))
          }
        }

        if ([Segment]::CellCount($rowResult) -gt $context.MaxWidth) {
          $result.AddRange([Segment]::Truncate($rowResult, $context.MaxWidth))
        } else {
          $result.AddRange($rowResult)
        }
        $result.Add([Segment]::LineBreak)
      }

      if ($isFirstRow -and $context.ShowBorder -and $context.ShowHeaders -and $context.HasRows) {
        $sep = $context.Border.GetColumnRow([TablePart]::HeaderSeparator, $columnWidths, $context.Columns)
        $result.Add([Segment]::new($sep, $context.BorderStyle))
        $result.Add([Segment]::LineBreak)
      }

      if ($context.Border.get_SupportsRowSeparator() -and $context.ShowRowSeparators -and (!$isFirstRow -or ($isFirstRow -and !$context.ShowHeaders)) -and !$isLastRow) {
        $hasVisFooter = ($context.ShowFooters -and $context.HasFooters)
        $isNextLastLine = ($index -eq $context.Rows.Count - 2)
        if (-not ($hasVisFooter -and $isNextLastLine)) {
          $sep = $context.Border.GetColumnRow([TablePart]::RowSeparator, $columnWidths, $context.Columns)
          $result.Add([Segment]::new($sep, $context.BorderStyle))
          $result.Add([Segment]::LineBreak)
        }
      }

      if ($isLastRow -and $context.ShowBorder) {
        $sep = $context.Border.GetColumnRow([TablePart]::Bottom, $columnWidths, $context.Columns)
        if (-not [string]::IsNullOrEmpty($sep)) {
          $result.Add([Segment]::new($sep, $context.BorderStyle))
          $result.Add([Segment]::LineBreak)
        }
      }
    }

    $result.AddRange([TableRenderer]::RenderAnnotation($context, $context.Caption, [TableRenderer]::_defaultCaptionStyle))
    return $result
  }

  static hidden [IEnumerable[Segment]] RenderAnnotation([TableRendererContext]$context, [TableTitle]$header, [Style]$defaultStyle) {
    if ($null -eq $header) { return [Segment[]]@() }

    $styleToUse = if ($null -ne $header.Style) { $header.Style } else { $defaultStyle }
    $p = [Markup]::new($header.Text, $styleToUse)
    $p.Justify = [Justify]::Center
    $p.Overflow = [Overflow]::Ellipsis

    $segs = [List[Segment]]::new()
    $segs.AddRange($p.Render($context.Options, $context.TableWidth))
    $segs.Add([Segment]::LineBreak)
    return $segs
  }
}