Elizium.Krayola.psm1

Set-StrictMode -Version 1.0

function Get-DefaultHostUiColours {

  <#
  .NAME
    Get-DefaultHostUiColours
 
  .SYNOPSIS
    Get the default foreground and background colours of the console host.
 
  .DESCRIPTION
    Currently there is an open issue <https://github.com/PowerShell/PowerShell/issues/14727>
  which means that on a mac, the default colours obtained from the host are both incorrectly
  set to -1. This function takes this deficiency into account and will ensure that sensible
  colour values are always returned.
 
  #>

  [OutputType([string[]])]
  param()

  [string]$rawFgc, [string]$rawBgc = get-RawHostUiColours;
  [boolean]$isLight = Get-IsKrayolaLightTerminal;

  [string]$defaultFgc = $isLight ? 'black' : 'gray';
  [string]$defaultBgc = $isLight ? 'gray' : 'black';

  [string]$fore = Get-EnvironmentVariable 'KRAYOLA_FORE' -Default $rawFgc;
  [string]$back = Get-EnvironmentVariable 'KRAYOLA_BACK' -Default $rawBgc;

  [string[]]$colours = [enum]::GetNames("System.ConsoleColor");

  if (-not($( $colours -contains $fore ))) {
    $fore = $defaultFgc;
  }

  if (-not($( $colours -contains $back ))) {
    $back = $defaultBgc;
  }

  return $fore, $back;
}
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, Position=0)]
    [string]$Variable,

    [Parameter(Position = 1)]
    $Default
  )
  $value = [System.Environment]::GetEnvironmentVariable($Variable);

  if (-not($value) -and ($Default)) {
    $value = $Default;
  }
  return $value;
}

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. Typically, a user would create their
  own custom theme and then populate this into the $KrayolaThemes collection. This should
  be done in the user profile so as to become available in all powershell sessions.
  #>

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

    [Parameter(Mandatory = $false)]
    [hashtable]$Themes = $KrayolaThemes,

    [Parameter(Mandatory = $false)]
    [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'     = ' // ';
    }
  )
  [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 Get-Krayon {
  <#
  .NAME
    Get-Krayon
 
  .SYNOPSIS
    Helper factory function that creates Krayon instance.
 
  .DESCRIPTION
    Creates a new krayon instance with the optional krayola theme provided. The krayon contains various methods for writing text directly to the console (See online documentation for more information).
  #>


  [OutputType([Krayon])]
  param(
    [Parameter()]
    [hashtable]$theme = $(Get-KrayolaTheme)
  )
  return New-Krayon -Theme $theme;
}

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 will 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;
  }
}

$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
      :warning: DEPRECATED, use Scribbler/Krayon instead.
 
      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
      !! DEPRECATED, use Scribbler/Krayon instead.
 
      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
      !! DEPRECATED, use Scribbler/Krayon instead.
 
      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 get-RawHostUiColours {
  # This function only really required to aid unit testing
  #
  [OutputType([array])]
  param()
  return (Get-Host).ui.rawUI.ForegroundColor, (Get-Host).ui.rawUI.BackgroundColor;
}

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;
}

<#
.NAME
  couplet
#>

class couplet {
  [string]$Key;
  [string]$Value;
  [boolean]$Affirm;

  couplet () {
  }

  couplet([string[]]$props) {
    $this.Key = $props[0].Replace('\,', ',').Replace('\;', ';');
    $this.Value = $props[1].Replace('\,', ',').Replace('\;', ';');
    $this.Affirm = $props.Length -gt 2 ? [boolean]$props[2] : $false;
  }

  couplet ([string]$key, [string]$value, [boolean]$affirm) {
    $this.Key = $key.Replace('\,', ',').Replace('\;', ';');
    $this.Value = $value.Replace('\,', ',').Replace('\;', ';');
    $this.Affirm = $affirm;
  }

  couplet ([string]$key, [string]$value) {
    $this.Key = $key.Replace('\,', ',').Replace('\;', ';');
    $this.Value = $value.Replace('\,', ',').Replace('\;', ';');
    $this.Affirm = $false;
  }

  couplet([PSCustomObject]$custom) {
    $this.Key = $custom.Key;
    $this.Value = $custom.Value;
    $this.Affirm = $custom.psobject.properties.match('Affirm') -and $custom.Affirm;
  }

  [boolean] equal ([couplet]$other) {
    return ($this.Key -eq $other.Key) `
      -and ($this.Value -eq $other.Value) `
      -and ($this.Affirm -eq $other.Affirm);
  }

  [boolean] cequal ([couplet]$other) {
    return ($this.Key -ceq $other.Key) `
      -and ($this.Value -ceq $other.Value) `
      -and ($this.Affirm -ceq $other.Affirm);
  }

  [string] ToString() {
    return "[Key: '$($this.Key)', Value: '$($this.Value)', Affirm: '$($this.Affirm)']";
  }
} # couplet

<#
.NAME
  line
#>

class line {
  [couplet[]]$Line;
  [string]$Message;

  line() {
  }

  line([couplet[]]$couplets) {
    $this.Line = $couplets.Clone();
  }

  line([string]$message, [couplet[]]$couplets) {
    $this.Message = $message;
    $this.Line = $couplets.Clone();
  }

  line([line]$line) {
    $this.Line = $line.Line.Clone();
  }

  [line] append([couplet]$couplet) {
    $this.Line += $couplet;
    return $this;
  }

  [line] append([couplet[]]$couplet) {
    $this.Line += $couplet;
    return $this;
  }

  [line] append([line]$other) {
    $this.Line += $other.Line;
    return $this;
  }

  [boolean] equal ([line]$other) {
    [boolean]$result = $true;

    if ($this.Line.Length -eq $other.Line.Length) {
      for ($index = 0; ($index -lt $this.Line.Length -and $result); $index++) {
        $result = $this.Line[$index].equal($other.line[$index]);
      }
    }
    else {
      $result = $false;
    }
    return $result;
  }

  [boolean] cequal ([line]$other) {
    [boolean]$result = $true;

    if ($this.Line.Length -eq $other.Line.Length) {
      for ($index = 0; ($index -lt $this.Line.Length -and $result); $index++) {
        $result = $this.Line[$index].cequal($other.line[$index]);
      }
    }
    else {
      $result = $false;
    }
    return $result;
  }

  [string] ToString() {
    return $($this.Line -join '; ');
  }
} # line

<#
.NAME
  Krayon
#>

class Krayon {
  static [array]$ThemeColours = @('affirm', 'key', 'message', 'meta', 'value');

  # Logically public properties
  #
  [string]$ApiFormatWithArg;
  [string]$ApiFormat;
  [hashtable]$Theme;

  # Logically private properties
  #
  hidden [string]$_fgc;
  hidden [string]$_bgc;
  hidden [string]$_defaultFgc;
  hidden [string]$_defaultBgc;

  hidden [array]$_affirmColours;
  hidden [array]$_keyColours;
  hidden [array]$_messageColours;
  hidden [array]$_metaColours;
  hidden [array]$_valueColours;

  hidden [string]$_format;
  hidden [string]$_keyPlaceHolder;
  hidden [string]$_valuePlaceHolder;
  hidden [string]$_open;
  hidden [string]$_close;
  hidden [string]$_separator;
  hidden [string]$_messageSuffix;
  hidden [string]$_messageSuffixFiller;

  hidden [regex]$_expression;
  hidden [regex]$_nativeExpression;

  Krayon([hashtable]$theme, [regex]$expression, [string]$FormatWithArg, [string]$Format, [regex]$NativeExpression) {
    $this.Theme = $theme;

    $this._defaultFgc, $this._defaultBgc = Get-DefaultHostUiColours
    $this._fgc = $this._defaultFgc;
    $this._bgc = $this._defaultBgc;

    $this._affirmColours = $this._initThemeColours('AFFIRM-COLOURS');
    $this._keyColours = $this._initThemeColours('KEY-COLOURS');
    $this._messageColours = $this._initThemeColours('MESSAGE-COLOURS');
    $this._metaColours = $this._initThemeColours('META-COLOURS');
    $this._valueColours = $this._initThemeColours('VALUE-COLOURS');

    $this._format = $theme['FORMAT'];
    $this._keyPlaceHolder = $theme['KEY-PLACE-HOLDER'];
    $this._valuePlaceHolder = $theme['VALUE-PLACE-HOLDER'];
    $this._open = $theme['OPEN'];
    $this._close = $theme['CLOSE'];
    $this._separator = $theme['SEPARATOR'];
    $this._messageSuffix = $theme['MESSAGE-SUFFIX'];
    $this._messageSuffixFiller = [string]::new(' ', $this._messageSuffix.Length);

    $this._expression = $expression;
    $this._nativeExpression = $NativeExpression;
    $this.ApiFormatWithArg = $FormatWithArg;
    $this.ApiFormat = $Format;
  }

  # +Scribblers+ --------------------------------------------------------------
  #
  [Krayon] Scribble([string]$source) {
    if (-not([string]::IsNullOrEmpty($source))) {
      [PSCustomObject []]$operations = $this._parse($source);

      if ($operations.Count -gt 0) {
        foreach ($op in $operations) {
          if ($op.psobject.properties.match('Arg') -and $op.Arg) {
            $null = $this.($op.Api)($op.Arg);
          }
          else {
            $null = $this.($op.Api)();
          }
        }
      }
    }

    return $this;
  }

  [Krayon] ScribbleLn([string]$source) {
    return $this.Scribble($source).Ln();
  }

  # +Text+ --------------------------------------------------------------------
  #
  [Krayon] Text([string]$value) {
    $this._print($value);
    return $this;
  }

  [Krayon] TextLn([string]$value) {
    return $this.Text($value).Ln();
  }

  # +Pair+ (Couplet) ----------------------------------------------------------
  #
  [Krayon] Pair([couplet]$couplet) {
    $this._couplet($couplet);
    return $this;
  }

  [Krayon] PairLn([couplet]$couplet) {
    return $this.Pair($couplet).Ln();
  }

  [Krayon] Pair([PSCustomObject]$couplet) {
    $this._couplet([couplet]::new($couplet));
    return $this;
  }

  [Krayon] PairLn([PSCustomObject]$couplet) {
    return $this.Pair([couplet]::new($couplet)).Ln();
  }

  # +Line+ --------------------------------------------------------------------
  #
  [Krayon] Line([line]$line) {
    $null = $this.fore($this._metaColours[0]).back($this._metaColours[1]).Text($this._open);

    $this._coreLine($line);

    $null = $this.fore($this._metaColours[0]).back($this._metaColours[1]).Text($this._close);
    return $this.Ln();
  }

  [Krayon] NakedLine([line]$nakedLine) {
    $null = $this.fore($this._metaColours[0]).back($this._metaColours[1]).Text(
      [string]::new(' ', $this._open.Length)
    );

    $this._coreLine($nakedLine);

    $null = $this.fore($this._metaColours[0]).back($this._metaColours[1]).Text(
      [string]::new(' ', $this._open.Length)
    );
    return $this.Ln();
  }

  [void] _coreLine([line]$line) {
    [int]$count = 0;
    foreach ($couplet in $line.Line) {
      $null = $this.Pair($couplet);
      $count++;

      if ($count -lt $line.Line.Count) {
        $null = $this.fore($this._metaColours[0]).back($this._metaColours[1]).Text($this._separator);
      }
    }
  }

  [Krayon] Line([string]$message, [line]$line) {
    $this._lineWithMessage($message, $line);

    return $this.Line($line);
  }

  [Krayon] NakedLine([string]$message, [line]$line) {
    $this._lineWithMessage($message, $line);

    return $this.NakedLine($line);
  }

  [void] _lineWithMessage([string]$message, [line]$line) {
    $null = $this.fore($this._messageColours[0]).back($this._messageColours[1]).Text($message);
    $null = $this.fore($this._messageColours[0]).back($this._messageColours[1]).Text(
      [string]::IsNullOrEmpty($message.Trim()) ? $this._messageSuffixFiller : $this._messageSuffix
    );
  }

  # +Message+ -----------------------------------------------------------------
  #
  [Krayon] Message([string]$message) {
    $null = $this.ThemeColour('message');
    return $this.Text($message).Text($this._messageSuffix);
  }

  [Krayon] MessageLn([string]$message) {
    return $this.Message($message).Ln();
  }

  [Krayon] MessageNoSuffix([string]$message) {
    $null = $this.ThemeColour('message');
    return $this.Text($message).Text($this._messageSuffixFiller);
  }

  [Krayon] MessageNoSuffixLn([string]$message) {
    return $this.MessageNoSuffix($message).Ln();
  }

  # +Dynamic+ -----------------------------------------------------------------
  #
  [Krayon] fore([string]$colour) {
    $this._fgc = $colour;
    return $this;
  }

  [Krayon] back([string]$colour) {
    $this._bgc = $colour;
    return $this;
  }

  [Krayon] defaultFore([string]$colour) {
    $this._defaultFgc = $colour;
    return $this;
  }

  [Krayon] defaultBack([string]$colour) {
    $this._defaultBgc = $colour;
    return $this;
  }

  [string] getDefaultFore() {
    return $this._defaultFgc;
  }

  [string] getDefaultBack() {
    return $this._defaultBgc;
  }

  # +Control+ -----------------------------------------------------------------
  #
  [void] End() {}

  [Krayon] Ln() {
    # Write a non-breaking space (0xA0)
    # https://en.wikipedia.org/wiki/Non-breaking_space
    # (This is required because of a con-host bug in windows =>
    # https://github.com/microsoft/terminal/issues/1040)
    #
    Write-Host ([char]0xA0);
    return $this;
  }

  [Krayon] Reset() {
    $this._fgc = $this._defaultFgc;
    $this._bgc = $this._defaultBgc;
    return $this;
  }

  # +Theme+ -------------------------------------------------------------------
  #
  [Krayon] ThemeColour([string]$val) {
    [string]$trimmedValue = $val.Trim();
    if ([Krayon]::ThemeColours -contains $trimmedValue) {
      [array]$cols = $this.Theme[$($trimmedValue.ToUpper() + '-COLOURS')];
      $this._fgc = $cols[0];
      $this._bgc = $cols.Length -eq 2 ? $cols[1] : $this._defaultBgc;
    }
    else {
      Write-Debug "Krayon.ThemeColour: ignoring invalid theme colour: '$trimmedValue'"
    }
    return $this;
  }

  # +Static Foreground Colours+ -----------------------------------------------
  #
  [Krayon] black() {
    $this._fgc = 'black';
    return $this;
  }

  [Krayon] darkBlue() {
    $this._fgc = 'darkBlue';
    return $this;
  }

  [Krayon] darkGreen() {
    $this._fgc = 'darkGreen';
    return $this;
  }

  [Krayon] darkCyan() {
    $this._fgc = 'darkCyan';
    return $this;
  }

  [Krayon] darkRed() {
    $this._fgc = 'darkRed';
    return $this;
  }

  [Krayon] darkMagenta() {
    $this._fgc = 'darkMagenta';
    return $this;
  }

  [Krayon] darkYellow() {
    $this._fgc = 'darkYellow';
    return $this;
  }

  [Krayon] gray() {
    $this._fgc = 'gray';
    return $this;
  }

  [Krayon] darkGray() {
    $this._fgc = 'darkGray';
    return $this;
  }

  [Krayon] blue() {
    $this._fgc = 'blue';
    return $this;
  }

  [Krayon] green() {
    $this._fgc = 'green';
    return $this;
  }

  [Krayon] cyan() {
    $this._fgc = 'cyan';
    return $this;
  }

  [Krayon] red() {
    $this._fgc = 'red';
    return $this;
  }

  [Krayon] magenta() {
    $this._fgc = 'magenta';
    return $this;
  }

  [Krayon] yellow() {
    $this._fgc = 'yellow';
    return $this;
  }

  [Krayon] white() {
    $this._fgc = 'white';
    return $this;
  }

  # +Background Colours+ ------------------------------------------------------
  #
  [Krayon] bgBlack() {
    $this._bgc = 'Black';
    return $this;
  }

  [Krayon] bgDarkBlue() {
    $this._bgc = 'DarkBlue';
    return $this;
  }

  [Krayon] bgDarkGreen() {
    $this._bgc = 'DarkGreen';
    return $this;
  }

  [Krayon] bgDarkCyan() {
    $this._bgc = 'DarkCyan';
    return $this;
  }

  [Krayon] bgDarkRed() {
    $this._bgc = 'DarkRed';
    return $this;
  }

  [Krayon] bgDarkMagenta() {
    $this._bgc = 'DarkMagenta';
    return $this;
  }

  [Krayon] bgDarkYellow() {
    $this._bgc = 'DarkYellow';
    return $this;
  }

  [Krayon] bgGray() {
    $this._bgc = 'Gray';
    return $this;
  }

  [Krayon] bgDarkGray() {
    $this._bgc = 'DarkGray';
    return $this;
  }

  [Krayon] bgBlue() {
    $this._bgc = 'Blue';
    return $this;
  }

  [Krayon] bgGreen() {
    $this._bgc = 'Green';
    return $this;
  }

  [Krayon] bgCyan() {
    $this._bgc = 'Cyan';
    return $this;
  }

  [Krayon] bgRed () {
    $this._bgc = 'Red';
    return $this;
  }

  [Krayon] bgMagenta() {
    $this._bgc = 'Magenta';
    return $this;
  }

  [Krayon] bgYellow() {
    $this._bgc = 'Yellow';
    return $this;
  }

  [Krayon] bgWhite() {
    $this._bgc = 'White';
    return $this;
  }

  # +Compounders+ -------------------------------------------------------------
  #
  [Krayon] Line([string]$semiColonSV) {
    return $this._lineFromSemiColonSV($semiColonSV, 'Line');
  }

  [Krayon] NakedLine([string]$semiColonSV) {
    return $this._lineFromSemiColonSV($semiColonSV, 'NakedLine');
  }

  hidden [line] _convertToLine([string[]]$constituents) {
    [couplet[]]$couplets = ($constituents | ForEach-Object {
        New-Pair $($_ -split '(?<!\\),', 0, 'RegexMatch');
      });
    [line]$line = New-Line $couplets;

    return $line;
  }

  hidden [Krayon] _lineFromSemiColonSV([string]$semiColonSV, [string]$op) {
    [string[]]$constituents = $semiColonSV -split '(?<!\\);', 0, 'RegexMatch';
    [string]$message, [string[]]$remainder = $constituents;

    [string]$unescapedComma = '(?<!\\),';
    if ($message -match $unescapedComma) {
      [line]$line = $this._convertToLine($constituents);
      $null = $this.$op($line);
    }
    else {
      [line]$line = $this._convertToLine($remainder);
      $null = $this.$op($message, $line);
    }

    return $this;
  }

  [Krayon] Pair([string]$csv) {
    [string[]]$constituents = $csv -split '(?<!\\),';

    [couplet]$pair = New-Pair $constituents;
    $this._couplet($pair);

    return $this;
  }

  [Krayon] PairLn([string]$csv) {
    return $this.Pair($csv).Ln();
  }

  # +Utility+ -----------------------------------------------------------------
  #

  [string] Native([string]$structured) {
    return $this._nativeExpression.Replace($structured, '');
  }

  # styles (don't exist yet; virtual terminal sequences?)
  #
  [Krayon] bold() {
    return $this;
  }

  [Krayon] italic() {
    return $this;
  }

  [Krayon] strike() {
    return $this;
  }

  [Krayon] under() {
    return $this;
  }

  # Logically private
  #
  hidden [void] _couplet([couplet]$couplet) {
    [string[]]$constituents = Split-KeyValuePairFormatter -Format $this._format `
      -KeyConstituent $couplet.Key -ValueConstituent $couplet.Value `
      -KeyPlaceHolder $this._keyPlaceHolder -ValuePlaceHolder $this._valuePlaceHolder;

    # header
    #
    $this._fgc = $this._metaColours[0];
    $this._bgc = $this._metaColours[1];
    $this._print($constituents[0]);

    # key
    #
    $this._fgc = $this._keyColours[0];
    $this._bgc = $this._keyColours[1];
    $this._print($constituents[1]);

    # mid
    #
    $this._fgc = $this._metaColours[0];
    $this._bgc = $this._metaColours[1];
    $this._print($constituents[2]);

    # value
    #
    $this._fgc = ($couplet.Affirm) ? $this._affirmColours[0] : $this._valueColours[0];
    $this._bgc = ($couplet.Affirm) ? $this._affirmColours[1] : $this._valueColours[1];
    $this._print($constituents[3]);

    # tail
    #
    $this._fgc = $this._metaColours[0];
    $this._bgc = $this._metaColours[1];
    $this._print($constituents[4]);

    $null = $this.Reset();
  } # _couplet

  hidden [void] _print([string]$text) {
    Write-Host $text -ForegroundColor $this._fgc -BackgroundColor $this._bgc -NoNewline;
  } # _print

  hidden [void] _printLn([string]$text) {
    Write-Host $text -ForegroundColor $this._fgc -BackgroundColor $this._bgc;
  } # _printLn

  hidden [array] _initThemeColours([string]$coloursKey) {
    [array]$thc = $this.Theme[$coloursKey];
    if ($thc.Length -eq 1) {
      $thc += $this._defaultBgc;
    }
    return $thc;
  } # _initThemeColours

  hidden [array] _parse ([string]$source) {
    [System.Text.RegularExpressions.Match]$previousMatch = $null;

    [PSCustomObject []]$operations = if ($this._expression.IsMatch($source)) {
      [System.Text.RegularExpressions.MatchCollection]$mc = $this._expression.Matches($source);
      [int]$count = 0;
      foreach ($m in $mc) {
        [string]$api = $m.Groups['api'];
        [string]$parm = $m.Groups['p'];

        if ($previousMatch) {
          [int]$snippetStart = $previousMatch.Index + $previousMatch.Length;
          [int]$snippetEnd = $m.Index;
          [int]$snippetSize = $snippetEnd - $snippetStart;
          [string]$snippet = $source.Substring($snippetStart, $snippetSize);

          # If we find a text snippet, it must be applied before the current api invoke
          #
          if (-not([string]::IsNullOrEmpty($snippet))) {
            [PSCustomObject] @{ Api = 'Text'; Arg = $snippet; }
          }

          if (-not([string]::IsNullOrEmpty($parm))) {
            [PSCustomObject] @{ Api = $api; Arg = $parm; }
          }
          else {
            [PSCustomObject] @{ Api = $api; }
          }
        }
        else {
          [string]$snippet = if ($m.Index -eq 0) {
            [int]$snippetStart = -1;
            [int]$snippetEnd = -1;
            [string]::Empty
          }
          else {
            [int]$snippetStart = 0;
            [int]$snippetEnd = $m.Index;
            $source.Substring($snippetStart, $snippetEnd);
          }
          if (-not([string]::IsNullOrEmpty($snippet))) {
            [PSCustomObject] @{ Api = 'Text'; Arg = $snippet; }
          }

          if (-not([string]::IsNullOrEmpty($parm))) {
            [PSCustomObject] @{ Api = $api; Arg = $parm; }
          }
          else {
            [PSCustomObject] @{ Api = $api; }
          }
        }
        $previousMatch = $m;
        $count++;

        if ($count -eq $mc.Count) {
          [int]$lastSnippetStart = $m.Index + $m.Length;
          [string]$snippet = $source.Substring($lastSnippetStart);

          if (-not([string]::IsNullOrEmpty($snippet))) {
            [PSCustomObject] @{ Api = 'Text'; Arg = $snippet; }
          }
        }
      } # foreach $m
    }
    else {
      @([PSCustomObject] @{ Api = 'Text'; Arg = $source; });
    }

    return $operations;
  } # _parse
} # Krayon

<#
.NAME
  Scribbler
#>

class Scribbler {
  [System.Text.StringBuilder]$Builder;
  [Krayon]$Krayon;

  hidden [System.Text.StringBuilder]$_session;

  Scribbler([System.Text.StringBuilder]$builder, [Krayon]$krayon,
    [System.Text.StringBuilder]$Session) {
    $this.Builder = $builder;
    $this.Krayon = $krayon;
    $this._session = $Session;
  }

  # +Scribblers+ --------------------------------------------------------------
  #
  [void] Scribble([string]$structuredContent) {
    $null = $this.Builder.Append($structuredContent);
  }

  # +Text+ Accelerators -------------------------------------------------------
  #
  [Scribbler] Text([string]$value) {
    $this.Scribble($value);
    return $this;
  }

  [Scribbler] TextLn([string]$value) {
    return $this.Text($value).Ln();
  }

  # +Pair+ (Couplet) Accelerators ---------------------------------------------
  #
  [Scribbler] Pair([couplet]$couplet) {
    [string]$pairSnippet = $this.PairSnippet($couplet);
    $this.Scribble($pairSnippet);

    return $this;
  }

  [Scribbler] PairLn([couplet]$couplet) {
    return $this.Pair($couplet).Ln();
  }

  [Scribbler] Pair([PSCustomObject]$coupletObj) {
    [couplet]$couplet = [couplet]::new($coupletObj);

    return $this.Pair($couplet);
  }

  [Scribbler] PairLn([PSCustomObject]$coupletObj) {
    return $this.Pair([couplet]::new($coupletObj)).Ln();
  }

  # +Line+ Accelerators -------------------------------------------------------
  #
  [Scribbler] Line([string]$message, [line]$line) {
    $this._coreScribbleLine($message, $line, 'Line');

    return $this;
  }

  [Scribbler] Line([line]$line) {
    $this.Line([string]::Empty, $line);

    return $this;
  }

  [Scribbler] NakedLine([string]$message, [line]$nakedLine) {
    $this._coreScribbleLine($message, $nakedLine, 'NakedLine');

    return $this;
  }

  [Scribbler] NakedLine([line]$line) {
    $this.NakedLine([string]::Empty, $line);

    return $this;
  }

  hidden [void] _coreScribbleLine([string]$message, [line]$line, [string]$lineType) {
    [string]$structuredLine = $(($Line.Line | ForEach-Object {
          "$($this._escape($_.Key)),$($this._escape($_.Value)),$($_.Affirm)"
        }) -join ';');

    if (-not([string]::IsNullOrEmpty($message))) {
      $structuredLine = "$message;" + $structuredLine;
    }

    [string]$lineSnippet = $this.WithArgSnippet(
      $lineType, $structuredLine
    )
    $this.Scribble("$($lineSnippet)");
  } # _coreScribbleLine

  # +Message+ Accelerators ----------------------------------------------------
  #
  [Scribbler] Message([string]$message) {
    [string]$snippet = $this.WithArgSnippet('Message', $message);
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] MessageLn([string]$message) {
    return $this.Message($message).Ln();
  }

  [Scribbler] MessageNoSuffix([string]$message) {
    [string]$snippet = $this.WithArgSnippet('MessageNoSuffix', $message);
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] MessageNoSuffixLn([string]$message) {
    return $this.MessageNoSuffix($message).Ln();
  }

  # +Dynamic+ Accelerators ----------------------------------------------------
  #
  [Scribbler] fore([string]$colour) {
    [string]$snippet = $this.WithArgSnippet('fore', $colour);
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] back([string]$colour) {
    [string]$snippet = $this.WithArgSnippet('back', $colour);
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] defaultFore([string]$colour) {
    [string]$snippet = $this.WithArgSnippet('defaultFore', $colour);
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] defaultBack([string]$colour) {
    [string]$snippet = $this.WithArgSnippet('defaultBack', $colour);
    $this.Scribble($snippet);

    return $this;
  }

  # +Control+ Accelerators ----------------------------------------------------
  #
  [void] End() { }

  [void] Flush () {
    $this.Krayon.Scribble($this.Builder.ToString());

    $this._clear();
  }

  [Scribbler] Ln() {
    [string]$snippet = $this.Snippets('Ln');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] Reset() {
    [string]$snippet = $this.Snippets('Reset');
    $this.Scribble($snippet);

    return $this;
  }

  [void] Restart() {
    if ($this._session) {
      $this._session.Clear();
    }
    $this.Builder.Clear();
    $this.Krayon.Reset().End();
  }

  [void] Save([string]$fullPath) {
    [string]$directoryPath = [System.IO.Path]::GetDirectoryName($fullPath);
    [string]$fileName = [System.IO.Path]::GetFileName($fullPath) + '.struct.txt';
    [string]$writeFullPath = Join-Path -Path $directoryPath -ChildPath $fileName;

    if ($this._session) {
      if (-not(Test-Path -Path $writeFullPath)) {
        Set-Content -LiteralPath $writeFullPath -Value $this._session.ToString();
      }
      else {
        Write-Warning -Message $(
          "Can't write session to '$writeFullPath'. (file already exists)."
        );
      }
    }
    else {
      Write-Warning -Message $(
        "Can't write session to '$writeFullPath'. (session not set)."
      );
    }
  }

  # +Theme+ Accelerators ------------------------------------------------------
  #
  [Scribbler] ThemeColour([string]$val) {
    [string]$snippet = $this.WithArgSnippet('ThemeColour', $val);

    $this.Scribble($snippet);
    return $this;
  }

  # +Static Foreground Colours+ Accelerators ----------------------------------
  #
  [Scribbler] black() {
    [string]$snippet = $this.Snippets('black');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] darkBlue() {
    [string]$snippet = $this.Snippets('darkBlue');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] darkGreen() {
    [string]$snippet = $this.Snippets('darkGreen');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] darkCyan() {
    [string]$snippet = $this.Snippets('darkCyan');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] darkRed() {
    [string]$snippet = $this.Snippets('darkRed');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] darkMagenta() {
    [string]$snippet = $this.Snippets('darkMagenta');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] darkYellow() {
    [string]$snippet = $this.Snippets('darkYellow');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] gray() {
    [string]$snippet = $this.Snippets('gray');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] darkGray() {
    [string]$snippet = $this.Snippets('darkGray');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] blue() {
    [string]$snippet = $this.Snippets('blue');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] green() {
    [string]$snippet = $this.Snippets('green');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] cyan() {
    [string]$snippet = $this.Snippets('cyan');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] red() {
    [string]$snippet = $this.Snippets('red');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] magenta() {
    [string]$snippet = $this.Snippets('magenta');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] yellow() {
    [string]$snippet = $this.Snippets('yellow');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] white() {
    [string]$snippet = $this.Snippets('white');
    $this.Scribble($snippet);

    return $this;
  }

  # +Background Colours+ Accelerators -----------------------------------------
  #
  [Scribbler] bgBlack() {
    [string]$snippet = $this.Snippets('bgBlack');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgDarkBlue() {
    [string]$snippet = $this.Snippets('bgDarkBlue');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgDarkGreen() {
    [string]$snippet = $this.Snippets('bgDarkGreen');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgDarkCyan() {
    [string]$snippet = $this.Snippets('bgDarkCyan');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgDarkRed() {
    [string]$snippet = $this.Snippets('bgDarkRed');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgDarkMagenta() {
    [string]$snippet = $this.Snippets('bgDarkMagenta');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgDarkYellow() {
    [string]$snippet = $this.Snippets('bgDarkYellow');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgGray() {
    [string]$snippet = $this.Snippets('bgGray');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgDarkGray() {
    [string]$snippet = $this.Snippets('bgDarkGray');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgBlue() {
    [string]$snippet = $this.Snippets('bgBlue');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgGreen() {
    [string]$snippet = $this.Snippets('bgGreen');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgCyan() {
    [string]$snippet = $this.Snippets('bgCyan');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgRed() {
    [string]$snippet = $this.Snippets('bgRed');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgMagenta() {
    [string]$snippet = $this.Snippets('bgMagenta');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgYellow() {
    [string]$snippet = $this.Snippets('bgYellow');
    $this.Scribble($snippet);

    return $this;
  }

  [Scribbler] bgWhite() {
    [string]$snippet = $this.Snippets('bgWhite');
    $this.Scribble($snippet);

    return $this;
  }

  # +Utility+ -----------------------------------------------------------------
  #

  [string] Snippets ([string[]]$items) {
    [string]$result = [string]::Empty;
    foreach ($i in $items) {
      $result += $($this.Krayon.ApiFormat -f $i);
    }
    return $result;
  }

  [string] WithArgSnippet([string]$api, [string]$arg) {
    return "$($this.Krayon.ApiFormatWithArg -f $api, $arg)";
  }

  [string] PairSnippet([couplet]$pair) {
    [string]$key = $this._escape($pair.Key);
    [string]$value = $this._escape($pair.Value);

    [string]$csv = "$($key),$($value),$($pair.Affirm)";
    [string]$pairSnippet = $this.WithArgSnippet(
      'Pair', $csv
    )
    return $pairSnippet;
  }

  [string] LineSnippet([line]$line) {
    [string]$structuredLine = $(($line.Line | ForEach-Object {
          "$($this._escape($_.Key)),$($this._escape($_.Value)),$($_.Affirm)"
        }) -join ';');

    [string]$lineSnippet = $this.WithArgSnippet(
      'Line', $structuredLine
    )
    return $lineSnippet;
  }

  # Other internal
  #
  hidden [void] _clear() {
    if ($this._session) {
      $this._session.Append($this.Builder);
    }

    $this.Builder.Clear();
  }

  hidden [string] _escape([string]$value) {
    return $value.Replace(';', '\;').Replace(',', '\,');
  }
} # Scribbler

class QuietScribbler: Scribbler {
  QuietScribbler([System.Text.StringBuilder]$builder, [Krayon]$krayon,
    [System.Text.StringBuilder]$Session):base($builder, $krayon, $Session) { }

  [void] Flush () {
    $this._clear();
  }
} # QuietScribbler

function New-Krayon {
  <#
  .NAME
    New-Krayon
 
  .SYNOPSIS
    Helper factory function that creates Krayon instance.
 
  .DESCRIPTION
    The client can specify a custom regular expression and corresponding
  formatters which together support the scribble functionality (the ability
  to invoke krayon functions via a 'structured' string as opposed to calling
  the methods explicitly). Normally, the client can accept the default
  expression and formatter arguments. However, depending on circumstance,
  a custom pattern can be supplied along with corresponding formatters. The
  formatters specified MUST correspond to the pattern and if they don't, then
  an exception is thrown.
    The default tokens used are as follows:
 
  * lead: 'µ'
  * open: '«'
  * close: '»'
 
  So this means that to invoke the 'red' function on the Krayon, the client
  should invoke the Scribble function with the following 'structured' string:
  'µ«red»'.
  To invoke a command which requires a parameter eg 'Message', the client needs
  to specify a string like: 'µ«Message,Greetings Earthlings»'. (NB: instructions
  are case insensitive). (Please note, that custom regular expressions do not have
  to have 'lead', 'open' and 'close' tokens as illustrated here; these are just
  what are used by default. The client can define any expression with formatters
  as long as it able to capture api calls with a single optional parameter.)
 
  However, please do not specify a literal string like this. If scribble functionality
  is required, then the Scribbler object should be used. The scribbler
  contains helper functions 'Snippets' and 'WithArgSnippet'.
  'Snippets', which when given an array of instructions will return the correct
  structured string. So to 'Reset', set the foreground colour to red and the
  background colour to black: $scribbler.Snippets(@('Reset', 'red', 'black'))
  which would return 'µ«Reset»µ«red»µ«black»'.
 
  And 'WithArgSnippet' for the above Message example, the client should use
  the following:
 
  [string]$snippet = $scribbler.WithArgSnippet('Message', 'Greetings Earthlings');
 
  $scribbler.Scribble($snippet);
 
  This is so that if for any reason, the expression and corresponding formatters
  need to be changed, then no other client code would be affected.
 
  And for completeness, an invoke requiring compound param representation eg to invoke
  the 'Line' method would be defined as:
 
  'one,Eve of Destruction;two,Bango' => this is a line with 2 couplets
  which would be invoked like so:
 
  [string]$snippet = $scribbler.WithArgSnippet('one,Eve of Destruction;two,Bango');
 
  and to Invoke 'Line' with a message:
 
  'Greetings Earthlings;one,Eve of Destruction;two,Bango'
 
  if you look at the first segment, you will see that it contains no comma. The scribbler
  will interpret the first segment as a message with subsequent segments containing
  valid comma separated values, split by semi-colons. (Be careful to construct this string
  properly; if a segment does not contain a comma (except for the first segment), then
  will likely be an error).
 
  And if the message required, includes a comma, then it should be escaped with a
  back slash '\':
 
  'Greetings\, Earthlings;one,Eve of Destruction;two,Bango'.
 
  .PARAMETER Expression
  A custom regular expression pattern than capture a Krayon method api call and an optional
  parameter. The expression MUST contain the following 2 named capture groups:
 
  * 'api': string to represent a method call on the Krayon instance.
  * 'p': optional string to represent a parameter passed into the function denoted by 'api'.
 
  Instructions can either have 0 or 1 argument. When an argument is specified that must represent
  a compound value (multiple items), then a compound representation must be used,
  eg a couplet is represented by a comma separated string and a line is represented
  by a semi-colon separated value, where the value inside each semi-colon segment is
  a pair represented by a comma separated value.
 
  .PARAMETER NativeExpression
    A custom regular expression pattern that recognises the content interpreted by the Krayon Scribble
  method. The 'inverse' native expression parses a structured string and returns the core text stripped
  of any api tokens.
 
  .PARAMETER Theme
  A hashtable instance containing the Krayola theme.
 
  .PARAMETER WriterFormat
    Format string that represents a Krayon api method call without an argument. This format
  needs to conform to the regular expression pattern specified by Expression.
 
  .PARAMETER WriterFormatWithArg
    Format string that represents a Krayon api method call with an argument. This format needs
  to conform to the regular expression pattern specified by Expression. This format must
  accommodate a single parameter.
  #>

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
  [OutputType([Krayon])]
  param(
    [Parameter()]
    [hashtable]$Theme = $(Get-KrayolaTheme),

    [Parameter()]
    # OLD: '&\[(?<api>[\w]+)(,(?<p>[^\]]+))?\]'
    [regex]$Expression = [regex]::new("µ«(?<api>[\w]+)(,(?<p>[^»]+))?»"),

    [Parameter()]
    # OLD: '&[{0},{1}]'
    [string]$WriterFormatWithArg = "µ«{0},{1}»",

    [Parameter()]
    # OLD: '&[{0}]'
    [string]$WriterFormat = "µ«{0}»",

    [Parameter()]
    # OLD: '&\[[\w\s\-_]+(?:,\s*[\w\s\-_]+)?\]'
    [string]$NativeExpression = [regex]::new("µ«[\w\s\-_]+(?:,\s*[\w\s\-_]+)?»")
  )

  [string]$dummyWithArg = $WriterFormatWithArg -replace "\{\d{1,2}\}", 'dummy';
  if (-not($Expression.IsMatch($dummyWithArg))) {
    throw "New-Krayon: invalid WriterFormatWithArg ('$WriterFormatWithArg'), does not match the provided Expression: '$($Expression.ToString())'";
  }

  [string]$dummy = $WriterFormat -replace "\{\d{1,2}\}", 'dummy';
  if (-not($Expression.IsMatch($dummy))) {
    throw "New-Krayon: invalid WriterFormat ('$WriterFormat'), does not match the provided Expression: '$($Expression.ToString())'";
  }
  return [Krayon]::new($Theme, $Expression, $WriterFormatWithArg, $WriterFormat, $NativeExpression);
} # New-Krayon

function New-Line {
  <#
  .NAME
    New-Line
 
  .SYNOPSIS
    Helper factory function that creates Line instance.
 
  .DESCRIPTION
    A Line is a wrapper around a collection of couplets.
 
  .PARAMETER Krayon
    The underlying krayon instance that performs real writes to the host.
 
  .PARAMETER couplets
    Collection of couplets to create Line with.
  #>

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
  [OutputType([line])]
  [Alias('kl')]
  param(
    [Parameter()]
    [couplet[]]$couplets = @()
  )
  return [line]::new($couplets);
} # New-Line

<#
.NAME
  New-Pair
 
.SYNOPSIS
  Helper factory function that creates a couplet instance.
 
.DESCRIPTION
  A couplet is logically 2 items, but can contain a 3rd element representing
its 'affirmed' status. An couplet that is affirmed is one that can be highlighted
according to the Krayola theme (AFFIRM-COLOURS).
 
.PARAMETER couplet
  A 2 or 3 item array representing a key/value pair and optional affirm boolean.
#>

function New-Pair {
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
  [OutputType([couplet])]
  [Alias('kp')]
  param(
    [Parameter()]
    [string[]]$couplet
  )
  return ($couplet.Count -ge 3) `
    ? [couplet]::new($couplet[0], $couplet[1], [System.Convert]::ToBoolean($couplet[2])) `
    : [couplet]::new($couplet[0], $couplet[1]);
} # New-Pair

function New-Scribbler {
  <#
  .NAME
    New-Scribbler
 
  .SYNOPSIS
    Helper factory function that creates a Scribbler instance.
 
  .DESCRIPTION
    Creates a new Scribbler instance with the optional krayon provided. The scribbler acts
  like a wrapper around the Krayon so that control can be exerted over where the output is
  directed to. In an interactive environment, clearly, the user needs to see the output so
  the Scribbler will direct the Krayon to render output to the console. However, within the
  context of a unit test the output needs to be suppressed. This is achieved by creating
  a Quiet Scribbler achieved by setting the Test switch or forcing it via the Silent switch.
 
  .PARAMETER Krayon
    The underlying krayon instance that performs real writes to the host.
 
  .PARAMETER Test
    switch to indicate if this is being invoked from a test case, so that the
  output can be suppressed if appropriate. By default, the test cases should be
  quiet. During development and test stage, the user might want to see actual
  output in the test output. The presence of variable 'EliziumTest' in the
  environment will enable verbose tests. When invoked by an interactive user in
  production environment, the Test flag should not be set. Doing so will suppress
  the output depending on the presence 'EliziumTest'. ALL test cases should
  specify this Test flag. This also applies to third party users building tests for commands
  that use the Scribbler.
 
  .PARAMETER Save
    switch to indicate if the Scribbler should record all output which will be
  saved to file for future playback.
 
  .PARAMETER Silent
    switch to force the creation of a Quiet Scribbler. Can not be specified at the
  same time as Test (although not currently enforced). Silent overrides Test.
  #>

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
  [OutputType([Scribbler])]
  param(
    [Parameter()]
    [Krayon]$Krayon = $(New-Krayon),

    [Parameter()]
    [switch]$Test,

    [Parameter()]
    [switch]$Save,

    [switch]$Silent
  )
  [System.text.StringBuilder]$builder = [System.text.StringBuilder]::new();
  [System.text.StringBuilder]$session = $Save.ToBool() ? [System.text.StringBuilder]::new() : $null;

  [Scribbler]$scribbler = if ($Silent) {
    [QuietScribbler]::New($builder, $Krayon, $null);
  }
  elseif ($Test) {
    $($null -eq (Get-EnvironmentVariable 'EliziumTest')) `
      ? [QuietScribbler]::New($builder, $Krayon, $session) `
      : [Scribbler]::New($builder, $Krayon, $session);
  }
  else {
    [Scribbler]::New($builder, $Krayon, $session);
  }

  return $scribbler;
} # New-Scribbler
Export-ModuleMember -Variable KrayolaThemes

Export-ModuleMember -Alias Show-ConsoleColors, Write-InColor, Write-RawPairsInColor, Write-ThemedPairsInColor, kp, kl

Export-ModuleMember -Function Get-DefaultHostUiColours, Get-EnvironmentVariable, Get-IsKrayolaLightTerminal, Get-KrayolaTheme, Get-Krayon, Show-ConsoleColours, Write-InColour, Write-RawPairsInColour, Write-ThemedPairsInColour, New-Krayon, New-Line, New-Pair, New-Scribbler