# encoding: ascii
# api: powershell
# title: ColorPattern
# description: Color strings via Write-Host via Regular Expressions or simple matching via pipelining. The order precedence is set under the $patterns variable, so you can have overlapping matches, resulting in output such as, e.g.:
# version: 0.1
# author: Daniel Cheng
# license: CC0
# x-poshcode-id: 5689
# x-archived: 2015-01-18T09:20:36
# x-published: 2015-01-13T16:31:00
#
# 0 = {ForegroundColor = 'White'; BackgroundColor='Gray'}
# '\d+' = {ForegroundColor = ‘Gray’; BackgroundColor=‘Black’}
# This will result in all strings ‘0’ colored as white with a gray background, while coloring all other digits gray with a black background.
# $colorCollection (system.collections.generic.dictionary) will convert everything, including digits, to a string.
# The performance is lackluster as the script parses regex matches, then finds out the non-matches of the line, and adds it all under a System.Collections.Generic.SortedDictionary object. This is used to output the colors.
# Uncomment #$VerbosePreference = ‘continue’ to get more details as it parses the matches and line characters.
# Tested under Powershell v4.0, and may not work under previous versions without modifications. This was posted as a late reply to http://stackoverflow.com/questions/7362097/color-words-in-powershell-script-format-table-output/27912993#27912993.
#
# Daniel Cheng

#$VerbosePreference = 'continue'
$VerbosePreference = 'silent'

filter ColorPattern {
    param ([object]$colors, [switch]$SimpleMatch)
    [string]$line = $_

    $collection = New-Object 'System.Collections.Generic.SortedDictionary[int, pscustomobject]'
    $RegexOptions = [Text.RegularExpressions.RegexOptions]::IgnoreCase.value__ + [Text.RegularExpressions.RegexOptions]::Singleline.value__

    if ($SimpleMatch){
        $patternMatches = $colors.keys | % {[regex]::Escape($_)}
        $reference = 'Value'
    } else {
        $patternMatches = $colors.keys
        $reference = 'Pattern'
    }

    # detect RegEx matches and add to collection object
    Write-Verbose "'$line'"

    $measureparsing_match = (Measure-Command {
        foreach ($pattern in $patternMatches){
            Write-Verbose "regex pattern: $pattern"
            foreach ($match in ([regex]::Matches($line, $pattern, $RegexOptions))){ # lazy matching
                Write-Verbose "`tmatch index: $($match.Index) length: $($match.length)"

                $currentset = ($match.Index)..($match.Index + $match.length - 1)
                Write-Verbose "`tcurrent set: $currentset"

                if (-not [bool]$collection.Count){
                    Write-Verbose "`t`tindex: $($match.Index) value: $($match.value) (inital add)"
                    [void]$collection.Add($match.Index, [PSCustomObject]@{Length = $match.Length; Value = $match.Value; Pattern = $pattern; Range = $currentset})
                } else {
                    (,$collection.Values) | % {
                        $currentRange = $_.range
                        $intersect = Compare-Object -PassThru $currentset $currentRange -IncludeEqual -ExcludeDifferent
                        if ($intersect){
                            Write-Verbose "`t`tintersect: $([string]($intersect | % {[string]::Concat($_)})) (skipped)"

                            $nonintersect = Compare-Object -PassThru $currentset $intersect
                            Write-Verbose "`t`tnonintersect: $([string]($nonintersect | % {[string]::Concat($_)}))"

                            $nonintersect | % {
                                if ($currentRange -notcontains $_){
                                    Write-Verbose "`t`tindex: $_ value: $($line[$_]) (adding intersect-fallout)"
                                    [void]$collection.Add($_, [PSCustomObject]@{Length = $_.Length; Value = $line[$_]; Pattern = $pattern; Range = $currentset})
                                } else {
                                    Write-Verbose "`t`t`tindex: $_ value: $($line[$_]) (skipped intersect-fallout)"
                                }
                            }
                        } else {
                            Write-Verbose "`t`tindex: $($match.index) value: $($match.value) (adding nonintersect)"
                            [void]$collection.Add($match.Index, [PSCustomObject]@{Length = $match.Length; Value = $match.Value; Pattern = $pattern; Range = $currentset})
                        }
                    } # end values
                } #end if
            } # end matching
        } # end pattern
    }).TotalMilliseconds

    $measureparsing_nonmatch = (Measure-Command {
        if ([bool]$collection.count){ # if there are no matches, skip!
            Compare-Object -PassThru `
            -ReferenceObject (
                $collection.Keys | % { # all matched keys and their lengths
                    $word = $collection.item($_)
                    $currentlength = ($word.value).length
                    ($_..($_ + ($currentlength - 1)))
                }) `
            -DifferenceObject (0..($line.Length - 1)) | # entire line
                % {[void]$collection.Add($_, [PSCustomObject]@{Length = $_.length; Value = $line[$_]})} # add non matches to collection
        }
    }).TotalMilliseconds

    Write-Verbose "match: $measureparsing_match ms. VS nonmatch: $measureparsing_nonmatch ms."

    $collection.keys | % {
        $word = $collection.item($_)
        if ($word.pattern){
            if ($colors.ContainsKey($word.$reference)){
                $color = @{
                    ForegroundColor = $colors[$word.$reference].ForegroundColor;
                    BackgroundColor = $colors[$word.$reference].BackgroundColor
                }
                if ($word.value){
                    Write-Host -NoNewline $([string]::Concat($word.value)) @color
                }
            }
        } else {
            Write-Host -NoNewline $([string]::Concat($word.value))
        }
    }
    Write-Host # needed for line feed
}

$Patterns = [ordered]@{
    # higher in list takes precendence
    'stopped' = @{ForegroundColor = 'Red'; BackgroundColor='DarkRed'}
    'running' = @{ForegroundColor = 'Green'; BackgroundColor='DarkGreen'}
    'paused' = @{ForegroundColor = 'Yellow'; BackgroundColor='DarkYellow'}
    0 = @{ForegroundColor = 'White'; BackgroundColor='Gray'}
    '\d+' = @{ForegroundColor = 'Gray'; BackgroundColor='Black'}
    '\.' = @{ForegroundColor = 'Magenta'; BackgroundColor='DarkMagenta'}
    '(a|e|i|o|u)' = @{ForegroundColor = 'Yellow'; BackgroundColor='DarkYellow'}
    '\w+' = @{ForegroundColor = 'Cyan'; BackgroundColor='DarkCyan'}

}

# strongly typed collection.. we could probably do this better..
$colorCollection = New-Object 'system.collections.generic.dictionary[string, hashtable]'([StringComparer]::OrdinalIgnoreCase) # Ordinal
$Patterns.GetEnumerator() | % {[void]$colorCollection.Add($_.Name, $_.Value)}

Get-Service | Out-String -Stream | ColorPattern -colors $colorCollection
#Get-Service | Out-String -Stream | ColorPattern -colors $colorCollection -SimpleMatch
