lib/Wildling.ParsePattern.ps1

$Script:WildlingTokenParsingRegex = [regex]'(\\[%@$*#&?!-]|[%@$*#&?!-]\{.*?\}|[%@$*#&?!-])'
$Script:WildlingLengthWithVariantsRegex = [regex]'\{((\d+)-(\d+)|(\d+))\}'
$Script:WildlingLengthWithStringRegex = [regex]'\{''(.*)''(?:,(\d+)-(\d+))?(?:,(\d+))?\}'

function Get-WildlingLengthWithVariants {
    param(
        [string] $Part,
        [string[]] $Variants
    )

    $startLength = 1
    $endLength = 1
    $match = $Script:WildlingLengthWithVariantsRegex.Match($Part)

    if ($match.Success) {
        if ($match.Groups[2].Success -and $match.Groups[2].Value.Length -gt 0) {
            $startLength = [int]$match.Groups[2].Value
            $endLength = [int]$match.Groups[3].Value
        }
        elseif ($match.Groups[1].Success) {
            $startLength = [int]$match.Groups[1].Value
            $endLength = $startLength
        }
    }

    return @{
        variants    = $Variants
        startLength = $startLength
        endLength   = $endLength
        src         = $Part
    }
}

function Get-WildlingLengthWithString {
    param([string] $Part)

    $match = $Script:WildlingLengthWithStringRegex.Match($Part)
    if (-not $match.Success) {
        return $null
    }

    if ($match.Groups[2].Success -and $match.Groups[3].Success) {
        return @{
            string      = $match.Groups[1].Value
            startLength = [int]$match.Groups[2].Value
            endLength   = [int]$match.Groups[3].Value
            src         = $Part
        }
    }

    if ($match.Groups[4].Success) {
        $length = [int]$match.Groups[4].Value
        return @{
            string      = $match.Groups[1].Value
            startLength = $length
            endLength   = $length
            src         = $Part
        }
    }

    return @{
        string      = $match.Groups[1].Value
        startLength = 1
        endLength   = 1
        src         = $Part
    }
}

function New-WildlingSimpleTokenizer {
    param([string] $VariantsString)

    $variants = @($VariantsString.ToCharArray() | ForEach-Object { "$_" })
    return {
        param([string] $Part)
        New-WildlingToken (Get-WildlingLengthWithVariants -Part $Part -Variants $variants)
    }.GetNewClosure()
}

function New-WildlingDictionaryToken {
    param(
        [string] $Part,
        [hashtable] $Dictionaries
    )

    $options = Get-WildlingLengthWithString -Part $Part
    if ($null -eq $options -or (
            $options.string -and
            -not $Dictionaries.Contains($options.string)
        )) {
        $options = @{
            variants    = @($Part)
            startLength = 1
            endLength   = 1
            src         = $Part
        }
    }
    else {
        $key = [string]$options.string
        if ($Dictionaries.Contains($key)) {
            $options.variants = @($Dictionaries[$key])
        }
        else {
            $options.variants = @()
        }
    }

    return New-WildlingToken $options
}

function New-WildlingWordsToken {
    param([string] $Part)

    $options = Get-WildlingLengthWithString -Part $Part
    if ($null -eq $options) {
        $options = @{
            variants    = @($Part)
            startLength = 1
            endLength   = 1
            src         = $Part
        }
    }
    else {
        $variants = New-Object System.Collections.Generic.List[string]
        $workString = [string]$options.string
        $index = 0
        while ($index -lt $workString.Length) {
            if (($index + 1) -lt $workString.Length -and
                $workString.Substring($index, 2) -eq '\,') {
                $index += 2
            }
            elseif ($workString[$index] -eq ',') {
                $variants.Add($workString.Substring(0, $index))
                $workString = $workString.Substring($index + 1)
                $index = 0
            }
            else {
                $index++
            }
        }
        $variants.Add($workString)
        $options.variants = @($variants | ForEach-Object { $_.Replace('\,', ',') })
    }

    return New-WildlingToken $options
}

function ConvertTo-WildlingToken {
    param(
        [string] $Part,
        [hashtable] $Dictionaries
    )

    $tokenizers = @{
        '#' = (New-WildlingSimpleTokenizer '0123456789')
        '@' = (New-WildlingSimpleTokenizer 'abcdefghijklmnopqrstuvwxyz')
        '*' = (New-WildlingSimpleTokenizer 'abcdefghijklmnopqrstuvwxyz0123456789')
        '-' = (New-WildlingSimpleTokenizer 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
        '!' = (New-WildlingSimpleTokenizer 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
        '?' = (New-WildlingSimpleTokenizer 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
        '&' = (New-WildlingSimpleTokenizer 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
        '%' = { param([string] $p) New-WildlingDictionaryToken -Part $p -Dictionaries $Dictionaries }
        '$' = { param([string] $p) New-WildlingWordsToken -Part $p }
    }

    $firstChar = if ($Part.Length -gt 0) { [string]$Part[0] } else { '' }
    $secondChar = if ($Part.Length -gt 1) { [string]$Part[1] } else { '' }

    $tokenizer = $null
    if ($firstChar -and $tokenizers.ContainsKey($firstChar)) {
        $tokenizer = $tokenizers[$firstChar]
    }

    $isEscapedToken = ($Part.Length -gt 1) -and ($firstChar -eq '\') -and $tokenizers.ContainsKey($secondChar)

    if ($null -ne $tokenizer) {
        return & $tokenizer $Part
    }

    if ($isEscapedToken) {
        return New-WildlingToken @{
            variants = @($Part.Substring(1))
            src      = $Part
        }
    }

    return New-WildlingToken @{
        variants = @($Part)
        src      = $Part
    }
}

function ConvertTo-WildlingTokens {
    param(
        [string] $InputPattern,
        [hashtable] $Dictionaries
    )

    if ($null -eq $Dictionaries) {
        $Dictionaries = [ordered]@{}
    }

    $parts = $Script:WildlingTokenParsingRegex.Split($InputPattern) |
        Where-Object { $_ -ne '' }

    return @($parts | ForEach-Object { ConvertTo-WildlingToken -Part $_ -Dictionaries $Dictionaries })
}