Elizium.Krayola.psm1

Set-StrictMode -Version 1.0
function Get-EnvironmentVariable
{
  <#
    .NAME
      Get-EnvironmentVariable
 
    .Synopsis
      Wrapper around [System.Environment]::GetEnvironmentVariable to support
    unit testing.
 
    .DESCRIPTION
      Retrieve the value of the environment variable specified. Returns
    $null if variable is not found.
 
    .EXAMPLE
      Get-EnvironmentVariable 'KRAYOLA-THEME-NAME'
  #>

    [CmdletBinding()]
    [OutputType([string])]
    Param
    (
      [Parameter(Mandatory = $true)]
      [string]$Variable
    )

  return [System.Environment]::GetEnvironmentVariable($Variable);
}

function Get-IsKrayolaLightTerminal {
  <#
    .NAME
      Get-IsKrayolaLightTerminal
 
    .SYNOPSIS
      Gets the value of KRAYOLA-LIGHT-TERMINAL as a boolean
 
    .DESCRIPTION
      For use by applications that need to use a Krayola theme that is dependent
    on whether a light or dark background colour is in effect in the current
    terminal.
  #>

  [OutputType([boolean])]
  param()

  return -not([string]::IsNullOrWhiteSpace(
    (Get-EnvironmentVariable 'KRAYOLA-LIGHT-TERMINAL')));
}

function Get-KrayolaTheme {
  <#
    .NAME
      Get-KrayolaTheme
 
    .SYNOPSIS
      Helper function that makes it easier for client applications to get a Krayola theme
    from the environment, which is compatible with the terminal colours being used.
    This helps keep output from different applications consistent.
 
    .DESCRIPTION
      If $KrayolaThemeName is specified, then it is used to lookup the theme in the global
    $KrayolaThemes hash-table exposed by the Krayola module. If either the theme specified
    does not exist or not specified, then a default theme is used. The default theme created
    should be compatible with the dark/lightness of the background of the terminal currently
    in use. By default, a dark terminal is assumed and the colours used show up clearly
    against a dark background. If KRAYOLA-LIGHT-TERMINAL is defined as an environment
    variable (can be set to any string apart from empty string/white space), then the colours
    chosen show up best against a light background.
  #>

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseBOMForUnicodeEncodedFile', '')]
  [OutputType([System.Collections.Hashtable])]
  param (
    [Parameter(
      Mandatory = $false,
      Position = 0
    )]
    [AllowEmptyString()]
    [string]$KrayolaThemeName,

    [Parameter(Mandatory = $false)]
    [System.Collections.Hashtable]$Themes = $KrayolaThemes,

    [Parameter(Mandatory = $false)]
    [System.Collections.Hashtable]$DefaultTheme = @{
      # DefaultTheme is compatible with dark consoles by default
      #
      'FORMAT'             = '"<%KEY%>" => "<%VALUE%>"';
      'KEY-PLACE-HOLDER'   = '<%KEY%>';
      'VALUE-PLACE-HOLDER' = '<%VALUE%>';
      'KEY-COLOURS'        = @('DarkCyan');
      'VALUE-COLOURS'      = @('White');
      "AFFIRM-COLOURS"     = @("Red");
      'OPEN'               = '[';
      'CLOSE'              = ']';
      'SEPARATOR'          = ', ';
      'META-COLOURS'       = @('Yellow');
      'MESSAGE-COLOURS'    = @('Cyan');
      'MESSAGE-SUFFIX'     = ' // ';
    }
  )
  [System.Collections.Hashtable]$displayTheme = $DefaultTheme;

  # Switch to use colours compatible with light consoles if KRAYOLA-LIGHT-TERMINAL
  # is set.
  #
  if (Get-IsKrayolaLightTerminal) {
    $displayTheme['KEY-COLOURS'] = @('DarkBlue');
    $displayTheme['VALUE-COLOURS'] = @('Red');
    $displayTheme['AFFIRM-COLOURS'] = @('Magenta');
    $displayTheme['META-COLOURS'] = @('DarkMagenta');
    $displayTheme['MESSAGE-COLOURS'] = @('Green');
  }

  [string]$themeName = $KrayolaThemeName;

  # Get the theme name
  #
  if ([string]::IsNullOrWhiteSpace($themeName)) {
    $themeName = Get-EnvironmentVariable 'KRAYOLA-THEME-NAME';
  }

  if ($Themes -and $Themes.ContainsKey($themeName)) {
    $displayTheme = $Themes[$themeName];
  }

  return $displayTheme;
}

function Show-ConsoleColours {

  [Alias('Show-ConsoleColors')]
  param ()
  <#
    .NAME
      Show-ConsoleColours
 
    .SYNOPSIS
      Helper function that shows all the available console colours in the colour
      they represent. This willl assist in the development of colour Themes.
  #>


  [Array]$colours = @('Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', `
      'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'White');

  foreach ($col in $colours) {
    Write-Host -ForegroundColor $col $col;
  }
}

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseBOMForUnicodeEncodedFile', '')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
# Param()

$Global:KrayolaThemes = @{

  'EMERGENCY-THEME'  = @{
    'FORMAT'             = '{{<%KEY%>}}={{<%VALUE%>}}';
    'KEY-PLACE-HOLDER'   = '<%KEY%>';
    'VALUE-PLACE-HOLDER' = '<%VALUE%>';
    'KEY-COLOURS'        = @('White');
    'VALUE-COLOURS'      = @('DarkGray');
    "AFFIRM-COLOURS"     = @("Yellow");
    'OPEN'               = '{';
    'CLOSE'              = '}';
    'SEPARATOR'          = '; ';
    'META-COLOURS'       = @('Black');
    'MESSAGE-COLOURS'    = @('Gray');
    'MESSAGE-SUFFIX'     = ' ֎ '
  };

  'ROUND-THEME' = @{
    'FORMAT'             = '"<%KEY%>"="<%VALUE%>"';
    'KEY-PLACE-HOLDER'   = '<%KEY%>';
    'VALUE-PLACE-HOLDER' = '<%VALUE%>';
    'KEY-COLOURS'        = @('DarkCyan');
    'VALUE-COLOURS'      = @('DarkBlue');
    "AFFIRM-COLOURS"     = @("Red");
    'OPEN'               = '••• (';
    'CLOSE'              = ') •••';
    'SEPARATOR'          = ' @@ ';
    'META-COLOURS'       = @('Yellow');
    'MESSAGE-COLOURS'    = @('Cyan');
    'MESSAGE-SUFFIX'     = ' ~~ '
  };

  'SQUARE-THEME' = @{
    'FORMAT'             = '"<%KEY%>"="<%VALUE%>"';
    'KEY-PLACE-HOLDER'   = '<%KEY%>';
    'VALUE-PLACE-HOLDER' = '<%VALUE%>';
    'KEY-COLOURS'        = @('DarkCyan');
    'VALUE-COLOURS'      = @('DarkBlue');
    "AFFIRM-COLOURS"     = @("Blue");
    'OPEN'               = '■■■ [';
    'CLOSE'              = '] ■■■';
    'SEPARATOR'          = ' ## ';
    'META-COLOURS'       = @('Black');
    'MESSAGE-COLOURS'    = @('DarkGreen');
    'MESSAGE-SUFFIX'     = ' == '
  };

  'ANGULAR-THEME' = @{
    'FORMAT'             = '"<%KEY%>"-->"<%VALUE%>"';
    'KEY-PLACE-HOLDER'   = '<%KEY%>';
    'VALUE-PLACE-HOLDER' = '<%VALUE%>';
    'KEY-COLOURS'        = @('DarkCyan');
    'VALUE-COLOURS'      = @('DarkBlue');
    "AFFIRM-COLOURS"     = @("Blue");
    'OPEN'               = '◄◄◄ <';
    'CLOSE'              = '> ►►►';
    'SEPARATOR'          = ' ^^ ';
    'META-COLOURS'       = @('Black');
    'MESSAGE-COLOURS'    = @('DarkGreen');
    'MESSAGE-SUFFIX'     = ' // '
  }
}

$null = $KrayolaThemes;

function Write-InColour {
  <#
    .NAME
      Write-InColour
 
    .SYNOPSIS
      Writes a multiple snippets of a line in colour with the provided text, foreground & background
      colours.
 
    .DESCRIPTION
      The user passes in an array of 1,2 or 3 element arrays, which contains any number of text fragments
      with an optional colour specification (ConsoleColor enumeration). The function will then write a
      multi coloured text line to the console.
 
      Element 0: text
      Element 1: foreground colour
      Element 2: background colour
 
      If the background colour is required, then the foreground colour must also be specified.
 
      Write-InColour -colouredTextLine @( ("some text", "Blue"), ("some more text", "Red", "White") )
      Write-InColour -colouredTextLine @( ("some text", "Blue"), ("some more text", "Red") )
      Write-InColour -colouredTextLine @( ("some text", "Blue"), ("some more text") )
 
      If you only need to write a single element, use an extra , preceding the array eg:
 
      Write-InColour -colouredTextLine @( ,@("some text", "Blue") )
 
      Empty snippets, should not be passed in, it's up to the caller to ensure that this is
      the case. If an empty snippet is found an ugly warning message is emitted, so this
      should not go un-noticed.
 
    .PARAMETER TextSnippets
      An array of an array of strings (see description).
 
    .PARAMETER NoNewLine
      Switch to indicate if a new line should be written after the text.
  #>


  # This function is supposed to write to the host, because the output is in colour.
  # Using Write-Host is Krayola's raison d'etre!
  #
  [Alias('Write-InColor')]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')]
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string[][]]
    $TextSnippets,

    [Switch]$NoNewLine
  )

  foreach ($snippet in $TextSnippets) {
    if ($snippet.Length -eq 0) {
      Write-Warning ' * Found malformed line (empty snippet entry), skipping * ';
      continue;
    }

    if ($snippet.Length -eq 1) {
      Write-Warning " * No colour specified for snippet '$($snippet[0])', skipping * ";
      continue;
    }

    if ($null -eq $snippet[0]) {
      Write-Warning ' * Found empty snippet text, skipping *';
      continue;
    }

    if ($snippet.Length -eq 2) {
      if ($null -eq $snippet[1]) {
        Write-Warning " * Foreground col is null, for snippet: '$($snippet[0])', skipping * ";
        continue;
      }

      # Foreground colour specified
      #
      Write-Host $snippet[0] -NoNewline -ForegroundColor $snippet[1];
    }
    else {
      # Foreground and background colours specified
      #
      Write-Host $snippet[0] -NoNewline -ForegroundColor $snippet[1] -BackgroundColor $snippet[2];

      if ($snippet.Length -gt 3) {
        Write-Warning " * Excess entries found for snippet: '$($snippet[0])' * ";
      }
    }
  }

  if (-not ($NoNewLine.ToBool())) {
    Write-Host '';
  }
}

function Write-RawPairsInColour {
  <#
    .NAME
      Write-RawPairsInColour
 
    .SYNOPSIS
 
    .DESCRIPTION
      The snippets passed in as element of $Pairs are in the same format as
      those passed into Write-InColour as TextSnippets. The only difference is that
      each snippet can only have 2 entries, the first being the key and the second being
      the value.
 
    .PARAMETER Pairs
      A 3 dimensional array representing a sequence of key/value pairs where each key and value
      are in themselves a sub-sequence of 2 or 3 items representing text, foreground colour &
      background colours.
 
      Eg:
 
      $PairsToWriteInColour = @(
        @(@("Sport", "Red"), @("Tennis", "Blue", "Yellow")),
        @(@("Star", "Green"), @("Martina Hingis", "Cyan"))
      );
 
    .PARAMETER Format
      A string containing a placeholder for the Key and the Value. It represents how the whole
      key/value pair should be represented. It must contain the KEY-PLACE-HOLDER and VALUE-PLACE-HOLDER strings
 
    .PARAMETER KeyPlaceHolder
      The place holder that identifies the Key in the FORMAT string
 
    .PARAMETER ValuePlaceHolder
      The place holder that identifies the Value in the FORMAT string.
 
    .PARAMETER KeyColours
      Array of 1 or 2 items only, the first is the foreground colour and the optional second
      value is the background colour, that specifies how Keys are displayed.
 
    .PARAMETER ValueColours
      The same as KEY-COLOURS but it applies to Values.
 
    .PARAMETER Open
      Specifies the leading wrapper around the whole key/value pair collection, typically '('.
 
    .PARAMETER Close
      Specifies the tail wrapper around the whole key/value pair collection typically ')'.
 
    .PARAMETER Separator
      Specifies a sequence of characters that separates the Key/Vale pairs, typically ','.
 
    .PARAMETER MetaColours
      This is the colour specifier for any character that is not the key or the value. Eg: if
      the format is defined as ['<%KEY%>'='<%VALUE%>'], then [' '=' '] will be written in
      this colour. As with other write in clour functionality, the user can specify just a
      single colour (in a single item array), which would represent the foreground colour, or
      2 colours can be specified, representing the foreground and background colours in
      that order inside the 2 element array (a pair). These meta colours will also apply
      to the Open, Close and Separator tokens.
 
    .PARAMETER MessageColours
      An optional message that appears preceding the Key/Value pair collection and this array
      describes the colours used to write that message.
 
    .PARAMETER MessageSuffix
      Specifies a sequence of characters that separates the MESSAGE (if present) from the
      Key/Value pair collection.
  #>


  # This function is supposed to write to the host, because the output is in colour.
  # Using Write-Host is Krayola's raison d'etre!
  #
  [Alias('Write-RawPairsInColor')]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')]
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [AllowEmptyCollection()]
    [string[][][]]
    $Pairs,

    [Parameter(Mandatory = $false)]
    [string]
    $Format = '("<%KEY%>"="<%VALUE%>")',

    [Parameter(Mandatory = $false)]
    [string]
    $KeyPlaceHolder = '<%KEY%>',

    [Parameter(Mandatory = $false)]
    [string]
    $ValuePlaceHolder = '<%VALUE%>',

    [Parameter(Mandatory = $false)]
    [AllowEmptyString()]
    $Open = '=== [',

    [AllowEmptyString()]
    [Parameter(Mandatory = $false)]
    $Close = '] ===',

    [AllowEmptyString()]
    [Parameter(Mandatory = $false)]
    $Separator = ', ',

    [Parameter(Mandatory = $false)]
    [string[]]
    $MetaColours = @('White'),

    [Parameter(Mandatory = $false)]
    [string]
    $Message,

    [Parameter(Mandatory = $false)]
    [string[]]
    $MessageColours = @('White'),

    [AllowEmptyString()]
    [Parameter(Mandatory = $false)]
    [string]
    $MessageSuffix = ' // ',

    [Switch]$NoNewLine
  )

  if ($Pairs.Length -eq 0) {
    return;
  }

  if (($MetaColours.Length -lt 1) -or ($MetaColours.Length -gt 2)) {
    Write-Error -Message "Bad meta colours spec, aborting (No of colours specified: $($MetaColours.Length))";
  }

  # Write the leading message
  #
  if (-not([String]::IsNullOrEmpty($Message))) {
    [string[]]$messageSnippet = @($Message) + $MessageColours;
    [string[][]]$wrapper = @(, $messageSnippet);
    Write-InColour -TextSnippets $wrapper -NoNewLine;

    if (-not([String]::IsNullOrEmpty($MessageSuffix))) {
      [string[]]$suffixSnippet = @($MessageSuffix) + $MessageColours;
      [string[][]]$wrapper = @(, $suffixSnippet);
      Write-InColour -TextSnippets $wrapper -NoNewLine;
    }
  }

  # Add the Open snippet
  #
  if (-not([String]::IsNullOrEmpty($Open))) {
    [string[]]$openSnippet = @($Open) + $MetaColours;
    [string[][]]$wrapper = @(, $openSnippet);
    Write-InColour -TextSnippets $wrapper -NoNewLine;
  }

  [int]$fieldCounter = 0;
  foreach ($field in $Pairs) {
    [string[][]]$displayField = @();

    # Each element of a pair is an instance of a snippet that is compatible with Write-InColour
    # which we can defer to. We need to create the 5 snippets that represents the field pair.
    #
    if ($field.Length -ge 2) {
      # Get the key and value
      #
      [string[]]$keySnippet = $field[0];
      $keyText, $keyColours = $keySnippet;

      [string[]]$valueSnippet = $field[1];
      $valueText, $valueColours = $valueSnippet;

      [string[]]$constituents = Split-KeyValuePairFormatter -Format $Format `
        -KeyConstituent $keyText -ValueConstituent $valueText `
        -KeyPlaceHolder $KeyPlaceHolder -ValuePlaceHolder $ValuePlaceHolder;

      [string[]]$constituentSnippet = @();
      # Now create the 5 snippets (header, key/value, mid, value/key, tail)
      #

      # header
      #
      $constituentSnippet = @($constituents[0]) + $MetaColours;
      $displayField += , $constituentSnippet;

      # key
      #
      $constituentSnippet = @($constituents[1]) + $keyColours;
      $displayField += , $constituentSnippet;

      # mid
      #
      $constituentSnippet = @($constituents[2]) + $MetaColours;
      $displayField += , $constituentSnippet;

      # value
      #
      $constituentSnippet = @($constituents[3]) + $valueColours;
      $displayField += , $constituentSnippet;

      # tail
      #
      $constituentSnippet = @($constituents[4]) + $MetaColours;
      $displayField += , $constituentSnippet;

      Write-InColour -TextSnippets $displayField -NoNewLine;

      if ($field.Length -gt 2) {
        Write-Warning ' * Ignoring excess snippets *';
      }
    }
    else {
      Write-Warning ' * Insufficient snippet pair, 2 required, skipping *';
    }

    # Field Separator snippet
    #
    if (($fieldCounter -lt ($Pairs.Length - 1)) -and (-not([String]::IsNullOrEmpty($Separator)))) {
      [string[]]$separatorSnippet = @($Separator) + $MetaColours;
      Write-InColour -TextSnippets @(, $separatorSnippet) -NoNewLine;
    }

    $fieldCounter++;
  }

  # Add the Close snippet
  #
  if (-not([String]::IsNullOrEmpty($Close))) {
    [string[]]$closeSnippet = @($Close) + $MetaColours;
    [string[][]]$wrapper = @(, $closeSnippet);
    Write-InColour -TextSnippets $wrapper -NoNewLine;
  }

  if (-not($NoNewLine.ToBool())) {
    Write-Host '';
  }
}

function Write-ThemedPairsInColour {
  <#
    .NAME
      Write-ThemedPairsInColour
 
    .SYNOPSIS
      Writes a collection of key/value pairs in colour according to a specified Theme.
 
    .DESCRIPTION
      The Pairs defined here are colour-less, instead colours coming from the KEY-COLOURS
      and VALUE-COLOURS in the theme. The implications of this are firstly, the Pairs are
      simpler to specify. However, the colour representation is more restricted, because
      all Keys displayed must be of the same colour and the same goes for the values.
 
      When using Write-RawPairsInColour directly, the user has to specify each element of a
      pair with 2 or 3 items; text, foreground colour & background colour, eg:
 
        @(@("Sport", "Red"), @("Tennis", "Blue", "Yellow"))
 
      Now, each pair is specified as a simply a pair of strings:
        @("Sport", "Tennis")
 
      The purpose of this function is to generate a single call to Write-RawPairsInColour in
      the form:
 
      $PairsToWriteInColour = @(
        @(@("Sport", "Red"), @("Tennis", "Blue", "Yellow")),
        @(@("Star", "Green"), @("Martina Hingis", "Cyan"))
      );
      Write-RawPairsInColour -Message ">>> Greetings" -MessageColours @("Magenta") `
        -Pairs $PairsToWriteInColour -Format "'<%KEY%>'<--->'<%VALUE%>'" `
        -MetaColours @(,"Blue") -Open " ••• <<" -Close ">> •••"
 
      A value can be highlighted by specifying a boolean affirmation value after the
      key/value pair. So the 'value' of a pair, eg 'Tennis' of @("Sport", "Tennis") can
      be highlighted by the addition of a boolean value: @("Sport", "Tennis", $true),
      will result in 'Tennis' being highlighted; written with a different colour
      value. This colour value is taken from the 'AFFIRM-COLOURS' entry in the theme. If
      the affirmation value is false, eg @("Sport", "Tennis", $false), then the value
      'Tennis' will be written as per-normal using the 'VALUE-COLOURS' entry.
 
      You can create your own theme, using this template for assistance:
 
      $YourTheme = @{
        "FORMAT" = "'<%KEY%>' = '<%VALUE%>'";
        "KEY-PLACE-HOLDER" = "<%KEY%>";
        "VALUE-PLACE-HOLDER" = "<%VALUE%>";
        "KEY-COLOURS" = @("Red");
        "VALUE-COLOURS" = @("Magenta");
        "AFFIRM-COLOURS" = @("White");
        "OPEN" = "(";
        "CLOSE" = ")";
        "SEPARATOR" = ", ";
        "META-COLOURS" = @("Blue");
        "MESSAGE-COLOURS" = @("Green");
        "MESSAGE-SUFFIX" = " // "
      }
 
    .PARAMETER Pairs
      A 2 dimensional array representing the key/value pairs to be rendered.
 
    .PARAMETER Theme
      Hash-table that must contain all the following fields
      FORMAT
      KEY-PLACE-HOLDER
      VALUE-PLACE-HOLDER
      KEY-COLOURS
      VALUE-COLOURS
      OPEN
      CLOSE
      SEPARATOR
      META-COLOURS
      MESSAGE-COLOURS
      MESSAGE-SUFFIX
 
    .PARAMETER Message
      An optional message that precedes the display of the Key/Value sequence.
  #>

  [Alias('Write-ThemedPairsInColor')]
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseBOMForUnicodeEncodedFile', '')]
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [AllowEmptyCollection()]
    [string[][]]
    $Pairs,

    [Parameter(Mandatory = $true)]
    [System.Collections.Hashtable]
    $Theme,

    [Parameter(Mandatory = $false)]
    [string]
    $Message
  )

  if (0 -eq $Pairs.Length) {
    return;
  }

  [boolean]$inEmergency = $false;

  function isThemeValid {
    param(
      [System.Collections.Hashtable]$themeToValidate
    )

    [int]$minimumNoOfThemeEntries = 11;
    return ($themeToValidate -and ($themeToValidate.Count -ge $minimumNoOfThemeEntries))
  }

  if (-not(isThemeValid($Theme))) {
    $Theme = $KrayolaThemes['EMERGENCY-THEME'];

    # In case the user has compromised the EMERGENCY theme, which should be modify-able (because we
    # can't be sure that the emergency theme we have defined is suitable for their console),
    # we'll use this internal emergency theme ...
    #
    if (-not(isThemeValid($Theme))) {
      $Theme = @{
        'FORMAT'             = '{{<%KEY%>}}={{<%VALUE%>}}';
        'KEY-PLACE-HOLDER'   = '<%KEY%>';
        'VALUE-PLACE-HOLDER' = '<%VALUE%>';
        'KEY-COLOURS'        = @('White');
        'VALUE-COLOURS'      = @('DarkGray');
        'OPEN'               = '{';
        'CLOSE'              = '}';
        'SEPARATOR'          = '; ';
        'META-COLOURS'       = @('Black');
        'MESSAGE-COLOURS'    = @('Gray');
        'MESSAGE-SUFFIX'     = ' ֎ '
      }
    }

    $inEmergency = $true;
  }

  [string[][][]] $pairsToWriteInColour = @();

  # Construct the pairs
  #
  [string[]]$keyColours = $Theme['KEY-COLOURS'];
  [string[]]$valueColours = $Theme['VALUE-COLOURS'];

  foreach ($pair in $Pairs) {
    if (1 -ge $pair.Length) {
      [string[]]$transformedKey = @('!INVALID!') + $keyColours;
      [string[]]$transformedValue = @('---') + $valueColours;

      Write-Error "Found pair that does not contain 2 items (pair: $($pair)) [!!! Reminder: you need to use the comma op for a single item array]";
    } else {
      [string[]]$transformedKey = @($pair[0]) + $keyColours;
      [string[]]$transformedValue = @($pair[1]) + $valueColours;

      # Apply affirmation
      #
      if ((3 -eq $pair.Length)) {
        if (($pair[2] -ieq 'true')) {
          if ($Theme.ContainsKey('AFFIRM-COLOURS')) {
            $transformedValue = @($pair[1]) + $Theme['AFFIRM-COLOURS'];
          }
          else {
            # Since the affirmation colour is missing, use another way of highlighting the value
            # ie, surround in asterisks
            #
            $transformedValue = @("*{0}*" -f $pair[1]) + $valueColours;
          }
        }
        elseif (-not($pair[2] -ieq 'false')) {
          Write-Error "Invalid affirm value found; not boolean value, found: $($pair[2]) [!!! Reminder: you need to use the comma op for a single item array]"
        } 
      } elseif (3 -lt $pair.Length) {
        Write-Error "Found pair with excess items (pair: $($pair)) [!!! Reminder: you need to use the comma op for a single item array]"
      }
    }

    $transformedPair = , @($transformedKey, $transformedValue);
    $pairsToWriteInColour += $transformedPair;  
  }

  [System.Collections.Hashtable]$parameters = @{
    'Pairs'            = $pairsToWriteInColour;
    'Format'           = $Theme['FORMAT'];
    'KeyPlaceHolder'   = $Theme['KEY-PLACE-HOLDER'];
    'ValuePlaceHolder' = $Theme['VALUE-PLACE-HOLDER'];
    'Open'             = $Theme['OPEN'];
    'Close'            = $Theme['CLOSE'];
    'Separator'        = $Theme['SEPARATOR'];
    'MetaColours'      = $Theme['META-COLOURS'];
  }

  if ([String]::IsNullOrEmpty($Message)) {
    if ($inEmergency) {
      $Message = 'ϞϞϞ ';
    }
  }
  else {
    if ($inEmergency) {
      $Message = 'ϞϞϞ ' + $Message;
    }
  }

  if (-not([String]::IsNullOrEmpty($Message))) {
    $parameters['Message'] = $Message;
    $parameters['MessageColours'] = $Theme['MESSAGE-COLOURS'];
    $parameters['MessageSuffix'] = $Theme['MESSAGE-SUFFIX'];
  }

  & 'Write-RawPairsInColour' @parameters;
}

function Split-KeyValuePairFormatter {
  <#
    .NAME
      Split-KeyValuePairFormatter
 
    .SYNOPSIS
      Splits an input string which should conform to the format string containing
      <%KEY%> and <%VALUE%> constituents.
 
    .DESCRIPTION
      The format string should contain key and value token place holders. This function,
      will split the input returning an array of 5 strings representing the constituents.
 
    .PARAMETER Format
      Format specifier for each key/value pair encountered. The string must contain the tokens
      whatever is defined in KeyPlaceHolder and ValuePlaceHolder.
 
    .PARAMETER KeyConstituent
      The value of the Key.
 
    .PARAMETER ValueConstituent
      The value of the Value!
 
    .PARAMETER KeyPlaceHolder
      The place holder that identifies the Key in the Format parameter.
 
    .PARAMETER ValuePlaceHolder
      The place holder that identifies the Value in the Format parameter.
  #>

  [OutputType([string[]])]
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]
    $Format,

    [string]
    $KeyConstituent,

    [string]
    $ValueConstituent,

    [string]
    $KeyPlaceHolder = '<%KEY%>',

    [string]
    $ValuePlaceHolder = '<%VALUE%>'
  )

  [string[]]$constituents = @();

  [int]$keyPosition = $Format.IndexOf($KeyPlaceHolder);
  [int]$valuePosition = $Format.IndexOf($ValuePlaceHolder);

  if ($keyPosition -eq -1) {
    Write-Error -Message "Invalid formatter: '$($Format)', key: '$({$KeyPlaceHolder})' not found";
  }

  if ($valuePosition -eq -1) {
    Write-Error -Message "Invalid formatter: '$($Format)', value: '$({$ValuePlaceHolder})' not found";
  }

  # Need this check just in case the user wants Value=Key!!!, or perhaps something
  # exotic like [Value(Key)], ie; it's bad to make the assumption that the key comes
  # before the value in the format sring.
  #
  if ($keyPosition -lt $valuePosition) {
    [string]$header = '';

    if ($keyPosition -ne 0) {
      # Insert everything up to the KeyFormat (the header)
      #
      $header = $Format.Substring(0, $keyPosition);
    }

    $constituents += $header;

    # Insert the KeyFormat
    #
    $constituents += $KeyConstituent;

    # Insert everything in between the key and value formats, typically the
    # equal sign (key=value), but it could be anything eg --> (key-->value)
    #
    [int]$midStart = $header.Length + $KeyPlaceHolder.Length;
    [int]$midLength = $valuePosition - $midStart;
    if ($midLength -lt 0) {
      Write-Error -Message "Internal error, couldn't get the middle of the formatter: '$Format'";
    }
    [string]$middle = $Format.Substring($midStart, $midLength);
    $constituents += $middle;

    # Insert the value
    #
    $constituents += $ValueConstituent;

    # Insert everything after the ValueFormat (the tail)
    # 0 1 2
    # 012345678901234567890
    # [<%KEY%>=<%VALUE%>]
    #
    [int]$tailStart = $valuePosition + $ValuePlaceHolder.Length; # 9 + 9
    [int]$tailEnd = $Format.Length - $tailStart; # 19 -18
    [string]$tail = $Format.Substring($tailStart, $tailEnd);

    $constituents += $tail;
  }
  else {
    [string]$header = '';

    if ($valuePosition -ne 0) {
      # Insert everything up to the ValueFormat (the header)
      #
      $header = $Format.Substring(0, $valuePosition);
    }

    $constituents += $header;

    # Insert the ValueFormat
    #
    $constituents += $ValueConstituent;

    # Insert everything in between the value and key formats, typically the
    # equal sign (value=key), but it could be anything eg --> (value-->key)
    #
    [int]$midStart = $header.Length + $ValuePlaceHolder.Length;
    [int]$midLength = $keyPosition - $midStart;
    if ($midLength -lt 0) {
      Write-Error -Message "Internal error, couldnt get the middle of the formatter: '$Format'";
    }
    [string]$middle = $Format.Substring($midStart, $midLength);
    $constituents += $middle;

    # Insert the key
    #
    $constituents += $KeyConstituent;

    # Insert everything after the KeyFormat (the tail)
    #
    [int]$tailStart = $keyPosition + $KeyPlaceHolder.Length;
    [int]$tailEnd = $Format.Length - $tailStart;
    [string]$tail = $Format.Substring($tailStart, $tailEnd);

    $constituents += $tail;
  }

  return [string[]]$constituents;
}
Export-ModuleMember -Variable KrayolaThemes

Export-ModuleMember -Function Get-EnvironmentVariable, Get-IsKrayolaLightTerminal, Get-KrayolaTheme, Show-ConsoleColours, Write-InColour, Write-RawPairsInColour, Write-ThemedPairsInColour