Private/Read-Toon.ps1

function Read-Toon {
    param(
        [string]$ToonText,
        [int]$IndentSize = 2,
        [bool]$Strict = $true,
        [string]$ExpandPaths = 'off'
    )

    $lines = $ToonText -split "`n"
    $parsed = Read-ToonLines -Lines $lines -IndentSize $IndentSize -Strict $Strict

    if ($ExpandPaths -eq 'safe') {
        $parsed = Convert-ExpandedPath -Value $parsed -Strict $Strict
    }

    return $parsed
}

function Read-ToonLines {
    param([string[]]$Lines, [int]$IndentSize, [bool]$Strict, [int]$CurrentDepth = 0)

    $nonEmptyLines = $Lines | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
    if ($nonEmptyLines.Count -eq 0) { return $null }

    $firstLine = $nonEmptyLines[0]
    # Match property tabular: users[2]{id,name,role}:
    if ($firstLine -match '^(\w+)\[(\d+)\](\{.*\})?:\s*(.*)$') {
        $prop = $matches[1]
        $length = [int]$matches[2]
        $fields = if ($matches[3]) { $matches[3].Trim('{','}').Split(',') | ForEach-Object { $_.Trim() } } else { $null }
        $inline = $matches[4]

        $array = @()
        if ($inline) {
            $array = $inline.Split(',') | ForEach-Object { Read-Primitive -Token $_.Trim() }
        } else {
            $rowLines = $nonEmptyLines | Select-Object -Skip 1
            foreach ($rowLine in $rowLines) {
                if ($fields) {
                    # Tabular
                    $values = $rowLine.Trim().Split(',')
                    $obj = [ordered]@{}
                    for ($k = 0; $k -lt $fields.Count; $k++) {
                        $obj[$fields[$k]] = Read-Primitive -Token $values[$k].Trim()
                    }
                    $array += $obj
                } else {
                    # List item
                    if ($rowLine.Trim() -match '^- (.+)$') {
                        $array += Read-Primitive -Token $matches[1]
                    }
                }
            }
        }
        $obj = [ordered]@{}
        $obj[$prop] = $array
        return $obj
    }
    elseif ($firstLine -match '^\[(\d+)\](\{.*\})?:\s*(.*)$') {
        # Root array
        $length = [int]$matches[1]
        $fields = if ($matches[2]) { $matches[2].Trim('{','}').Split(',') | ForEach-Object { $_.Trim() } } else { $null }
        $inline = $matches[3]

        $array = @()
        if ($inline) {
            $array = $inline.Split(',') | ForEach-Object { Read-Primitive -Token $_.Trim() }
        } else {
            $rowLines = $nonEmptyLines | Select-Object -Skip 1
            foreach ($rowLine in $rowLines) {
                if ($fields) {
                    # Tabular
                    $values = $rowLine.Trim().Split(',')
                    $obj = [ordered]@{}
                    for ($k = 0; $k -lt $fields.Count; $k++) {
                        $obj[$fields[$k]] = Read-Primitive -Token $values[$k].Trim()
                    }
                    $array += $obj
                } else {
                    # List item
                    if ($rowLine.Trim() -match '^- (.+)$') {
                        $array += Read-Primitive -Token $matches[1]
                    }
                }
            }
        }
        return $array
    } else {
        # Root object
        $obj = [ordered]@{}
        foreach ($line in $nonEmptyLines) {
            if ($line -match '^(\w+):\s*(.*)$') {
                $key = $matches[1]
                $val = $matches[2]
                $obj[$key] = Read-Primitive -Token $val
            }
        }
        return $obj
    }
}

function Get-LineDepth {
    param([string]$Line, [int]$IndentSize)
    $leadingSpaces = ($Line -match '^(\s*)')[1].Length
    return $leadingSpaces / $IndentSize
}

function Read-Primitive {
    param([string]$Token)

    $token = $Token.Trim()
    if ($token -eq 'null') { return $null }
    if ($token -eq 'true') { return $true }
    if ($token -eq 'false') { return $false }
    if ($token -match '^".*"$') {
        return $token.Trim('"').Replace('\\n', "`n").Replace('\\r', "`r").Replace('\\t', "`t").Replace('\\"', '"').Replace('\\\\', '\')
    }
    if ($token -match '^-?\d+(\.\d+)?$') { return [double]$token }
    return $token
}

function Convert-ExpandedPath {
    param([object]$Value, [bool]$Strict)

    # Simple expansion for dotted keys
    if ($Value -is [System.Collections.IDictionary]) {
        $expanded = [ordered]@{}
        foreach ($key in $Value.Keys) {
            if ($key -match '^\w+\.\w+$') {
                $parts = $key.Split('.')
                if (-not $expanded.Contains($parts[0])) {
                    $expanded[$parts[0]] = [ordered]@{}
                }
                $expanded[$parts[0]][$parts[1]] = Convert-ExpandedPath -Value $Value[$key] -Strict $Strict
            } else {
                $expanded[$key] = Convert-ExpandedPath -Value $Value[$key] -Strict $Strict
            }
        }
        return $expanded
    }
    elseif ($Value -is [System.Collections.IList]) {
        return $Value | ForEach-Object { Convert-ExpandedPath -Value $_ -Strict $Strict }
    }
    else {
        return $Value
    }
}