utils/text-tag-parser.psm1

class TextTagParser {
    [regex] $TagRegex
    [string] $DefaultColor
    [string] $DefaultBgColor

    TextTagParser() {
        $this.init('black', 'white')
    }

    TextTagParser([string]$defaultColor = 'black', [string]$defaultBgColor = 'white') {
        $this.init($defaultColor, $defaultBgColor)
    }

    [void]init([string]$defaultColor, [string]$defaultBgColor) {
        $pattern = '(?:<b>(?<bold>.*?)<\/b>)|(?:<i>(?<italic>.*?)<\/i>)|(?:<color:(?<colorName>[#a-z0-9]{3,15})>(?<colorText>.*?)<\/color>)|(?:<bgColor:(?<bgName>[#a-z0-9]{3,15})>(?<bgText>.*?)<\/bgColor>)'
        $options = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Singleline
        $this.TagRegex = [regex]::new($pattern, $options)
        $this.DefaultColor = $defaultColor
        $this.DefaultBgColor = $defaultBgColor
    }

    [System.Collections.ArrayList] Parse([string] $text) {
        $text = $text -replace "(\r?\n)\s*", " "
        $result = [System.Collections.ArrayList]::new()
        $this.ParseRecursive($text, $this.DefaultColor, $this.DefaultBgColor, $false, $false, $result)
        return $result
    }

    hidden [void] ParseRecursive(
        [string] $text,
        [string] $currentColor,
        [string] $currentBgColor,
        [bool] $currentBold,
        [bool] $currentItalic,
        [System.Collections.ArrayList] $collector
    ) {
        if (-not $text) { return }

        $regexMatches = $this.TagRegex.Matches($text)
        if ($regexMatches.Count -eq 0) {
            $null = $collector.Add(@{
                    text    = $text
                    color   = $currentColor
                    bgColor = $currentBgColor
                    bold    = $currentBold
                    italic  = $currentItalic
                })
            return
        }

        $lastIndex = 0
        foreach ($match in $regexMatches) {
            # Text before the current tag
            $prefixLen = $match.Index - $lastIndex
            if ($prefixLen -gt 0) {
                $prefix = $text.Substring($lastIndex, $prefixLen)
                if ($prefix) {
                    $null = $collector.Add(@{
                            text    = $prefix
                            color   = $currentColor
                            bgColor = $currentBgColor
                            bold    = $currentBold
                            italic  = $currentItalic
                        })
                }
            }

            # Determine which tag matched
            if ($match.Groups['bold'].Success) {
                $this.ParseRecursive($match.Groups['bold'].Value, $currentColor, $currentBgColor, $true, $currentItalic, $collector)
            }
            elseif ($match.Groups['italic'].Success) {
                $this.ParseRecursive($match.Groups['italic'].Value, $currentColor, $currentBgColor, $currentBold, $true, $collector)
            }
            elseif ($match.Groups['colorName'].Success -and $match.Groups['colorText'].Success) {
                $this.ParseRecursive($match.Groups['colorText'].Value, $match.Groups['colorName'].Value, $currentBgColor, $currentBold, $currentItalic, $collector)
            }
            elseif ($match.Groups['bgName'].Success -and $match.Groups['bgText'].Success) {
                $this.ParseRecursive($match.Groups['bgText'].Value, $currentColor, $match.Groups['bgName'].Value, $currentBold, $currentItalic, $collector)
            }

            $lastIndex = $match.Index + $match.Length
        }

        # Trailing text after the last tag
        if ($lastIndex -lt $text.Length) {
            $suffix = $text.Substring($lastIndex)
            if ($suffix) {
                $null = $collector.Add(@{
                        text    = $suffix
                        color   = $currentColor
                        bgColor = $currentBgColor
                        bold    = $currentBold
                        italic  = $currentItalic
                    })
            }
        }
    }
}


$par = [TextTagParser]::new()

$par.Parse("Neutral text <color:red><b>bold red text</b></color> other neutral text<color:green>green text</color> another neutral text <bgColor:blue>blue <i>background</i> text</bgColor>")