Public/Output/Write-Ascii.ps1

function Write-Ascii {
  # .SYNOPSIS
  # Svendsen Tech's PowerShell ASCII art module creates ASCII art characters
  # from a subset of common letters, numbers and punctuation characters.
  # You can add new characters by editing the XML (for developers).

  # MIT license.

  # Copyright (c) 2012-present, Joakim Borger Svendsen, Svendsen Tech.
  # All rights reserved.

  # .DESCRIPTION
  # This script reads characters from an XML file that's expected to have the name
  # "letters.xml", be encoded in UTF-8 and to be in the module's working directory.

  # It was written to be used in conjunction with a modified version of
  # PowerBot (http://poshcode.org/2510), a simple IRC bot framework written
  # using SmartIrc4Net; that's why it can prepend an apostrophe - because somewhere
  # along the way the leading spaces get lost before it hits the IRC channel.

  # Currently the XML only contains lowercase letters, mostly because PowerShell/
  # Windows is case-insensitive by default, which isn't an advantage here.
  # .PARAMETER InputText
  # String(s) to convert to ASCII.
  # .PARAMETER PrependChar
  # Optional. Makes the script prepend an apostrophe.
  # .PARAMETER Compression
  # Optional. Compress to five lines when possible, even when it causes incorrect
  # alignment of the letters g, y, p and q (and "¤").
  # .PARAMETER ForegroundColor
  # Optional. Console only. Changes text foreground color.
  # .PARAMETER BackgroundColor
  # Optional. Console only. Changes text background color.
  [CmdletBinding()]
  param(
    [Parameter(ValueFromPipeline = $True, Mandatory = $True)]
    [Alias('InputText')]
    [String[]]$InputObject,

    [Switch]$PrependChar,

    [Alias('Compression')][Switch]$Compress,

    [ValidateSet("Black", "Blue", "Cyan", "DarkBlue", "DarkCyan", "DarkGray",
      "DarkGreen", "DarkMagenta", "DarkRed", "DarkYellow", "Default", "Gray", "Green",
      "Magenta", "Red", "Rainbow", "White", "Yellow")]
    [String] $ForegroundColor = 'Default',

    [ValidateSet("Black", "Blue", "Cyan", "DarkBlue", "DarkCyan", "DarkGray",
      "DarkGreen", "DarkMagenta", "DarkRed", "DarkYellow", "Default", "Gray", "Green",
      "Magenta", "Red", "Rainbow", "White", "Yellow")]
    [String] $BackgroundColor = 'Default'
    #[int] $MaxChars = '25'
  )

  begin {

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    # Algorithm from hell... This was painful. I hope there's a better way.
    function Get-Ascii {
      param([String] $Text)
      $LetterArray = [Char[]] $Text.ToLower()
      #Write-Host -fore green $LetterArray

      # Find the letter with the most lines.
      $MaxLines = 0
      $LetterArray | ForEach-Object {
        if ($Letters.([String] $_).Lines -gt $MaxLines ) {
          $MaxLines = $Letters.([String] $_).Lines
        }
      }

      # Now this sure was a simple way of making sure all letter align tidily without changing a lot of code!
      if (!$Compress) { $MaxLines = 6 }

      $LetterWidthArray = $LetterArray | ForEach-Object {
        $Letter = [String] $_
        $Letters.$Letter.Width
      }
      $LetterLinesArray = $LetterArray | ForEach-Object {
        $Letter = [String] $_
        $Letters.$Letter.Lines
      }

      #$LetterLinesArray

      $Lines = @{
        '1' = ''
        '2' = ''
        '3' = ''
        '4' = ''
        '5' = ''
        '6' = ''
      }

      #$LineLengths = @(0, 0, 0, 0, 0, 0)

      # Debug
      #Write-Host "MaxLines: $Maxlines"

      $LetterPos = 0
      foreach ($Letter in $LetterArray) {

        # We need to work with strings for indexing the hash by letter
        $Letter = [String] $Letter

        # Each ASCII letter can be from 4 to 6 lines.

        # If the letter has the maximum of 6 lines, populate hash with all lines.
        if ($LetterLinesArray[$LetterPos] -eq 6) {

          #Write-Host "Six letter letter"

          foreach ($Num in 1..6) {

            $LineFragment = [String](($Letters.$Letter.ASCII).Split("`n"))[$Num - 1]

            if ($LineFragment.Length -lt $Letters.$Letter.Width) {
              $LineFragment += ' ' * ($Letters.$Letter.Width - $LineFragment.Length)
            }

            $StringNum = [String] $Num
            $Lines.$StringNum += $LineFragment
          }
        }

        # Add padding for line 1 for letters with 5 lines and populate lines 2-6.
        ## Changed to top-adjust 5-line letters if there are 6 total.
        ## Added XML properties for letter alignment. Most are "default", which is top-aligned.
        ## Also added script logic to handle it (2012-12-29): <fixation>bottom</fixation>
        elseif ($LetterLinesArray[$LetterPos] -eq 5) {

          if ($MaxLines -lt 6 -or $Letters.$Letter.fixation -eq 'bottom') {

            $Padding = ' ' * $LetterWidthArray[$LetterPos]
            $Lines.'1' += $Padding

            foreach ($Num in 2..6) {

              $LineFragment = [String](($Letters.$Letter.ASCII).Split("`n"))[$Num - 2]

              if ($LineFragment.Length -lt $Letters.$Letter.Width) {
                $LineFragment += ' ' * ($Letters.$Letter.Width - $LineFragment.Length)
              }
              $StringNum = [String] $Num
              $Lines.$StringNum += $LineFragment
            }
          } else {
            $Padding = ' ' * $LetterWidthArray[$LetterPos]
            $Lines.'6' += $Padding
            foreach ($Num in 1..5) {
              $StringNum = [String] $Num
              $LineFragment = [String](($Letters.$Letter.ASCII).Split("`n"))[$Num - 1]
              if ($LineFragment.Length -lt $Letters.$Letter.Width) {
                $LineFragment += ' ' * ($Letters.$Letter.Width - $LineFragment.Length)
              }
              $Lines.$StringNum += $LineFragment
            }
          }
        } else {
          # Here we deal with letters with four lines.
          # Dynamic algorithm that places four-line letters on the bottom line if there are
          # 4 or 5 lines only in the letter with the most lines.
          # Default to putting the 4-liners at line 3-6
          $StartRange, $EndRange, $IndexSubtract = 3, 6, 3
          $Padding = ' ' * $LetterWidthArray[$LetterPos]

          # If there are 4 or 5 lines...
          if ($MaxLines -lt 6) {
            $Lines.'2' += $Padding
          } else {
            # There are 6 lines maximum, put 4-line letters in the middle.
            $Lines.'1' += $Padding
            $Lines.'6' += $Padding
            $StartRange, $EndRange, $IndexSubtract = 2, 5, 2
          }

          # There will always be at least four lines. Populate lines 2-5 or 3-6 in the hash.
          foreach ($Num in $StartRange..$EndRange) {

            $StringNum = [String] $Num

            $LineFragment = [String](($Letters.$Letter.ASCII).Split("`n"))[$Num - $IndexSubtract]

            if ($LineFragment.Length -lt $Letters.$Letter.Width) {
              $LineFragment += ' ' * ($Letters.$Letter.Width - $LineFragment.Length)
            }
            $Lines.$StringNum += $LineFragment
          }
        }
        $LetterPos++
      }

      # Return stuff
      $Lines.GetEnumerator() |
        Sort-Object -Property Name |
        Select-Object -ExpandProperty Value |
        Where-Object {
          $_ -match '\S'
        } | ForEach-Object {
          if ($PrependChar) {
            "'" + $_
          } else {
            $_
          }
        }
    }

    # Populate the $Letters hashtable with character data from the XML.
    Function Get-LetterXML {

      $LetterFile = Join-Path $PSScriptRoot 'letters.xml'
      $Xml = [xml] (Get-Content $LetterFile)

      $Xml.Chars.Char | ForEach-Object {

        $Letters.($_.Name) = New-Object PSObject -Property @{

          'Fixation' = $_.fixation
          'Lines'    = $_.lines
          'ASCII'    = $_.data
          'Width'    = $_.width
        }
      }
    }

    function Write-RainbowString {

      param([String] $Line,
        [String] $ForegroundColor = '',
        [String] $BackgroundColor = '')

      $Colors = @('Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 'DarkYellow',
        'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'White')


      # $Colors[(Get-Random -Min 0 -Max 16)]

      [Char[]] $Line | ForEach-Object {

        if ($ForegroundColor -and $ForegroundColor -ieq 'rainbow') {

          if ($BackgroundColor -and $BackgroundColor -ieq 'rainbow') {
            Write-Host -ForegroundColor $Colors[(
              Get-Random -Min 0 -Max 16
            )] -BackgroundColor $Colors[(
              Get-Random -Min 0 -Max 16
            )] -NoNewline $_
          } elseif ($BackgroundColor) {
            Write-Host -ForegroundColor $Colors[(
              Get-Random -Min 0 -Max 16
            )] -BackgroundColor $BackgroundColor `
              -NoNewline $_
          } else {
            Write-Host -ForegroundColor $Colors[(
              Get-Random -Min 0 -Max 16
            )] -NoNewline $_
          }
        } else {
          # One of them has to be a rainbow, so we know the background is a rainbow here...
          if ($ForegroundColor) {
            Write-Host -ForegroundColor $ForegroundColor -BackgroundColor $Colors[(
              Get-Random -Min 0 -Max 16
            )] -NoNewline $_
          } else {
            Write-Host -BackgroundColor $Colors[(Get-Random -Min 0 -Max 16)] -NoNewline $_
          }
        }
      }
      Write-Host ''
    }

    # Get ASCII art letters/characters and data from XML. Make it persistent for the module.
    if (!(Get-Variable -EA SilentlyContinue -Scope Script -Name Letters)) {
      $script:Letters = @{}
      Get-LetterXML
    }

    # Turn the [string[]] into a [String] the only way I could figure out how... wtf
    #$Text = ''
    #$InputObject | ForEach-Object { $Text += "$_ " }

    # Limit to 30 characters
    #$MaxChars = 30
    #if ($Text.Length -gt $MaxChars) { "Too long text. There's a maximum of $MaxChars characters."; return }

    # Replace spaces with underscores (that's what's used for spaces in the XML).
    #$Text = $Text -replace ' ', '_'

    # Define accepted characters (which are found in XML).
    #$AcceptedChars = '[^a-z0-9 _,!?./;:<>()¤{}\[\]\|\^=\$\-''+`\\"æøåâàáéèêóòôü]' # Some chars only works when sent as UTF-8 on IRC
    $LetterArray = [string[]]($Letters.GetEnumerator() | Sort-Object Name | Select-Object -ExpandProperty Name)
    $AcceptedChars = [regex] ( '(?i)[^' + ([regex]::Escape(($LetterArray -join '')) -replace '-', '\-' -replace '\]', '\]') + ' ]' )
    # Debug
    #Write-Host -fore cyan $AcceptedChars.ToString()
  }

  process {
    if ($InputObject -match $AcceptedChars) {
      "Unsupported character, using these accepted characters: " + ($LetterArray -replace '^template$' -join ', ') + "."
      return
    }

    # Filthy workaround (now worked around in the foreach creating the string).
    #if ($Text.Length -eq 1) { $Text += '_' }

    $Lines = @()

    foreach ($Text in $InputObject) {

      $ASCII = Get-Ascii ($Text -replace ' ', '_')

      if ($ForegroundColor -ne 'Default' -and $BackgroundColor -ne 'Default') {
        if ($ForegroundColor -ieq 'rainbow' -or $BackGroundColor -ieq 'rainbow') {
          $ASCII | ForEach-Object {
            Write-RainbowString -ForegroundColor $ForegroundColor -BackgroundColor $BackgroundColor -Line $_
          }
        } else {
          Write-Host -ForegroundColor $ForegroundColor -BackgroundColor $BackgroundColor ($ASCII -join "`n")
        }
      } elseif ($ForegroundColor -ne 'Default') {
        if ($ForegroundColor -ieq 'rainbow') {
          $ASCII | ForEach-Object {
            Write-RainbowString -ForegroundColor $ForegroundColor -Line $_
          }
        } else {
          Write-Host -ForegroundColor $ForegroundColor ($ASCII -join "`n")
        }
      } elseif ($BackgroundColor -ne 'Default') {
        if ($BackgroundColor -ieq 'rainbow') {
          $ASCII | ForEach-Object {
            Write-RainbowString -BackgroundColor $BackgroundColor -Line $_
          }
        } else {
          Write-Host -BackgroundColor $BackgroundColor ($ASCII -join "`n")
        }
      } else { $ASCII -replace '\s+$' }
    }
  }
}