Lua.psm1
|
[CmdletBinding()] param() $baseName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath) $script:PSModuleInfo = Import-PowerShellDataFile -Path "$PSScriptRoot\$baseName.psd1" $script:PSModuleInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ } $scriptName = $script:PSModuleInfo.Name Write-Debug "[$scriptName] - Importing module" #region [functions] - [private] Write-Debug "[$scriptName] - [functions] - [private] - Processing folder" #region [functions] - [private] - [ConvertFrom-LuaTable] Write-Debug "[$scriptName] - [functions] - [private] - [ConvertFrom-LuaTable] - Importing" function ConvertFrom-LuaTable { <# .SYNOPSIS Parses a Lua table constructor string into a PowerShell object. .DESCRIPTION Takes a Lua table constructor string and converts it to PowerShell hashtables, arrays, and primitive types. This is the internal parsing engine used by ConvertFrom-Lua. #> [OutputType([object])] [CmdletBinding()] param( # The Lua table string to parse. [Parameter(Mandatory)] [string] $InputString, # Whether to output PSCustomObjects instead of hashtables. [Parameter()] [switch] $AsPSCustomObject, # Maximum allowed nesting depth. [Parameter()] [int] $MaxDepth = 1024 ) begin {} process { $script:luaString = $InputString $script:luaPos = 0 $script:luaAsPSCustomObject = $AsPSCustomObject.IsPresent $script:luaMaxDepth = $MaxDepth $script:luaCurrentDepth = 0 Skip-LuaWhitespace # Skip optional leading 'return' keyword (common in Lua data files) if ($script:luaPos + 6 -le $script:luaString.Length -and $script:luaString.Substring($script:luaPos, 6) -ceq 'return') { $nextPos = $script:luaPos + 6 if ($nextPos -ge $script:luaString.Length -or $script:luaString[$nextPos] -match '[\s{]') { $script:luaPos = $nextPos Skip-LuaWhitespace } } $result = Read-LuaValue Skip-LuaWhitespace if ($script:luaPos -lt $script:luaString.Length) { $remainingInput = $script:luaString.Substring($script:luaPos) throw "Unexpected trailing content after Lua value at position $($script:luaPos): $remainingInput" } return $result } end {} } Write-Debug "[$scriptName] - [functions] - [private] - [ConvertFrom-LuaTable] - Done" #endregion [functions] - [private] - [ConvertFrom-LuaTable] #region [functions] - [private] - [ConvertTo-LuaTable] Write-Debug "[$scriptName] - [functions] - [private] - [ConvertTo-LuaTable] - Importing" function ConvertTo-LuaTable { <# .SYNOPSIS Converts a PowerShell object to a Lua table string representation. .DESCRIPTION Recursively converts a PowerShell object (hashtable, array, PSCustomObject, or primitive) into a Lua table constructor string. This is the internal serialization engine used by ConvertTo-Lua. Uses fixed 4-space indentation per the Lua community convention. Properties with $null values are omitted (Lua nil-means-absent semantics). #> [OutputType([string])] [CmdletBinding()] param( # The object to convert to a Lua table string. [Parameter(Mandatory)] [AllowNull()] [object] $InputObject, # The current recursion depth. [Parameter()] [int] $CurrentDepth = 0, # Maximum allowed recursion depth. [Parameter()] [int] $MaxDepth = 2, # Whether to compress the output (no newlines or indentation). [Parameter()] [switch] $Compress, # Serialize enum values as their string name instead of numeric value. [Parameter()] [switch] $EnumsAsStrings ) begin { $indent = if ($Compress) { '' } else { ' ' * (4 * $CurrentDepth) } $childIndent = if ($Compress) { '' } else { ' ' * (4 * ($CurrentDepth + 1)) } $newline = if ($Compress) { '' } else { "`n" } $separator = if ($Compress) { ',' } else { ",`n" } } process { if ($null -eq $InputObject) { return 'nil' } if ($InputObject -is [bool]) { if ($InputObject) { return 'true' } else { return 'false' } } # Enum handling if ($InputObject -is [enum]) { if ($EnumsAsStrings) { $escaped = $InputObject.ToString() -replace '\\', '\\\\' -replace '"', '\"' return "`"$escaped`"" } return ([int]$InputObject).ToString([System.Globalization.CultureInfo]::InvariantCulture) } if ($InputObject -is [int] -or $InputObject -is [long] -or $InputObject -is [int16] -or $InputObject -is [int64] -or $InputObject -is [uint16] -or $InputObject -is [uint32] -or $InputObject -is [uint64] -or $InputObject -is [byte] -or $InputObject -is [sbyte]) { return $InputObject.ToString([System.Globalization.CultureInfo]::InvariantCulture) } if ($InputObject -is [float] -or $InputObject -is [double] -or $InputObject -is [decimal] -or $InputObject -is [single]) { return $InputObject.ToString([System.Globalization.CultureInfo]::InvariantCulture) } if ($InputObject -is [string]) { $escaped = $InputObject ` -replace '\\', '\\' ` -replace '"', '\"' ` -replace "`0", '\0' ` -replace "`a", '\a' ` -replace "`b", '\b' ` -replace "`f", '\f' ` -replace "`n", '\n' ` -replace "`r", '\r' ` -replace "`t", '\t' ` -replace "`v", '\v' return "`"$escaped`"" } # Depth check for complex types if ($CurrentDepth -ge $MaxDepth) { Write-Warning "Depth limit ($MaxDepth) exceeded. Serializing remaining object as string." $str = $InputObject.ToString() ` -replace '\\', '\\\\' ` -replace '"', '\"' return "`"$str`"" } if ($InputObject -is [System.Collections.IList]) { if ($InputObject.Count -eq 0) { return '{}' } $items = [System.Collections.Generic.List[string]]::new() foreach ($item in $InputObject) { $childParams = @{ InputObject = $item CurrentDepth = $CurrentDepth + 1 MaxDepth = $MaxDepth Compress = $Compress EnumsAsStrings = $EnumsAsStrings } $value = ConvertTo-LuaTable @childParams $items.Add("$childIndent$value") } return "{$newline$($items -join $separator)$newline$indent}" } # Handle hashtables and ordered dictionaries if ($InputObject -is [System.Collections.IDictionary]) { if ($InputObject.Count -eq 0) { return '{}' } $entries = [System.Collections.Generic.List[string]]::new() foreach ($key in $InputObject.Keys) { $val = $InputObject[$key] # Omit $null values per Lua nil-means-absent semantics if ($null -eq $val) { continue } $value = ConvertTo-LuaTable -InputObject $val ` -CurrentDepth ($CurrentDepth + 1) ` -MaxDepth $MaxDepth ` -Compress:$Compress ` -EnumsAsStrings:$EnumsAsStrings $luaKey = Format-LuaKey -Key ([string]$key) $space = if ($Compress) { '' } else { ' ' } $entries.Add("$childIndent$luaKey$space=${space}$value") } if ($entries.Count -eq 0) { return '{}' } return "{$newline$($entries -join $separator)$newline$indent}" } # Handle PSCustomObject if ($InputObject -is [psobject]) { $properties = $InputObject.PSObject.Properties | Where-Object { $_.MemberType -eq 'NoteProperty' } if (-not $properties) { return '{}' } $entries = [System.Collections.Generic.List[string]]::new() foreach ($prop in $properties) { # Omit $null values per Lua nil-means-absent semantics if ($null -eq $prop.Value) { continue } $value = ConvertTo-LuaTable -InputObject $prop.Value ` -CurrentDepth ($CurrentDepth + 1) ` -MaxDepth $MaxDepth ` -Compress:$Compress ` -EnumsAsStrings:$EnumsAsStrings $luaKey = Format-LuaKey -Key $prop.Name $space = if ($Compress) { '' } else { ' ' } $entries.Add("$childIndent$luaKey$space=${space}$value") } if ($entries.Count -eq 0) { return '{}' } return "{$newline$($entries -join $separator)$newline$indent}" } # Fallback: convert to string $escaped = ($InputObject.ToString()) -replace '\\', '\\\\' -replace '"', '\"' return "`"$escaped`"" } end {} } Write-Debug "[$scriptName] - [functions] - [private] - [ConvertTo-LuaTable] - Done" #endregion [functions] - [private] - [ConvertTo-LuaTable] #region [functions] - [private] - [Format-LuaKey] Write-Debug "[$scriptName] - [functions] - [private] - [Format-LuaKey] - Importing" function Format-LuaKey { <# .SYNOPSIS Formats a string as a valid Lua table key. .DESCRIPTION Returns the key as a bare identifier if it matches Lua identifier rules and is not a reserved word, otherwise wraps it in bracket-quote notation: ["key"]. #> [OutputType([string])] [CmdletBinding()] param( # The key string to format. [Parameter(Mandatory)] [string] $Key ) begin { # Lua 5.4 reserved words per §3.1 $reservedWords = @( 'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while' ) } process { if ($Key -match '^[a-zA-Z_][a-zA-Z0-9_]*$' -and $Key -notin $reservedWords) { return $Key } $escaped = $Key -replace '\\', '\\\\' -replace '"', '\"' return "[`"$escaped`"]" } end {} } Write-Debug "[$scriptName] - [functions] - [private] - [Format-LuaKey] - Done" #endregion [functions] - [private] - [Format-LuaKey] #region [functions] - [private] - [Read-LuaHexFloat] Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaHexFloat] - Importing" function Read-LuaHexFloat { <# .SYNOPSIS Parses a Lua hex float string (e.g. 0x1.fp10) to a double. #> [OutputType([double])] [CmdletBinding()] param( [Parameter(Mandatory)] [string] $HexString ) begin {} process { $isNegative = $HexString.StartsWith('-') $str = if ($isNegative) { $HexString.Substring(3) } else { $HexString.Substring(2) } $parts = $str -split '[pP]' $mantissaStr = $parts[0] $exponent = if ($parts.Length -gt 1) { [int]$parts[1] } else { 0 } $mantissaParts = $mantissaStr -split '\.' $intPart = if ($mantissaParts[0]) { [Convert]::ToInt64($mantissaParts[0], 16) } else { 0 } $fracValue = 0.0 if ($mantissaParts.Length -gt 1 -and $mantissaParts[1]) { $fracStr = $mantissaParts[1] for ($i = 0; $i -lt $fracStr.Length; $i++) { $digitVal = [Convert]::ToInt32( $fracStr[$i].ToString(), 16 ) $fracValue += $digitVal * [Math]::Pow( 16, - ($i + 1) ) } } $result = ($intPart + $fracValue) * [Math]::Pow(2, $exponent) if ($isNegative) { $result = (-$result) } return $result } end {} } Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaHexFloat] - Done" #endregion [functions] - [private] - [Read-LuaHexFloat] #region [functions] - [private] - [Read-LuaMultiLineString] Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaMultiLineString] - Importing" function Read-LuaMultiLineString { <# .SYNOPSIS Reads a multi-line Lua string delimited by long brackets [[ ]], [=[ ]=], [==[ ]==], etc. #> [OutputType([string])] [CmdletBinding()] param() begin {} process { # Count the number of '=' characters in the opening long bracket $script:luaPos++ # skip first [ $equalsCount = 0 while ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq '=') { $equalsCount++ $script:luaPos++ } if ($script:luaPos -ge $script:luaString.Length -or $script:luaString[$script:luaPos] -ne '[') { throw 'Invalid long bracket string opening.' } $script:luaPos++ # skip second [ # Build the closing pattern: ] + N '=' + ] $closingBracket = ']' + ('=' * $equalsCount) + ']' $closeLen = $closingBracket.Length $result = [System.Text.StringBuilder]::new() # Per Lua spec, a newline immediately after the opening bracket is ignored if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq "`n") { $script:luaPos++ } elseif ($script:luaPos + 1 -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq "`r" -and $script:luaString[$script:luaPos + 1] -eq "`n") { $script:luaPos += 2 } while ($script:luaPos -lt $script:luaString.Length) { if ($script:luaPos + $closeLen - 1 -lt $script:luaString.Length -and $script:luaString.Substring($script:luaPos, $closeLen) -eq $closingBracket) { $script:luaPos += $closeLen return $result.ToString() } $null = $result.Append($script:luaString[$script:luaPos]) $script:luaPos++ } throw 'Unterminated multi-line string.' } end {} } Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaMultiLineString] - Done" #endregion [functions] - [private] - [Read-LuaMultiLineString] #region [functions] - [private] - [Read-LuaNumber] Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaNumber] - Importing" function Read-LuaNumber { <# .SYNOPSIS Reads a Lua number (integer, float, hex, hex float, scientific notation). #> [OutputType([int])] [OutputType([long])] [OutputType([double])] [CmdletBinding()] param() begin {} process { $start = $script:luaPos $isFloat = $false $isHex = $false if ($script:luaString[$script:luaPos] -eq '-') { $script:luaPos++ } # Hex number if ($script:luaPos + 1 -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq '0' -and $script:luaString[$script:luaPos + 1] -match '[xX]') { $isHex = $true $script:luaPos += 2 while ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -match '[0-9a-fA-F]') { $script:luaPos++ } # Hex float fractional part if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq '.') { $isFloat = $true $script:luaPos++ while ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -match '[0-9a-fA-F]') { $script:luaPos++ } } # Hex float exponent (p/P) if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -match '[pP]') { $isFloat = $true $script:luaPos++ if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -match '[+-]') { $script:luaPos++ } while ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -match '[0-9]') { $script:luaPos++ } } } else { while ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -match '[0-9]') { $script:luaPos++ } if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq '.') { $isFloat = $true $script:luaPos++ while ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -match '[0-9]') { $script:luaPos++ } } # Scientific notation if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -match '[eE]') { $isFloat = $true $script:luaPos++ if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -match '[+-]') { $script:luaPos++ } while ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -match '[0-9]') { $script:luaPos++ } } } $numStr = $script:luaString.Substring( $start, $script:luaPos - $start ) if ($isFloat) { if ($isHex) { # Hex float like 0x1.fp10 - parse manually return [double](Read-LuaHexFloat -HexString $numStr) } return [double]::Parse( $numStr, [System.Globalization.CultureInfo]::InvariantCulture ) } if ($isHex) { $isNegative = $numStr.StartsWith('-') $hexPart = if ($isNegative) { $numStr.Substring(3) } else { $numStr.Substring(2) } $longVal = [Convert]::ToInt64($hexPart, 16) if ($isNegative) { $longVal = (-$longVal) } if ($longVal -ge [int]::MinValue -and $longVal -le [int]::MaxValue) { return [int]$longVal } return $longVal } $longValue = [long]0 if ([long]::TryParse($numStr, [ref]$longValue)) { if ($longValue -ge [int]::MinValue -and $longValue -le [int]::MaxValue) { return [int]$longValue } return $longValue } return [double]::Parse( $numStr, [System.Globalization.CultureInfo]::InvariantCulture ) } end {} } Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaNumber] - Done" #endregion [functions] - [private] - [Read-LuaNumber] #region [functions] - [private] - [Read-LuaString] Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaString] - Importing" function Read-LuaString { <# .SYNOPSIS Reads a quoted Lua string with escape sequence support per Lua 5.4 §3.1. #> [OutputType([string])] [CmdletBinding()] param( [Parameter(Mandatory)] [char] $QuoteChar ) begin {} process { $script:luaPos++ # skip opening quote $result = [System.Text.StringBuilder]::new() while ($script:luaPos -lt $script:luaString.Length) { $char = $script:luaString[$script:luaPos] if ($char -eq '\') { $script:luaPos++ if ($script:luaPos -ge $script:luaString.Length) { throw 'Unexpected end of string after escape character.' } $nextChar = $script:luaString[$script:luaPos] switch ($nextChar) { 'a' { $null = $result.Append([char]7) $script:luaPos++ } 'b' { $null = $result.Append("`b") $script:luaPos++ } 'f' { $null = $result.Append([char]12) $script:luaPos++ } 'n' { $null = $result.Append("`n") $script:luaPos++ } 'r' { $null = $result.Append("`r") $script:luaPos++ } 't' { $null = $result.Append("`t") $script:luaPos++ } 'v' { $null = $result.Append([char]11) $script:luaPos++ } '\' { $null = $result.Append('\') $script:luaPos++ } '"' { $null = $result.Append('"') $script:luaPos++ } "'" { $null = $result.Append("'") $script:luaPos++ } 'x' { # \xXX - two hex digits $script:luaPos++ if ($script:luaPos + 1 -lt $script:luaString.Length) { $hexStr = $script:luaString.Substring( $script:luaPos, 2 ) $hexVal = [Convert]::ToInt32($hexStr, 16) $null = $result.Append([char]$hexVal) $script:luaPos += 2 } else { throw 'Invalid \x escape sequence.' } } 'u' { # \u{XXXX} - Unicode code point $script:luaPos++ if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq '{') { $script:luaPos++ $hexStart = $script:luaPos while ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -ne '}') { $script:luaPos++ } $hexStr = $script:luaString.Substring( $hexStart, $script:luaPos - $hexStart ) $codePoint = [Convert]::ToInt32($hexStr, 16) $null = $result.Append( [char]::ConvertFromUtf32($codePoint) ) $script:luaPos++ # skip } } else { throw 'Invalid \u escape sequence.' } } "`n" { $null = $result.Append("`n") $script:luaPos++ if ( $script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq "`r" ) { $script:luaPos++ } } "`r" { $null = $result.Append("`n") $script:luaPos++ if ( $script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq "`n" ) { $script:luaPos++ } } 'z' { $script:luaPos++ while ( $script:luaPos -lt $script:luaString.Length -and [char]::IsWhiteSpace($script:luaString[$script:luaPos]) ) { $script:luaPos++ } } default { # \ddd - decimal byte sequence (1-3 digits) if ($nextChar -match '[0-9]') { $numStr = $nextChar.ToString() $script:luaPos++ for ($d = 0; $d -lt 2; $d++) { if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -match '[0-9]') { $numStr += $script:luaString[$script:luaPos] $script:luaPos++ } else { break } } $null = $result.Append([char][int]$numStr) } else { # Unknown escape - just pass through $null = $result.Append($nextChar) $script:luaPos++ } } } continue } if ($char -eq $QuoteChar) { $script:luaPos++ # skip closing quote return $result.ToString() } $null = $result.Append($char) $script:luaPos++ } throw 'Unterminated string literal.' } end {} } Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaString] - Done" #endregion [functions] - [private] - [Read-LuaString] #region [functions] - [private] - [Read-LuaTable] Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaTable] - Importing" function Read-LuaTable { <# .SYNOPSIS Reads a Lua table constructor and returns an array, hashtable, or PSCustomObject. #> [OutputType([object[]])] [OutputType([System.Collections.Specialized.OrderedDictionary])] [OutputType([pscustomobject])] [CmdletBinding()] param() begin {} process { $script:luaCurrentDepth++ if ($script:luaCurrentDepth -gt $script:luaMaxDepth) { throw "Maximum nesting depth ($($script:luaMaxDepth)) exceeded." } $script:luaPos++ # skip { Skip-LuaWhitespace $entries = [System.Collections.Generic.List[object]]::new() $arrayValues = [System.Collections.Generic.List[object]]::new() $hasStringKeys = $false $hasArrayValues = $false while ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -ne '}') { Skip-LuaWhitespace if ($script:luaPos -ge $script:luaString.Length -or $script:luaString[$script:luaPos] -eq '}') { break } # Check for bracket key: ["key"] = value or [expr] = value if ($script:luaString[$script:luaPos] -eq '[' -and $script:luaPos + 1 -lt $script:luaString.Length -and $script:luaString[$script:luaPos + 1] -ne '[') { $script:luaPos++ # skip [ Skip-LuaWhitespace $key = Read-LuaValue Skip-LuaWhitespace if ($script:luaPos -ge $script:luaString.Length -or $script:luaString[$script:luaPos] -ne ']') { throw "Expected ']' after bracket key in Lua table." } $script:luaPos++ # skip ] Skip-LuaWhitespace if ($script:luaPos -ge $script:luaString.Length -or $script:luaString[$script:luaPos] -ne '=') { throw "Expected '=' after bracket key in Lua table." } $script:luaPos++ # skip = Skip-LuaWhitespace $value = Read-LuaValue $entries.Add(@{ Key = [string]$key; Value = $value }) $hasStringKeys = $true } elseif ($script:luaString[$script:luaPos] -match '[a-zA-Z_]') { # Check for identifier key: key = value $identStart = $script:luaPos while ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -match '[a-zA-Z0-9_]') { $script:luaPos++ } $ident = $script:luaString.Substring( $identStart, $script:luaPos - $identStart ) Skip-LuaWhitespace if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq '=') { # Key = value pair $script:luaPos++ # skip = Skip-LuaWhitespace $value = Read-LuaValue $entries.Add(@{ Key = $ident Value = $value }) $hasStringKeys = $true } else { # Bare identifier as keyword value switch ($ident) { 'true' { $arrayValues.Add($true) } 'false' { $arrayValues.Add($false) } 'nil' { $arrayValues.Add($null) } default { throw "Unexpected bare identifier '$ident'." } } $hasArrayValues = $true } } else { # Array value $value = Read-LuaValue $arrayValues.Add($value) $hasArrayValues = $true } Skip-LuaWhitespace # Lua requires a comma or semicolon between fields unless the next token is } if ($script:luaPos -lt $script:luaString.Length) { if ($script:luaString[$script:luaPos] -eq ',' -or $script:luaString[$script:luaPos] -eq ';') { $script:luaPos++ } elseif ($script:luaString[$script:luaPos] -ne '}') { throw "Expected ',', ';', or '}' in Lua table constructor." } } } if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq '}') { $script:luaPos++ # skip } } $script:luaCurrentDepth-- # Pure array (no string keys) if ($hasArrayValues -and -not $hasStringKeys) { return , [object[]]$arrayValues.ToArray() } # Empty table if (-not $hasArrayValues -and -not $hasStringKeys) { if ($script:luaAsPSCustomObject) { return [pscustomobject]@{} } return [ordered]@{} } # Build ordered hashtable (or PSCustomObject) $table = [ordered]@{} foreach ($entry in $entries) { $table[$entry.Key] = $entry.Value } # Mixed table: sequential values get integer keys starting at 1 $arrayIndex = 1 foreach ($val in $arrayValues) { $table[[string]$arrayIndex] = $val $arrayIndex++ } if ($script:luaAsPSCustomObject) { return [pscustomobject]$table } return $table } end {} } Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaTable] - Done" #endregion [functions] - [private] - [Read-LuaTable] #region [functions] - [private] - [Read-LuaValue] Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaValue] - Importing" function Read-LuaValue { <# .SYNOPSIS Reads a single Lua value from the current parser position. #> [OutputType([object])] [OutputType([bool])] [OutputType([string])] [OutputType([int])] [OutputType([long])] [OutputType([double])] [CmdletBinding()] param() begin {} process { Skip-LuaWhitespace if ($script:luaPos -ge $script:luaString.Length) { return $null } $char = $script:luaString[$script:luaPos] # Table if ($char -eq '{') { return Read-LuaTable } # String (double-quoted) if ($char -eq '"') { return Read-LuaString -QuoteChar '"' } # String (single-quoted) if ($char -eq "'") { return Read-LuaString -QuoteChar "'" } # Multi-line string [[ ... ]] or [=[ ... ]=] if ($char -eq '[' -and $script:luaPos + 1 -lt $script:luaString.Length -and ($script:luaString[$script:luaPos + 1] -eq '[' -or $script:luaString[$script:luaPos + 1] -eq '=')) { return Read-LuaMultiLineString } # Number or negative number (including .5 style floats) if ($char -match '[0-9]' -or ($char -eq '.' -and $script:luaPos + 1 -lt $script:luaString.Length -and $script:luaString[$script:luaPos + 1] -match '[0-9]') -or ($char -eq '-' -and $script:luaPos + 1 -lt $script:luaString.Length -and $script:luaString[$script:luaPos + 1] -match '[0-9.]')) { return Read-LuaNumber } # Keywords and bare identifiers if ($char -match '[a-zA-Z_]') { $identStart = $script:luaPos while ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -match '[a-zA-Z0-9_]') { $script:luaPos++ } $ident = $script:luaString.Substring( $identStart, $script:luaPos - $identStart ) switch ($ident) { 'true' { return $true } 'false' { return $false } 'nil' { return $null } default { throw "Unexpected bare identifier '$ident' at position $identStart." } } } throw "Unexpected character '$char' at position $($script:luaPos)." } end {} } Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaValue] - Done" #endregion [functions] - [private] - [Read-LuaValue] #region [functions] - [private] - [Skip-LuaWhitespace] Write-Debug "[$scriptName] - [functions] - [private] - [Skip-LuaWhitespace] - Importing" function Skip-LuaWhitespace { <# .SYNOPSIS Advances the parser position past whitespace and comments. #> [CmdletBinding()] param() begin {} process { while ($script:luaPos -lt $script:luaString.Length) { $char = $script:luaString[$script:luaPos] # Skip whitespace if ($char -match '\s') { $script:luaPos++ continue } # Skip comments if ($script:luaPos + 1 -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq '-' -and $script:luaString[$script:luaPos + 1] -eq '-') { $script:luaPos += 2 # Multi-line comment --[[ ... ]] or --[=[ ... ]=] etc. if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq '[') { $eqStart = $script:luaPos + 1 $eqCount = 0 while ($eqStart + $eqCount -lt $script:luaString.Length -and $script:luaString[$eqStart + $eqCount] -eq '=') { $eqCount++ } if ($eqStart + $eqCount -lt $script:luaString.Length -and $script:luaString[$eqStart + $eqCount] -eq '[') { # Valid long bracket comment opening $script:luaPos = $eqStart + $eqCount + 1 $closePattern = ']' + ('=' * $eqCount) + ']' $closeLen = $closePattern.Length while ($script:luaPos -lt $script:luaString.Length) { if ($script:luaPos + $closeLen - 1 -lt $script:luaString.Length -and $script:luaString.Substring($script:luaPos, $closeLen) -eq $closePattern) { $script:luaPos += $closeLen break } $script:luaPos++ } } else { # Not a long bracket - treat as single-line comment while ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -ne "`n") { $script:luaPos++ } } } else { # Single-line comment while ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -ne "`n") { $script:luaPos++ } } continue } break } } end {} } Write-Debug "[$scriptName] - [functions] - [private] - [Skip-LuaWhitespace] - Done" #endregion [functions] - [private] - [Skip-LuaWhitespace] Write-Debug "[$scriptName] - [functions] - [private] - Done" #endregion [functions] - [private] #region [functions] - [public] Write-Debug "[$scriptName] - [functions] - [public] - Processing folder" #region [functions] - [public] - [Lua] Write-Debug "[$scriptName] - [functions] - [public] - [Lua] - Processing folder" #region [functions] - [public] - [Lua] - [ConvertFrom-Lua] Write-Debug "[$scriptName] - [functions] - [public] - [Lua] - [ConvertFrom-Lua] - Importing" function ConvertFrom-Lua { <# .SYNOPSIS Converts a Lua table constructor string to a PowerShell object. .DESCRIPTION Takes a Lua table constructor string and parses it into PowerShell objects. By default, Lua tables with string keys become PSCustomObjects and Lua sequences become arrays. Use -AsHashtable to get ordered hashtables instead. Supports the following Lua to PowerShell type mappings: - Lua table (key = value) -> [PSCustomObject] or [ordered] hashtable - Lua sequence (array) -> [object[]] - Lua double-quoted string -> [string] - Lua single-quoted string -> [string] - Lua multi-line string [[ ]] -> [string] - Lua number (integer) -> [int] or [long] - Lua number (float) -> [double] - Lua boolean (true/false) -> [bool] - nil -> $null - Single-line comments (--) -> Ignored - Multi-line comments (--[[ ]]) -> Ignored .EXAMPLE ```powershell '{ name = "Alice", age = 30 }' | ConvertFrom-Lua name age ---- --- Alice 30 ``` .EXAMPLE ```powershell ConvertFrom-Lua -InputObject '{ 1, 2, 3 }' 1 2 3 ``` .EXAMPLE ```powershell '{ name = "Alice" }' | ConvertFrom-Lua -AsHashtable Name Value ---- ----- name Alice ``` .NOTES [Lua 5.4 Reference Manual - Table Constructors](https://www.lua.org/manual/5.4/manual.html#3.4.9) .LINK https://psmodule.io/Lua/Functions/ConvertFrom-Lua/ .LINK https://www.lua.org/manual/5.4/manual.html#3.4.9 #> [OutputType([object])] [OutputType([System.Array])] [CmdletBinding()] param( # The Lua table constructor string to convert to a PowerShell object. [Parameter(Mandatory, Position = 0, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string] $InputObject, # Output ordered hashtables instead of PSCustomObjects for Lua tables with string keys. [Parameter()] [switch] $AsHashtable, # Max nesting depth allowed in input. Throws a terminating error when exceeded. [Parameter()] [ValidateRange(0, 1024)] [int] $Depth = 1024, # Output arrays as a single object instead of enumerating elements through the pipeline. [Parameter()] [switch] $NoEnumerate ) begin {} process { $result = ConvertFrom-LuaTable -InputString $InputObject -AsPSCustomObject:(-not $AsHashtable) -MaxDepth $Depth if ($NoEnumerate) { , $result } else { $result } } end {} } Write-Debug "[$scriptName] - [functions] - [public] - [Lua] - [ConvertFrom-Lua] - Done" #endregion [functions] - [public] - [Lua] - [ConvertFrom-Lua] #region [functions] - [public] - [Lua] - [ConvertTo-Lua] Write-Debug "[$scriptName] - [functions] - [public] - [Lua] - [ConvertTo-Lua] - Importing" function ConvertTo-Lua { <# .SYNOPSIS Converts a PowerShell object to a Lua table constructor string. .DESCRIPTION Takes a PowerShell object (hashtable, PSCustomObject, array, or primitive value) and converts it to a Lua table constructor string representation. Nested structures are recursively converted with 4-space indentation. Supports the following type mappings: - [hashtable] / [ordered] -> Lua table with key = value pairs - [PSCustomObject] -> Lua table with key = value pairs - [array] -> Lua table (sequence) - [string] -> Lua double-quoted string with escape sequences - [int] / [long] -> Lua integer - [float] / [double] -> Lua float - [bool] -> Lua boolean (true/false) - $null -> omitted (nil means absent in Lua) .EXAMPLE ```powershell @{ name = "Alice"; age = 30 } | ConvertTo-Lua { age = 30, name = "Alice" } ``` .EXAMPLE ```powershell ConvertTo-Lua -InputObject @(1, 2, 3) -Compress {1,2,3} ``` .EXAMPLE ```powershell "hello" | ConvertTo-Lua -AsArray { "hello" } ``` .NOTES [Lua 5.4 Reference Manual - Table Constructors](https://www.lua.org/manual/5.4/manual.html#3.4.9) .LINK https://psmodule.io/Lua/Functions/ConvertTo-Lua/ .LINK https://www.lua.org/manual/5.4/manual.html#3.4.9 #> [OutputType([string])] [CmdletBinding()] param( # The object to convert to a Lua table constructor string. [Parameter(Mandatory, Position = 0, ValueFromPipeline)] [AllowNull()] [object] $InputObject, # Max recursion depth for nested object serialization. Emits a warning when exceeded. [Parameter()] [ValidateRange(0, 100)] [int] $Depth = 2, # Omit whitespace and indentation. [Parameter()] [switch] $Compress, # Serialize PowerShell enum values as their string name instead of numeric value. [Parameter()] [switch] $EnumsAsStrings, # Always wrap output in a Lua sequence table, even for a single value. [Parameter()] [switch] $AsArray ) begin {} process { $objectToConvert = $InputObject if ($AsArray -and $InputObject -isnot [System.Collections.IList]) { $objectToConvert = @(, $InputObject) } ConvertTo-LuaTable -InputObject $objectToConvert ` -CurrentDepth 0 ` -MaxDepth $Depth ` -Compress:$Compress ` -EnumsAsStrings:$EnumsAsStrings } end {} } Write-Debug "[$scriptName] - [functions] - [public] - [Lua] - [ConvertTo-Lua] - Done" #endregion [functions] - [public] - [Lua] - [ConvertTo-Lua] Write-Debug "[$scriptName] - [functions] - [public] - [Lua] - Done" #endregion [functions] - [public] - [Lua] Write-Debug "[$scriptName] - [functions] - [public] - Done" #endregion [functions] - [public] #region Member exporter $exports = @{ Alias = '*' Cmdlet = '' Function = @( 'ConvertFrom-Lua' 'ConvertTo-Lua' ) Variable = '' } Export-ModuleMember @exports #endregion Member exporter |