From 8ef5a874ffffd506cc5c70a742e45dc3cbbcbbfc Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 13 Apr 2026 15:13:43 +0200 Subject: [PATCH 01/24] Add Lua conversion functions and corresponding tests - Implemented ConvertTo-Lua and ConvertFrom-Lua functions for converting PowerShell objects to Lua table strings and vice versa. - Added helper functions ConvertTo-LuaTable and Format-LuaKey for internal processing. - Created comprehensive tests for both conversion functions, covering various data types including primitives, strings, arrays, and tables. - Included test data files for validation of string and array conversions. - Ensured proper handling of comments and special characters in Lua syntax. --- README.md | 81 ++- examples/General.ps1 | 46 +- src/assemblies/LsonLib.dll | Bin 43520 -> 0 bytes src/classes/private/SecretWriter.ps1 | 15 - src/classes/public/Book.ps1 | 147 ------ src/data/Config.psd1 | 3 - src/data/Settings.psd1 | 3 - src/finally.ps1 | 3 - src/formats/CultureInfo.Format.ps1xml | 37 -- src/formats/Mygciview.Format.ps1xml | 65 --- .../private/ConvertFrom-LuaTable.ps1 | 443 ++++++++++++++++ src/functions/private/ConvertTo-LuaTable.ps1 | 115 ++++ src/functions/private/Format-LuaKey.ps1 | 29 ++ .../private/Get-InternalPSModule.ps1 | 18 - .../private/Set-InternalPSModule.ps1 | 22 - src/functions/public/Lua/ConvertFrom-Lua.ps1 | 82 +++ src/functions/public/Lua/ConvertTo-Lua.ps1 | 82 +++ .../public/PSModule/Get-PSModuleTest.ps1 | 26 - .../public/PSModule/New-PSModuleTest.ps1 | 40 -- src/functions/public/PSModule/PSModule.md | 3 - .../public/SomethingElse/Set-PSModuleTest.ps1 | 25 - .../public/SomethingElse/SomethingElse.md | 1 - src/functions/public/Test-PSModuleTest.ps1 | 21 - src/functions/public/completers.ps1 | 8 - src/header.ps1 | 3 +- src/init/initializer.ps1 | 3 - src/manifest.psd1 | 3 +- src/modules/OtherPSModule.psm1 | 19 - src/scripts/loader.ps1 | 3 - src/types/DirectoryInfo.Types.ps1xml | 21 - src/types/FileInfo.Types.ps1xml | 14 - src/variables/private/PrivateVariables.ps1 | 47 -- src/variables/public/Moons.ps1 | 6 - src/variables/public/Planets.ps1 | 20 - src/variables/public/SolarSystems.ps1 | 17 - tests/Lua.Tests.ps1 | 490 ++++++++++++++++++ tests/PSModuleTest.Tests.ps1 | 25 - tests/data/Arrays.json | 26 + tests/data/Arrays.lua | 6 + tests/data/Strings.json | 7 + tests/data/Strings.lua | 7 + tests/data/TestStructure.json | 77 +++ tests/data/TestStructure.lua | 65 +++ 43 files changed, 1526 insertions(+), 648 deletions(-) delete mode 100644 src/assemblies/LsonLib.dll delete mode 100644 src/classes/private/SecretWriter.ps1 delete mode 100644 src/classes/public/Book.ps1 delete mode 100644 src/data/Config.psd1 delete mode 100644 src/data/Settings.psd1 delete mode 100644 src/finally.ps1 delete mode 100644 src/formats/CultureInfo.Format.ps1xml delete mode 100644 src/formats/Mygciview.Format.ps1xml create mode 100644 src/functions/private/ConvertFrom-LuaTable.ps1 create mode 100644 src/functions/private/ConvertTo-LuaTable.ps1 create mode 100644 src/functions/private/Format-LuaKey.ps1 delete mode 100644 src/functions/private/Get-InternalPSModule.ps1 delete mode 100644 src/functions/private/Set-InternalPSModule.ps1 create mode 100644 src/functions/public/Lua/ConvertFrom-Lua.ps1 create mode 100644 src/functions/public/Lua/ConvertTo-Lua.ps1 delete mode 100644 src/functions/public/PSModule/Get-PSModuleTest.ps1 delete mode 100644 src/functions/public/PSModule/New-PSModuleTest.ps1 delete mode 100644 src/functions/public/PSModule/PSModule.md delete mode 100644 src/functions/public/SomethingElse/Set-PSModuleTest.ps1 delete mode 100644 src/functions/public/SomethingElse/SomethingElse.md delete mode 100644 src/functions/public/Test-PSModuleTest.ps1 delete mode 100644 src/functions/public/completers.ps1 delete mode 100644 src/init/initializer.ps1 delete mode 100644 src/modules/OtherPSModule.psm1 delete mode 100644 src/scripts/loader.ps1 delete mode 100644 src/types/DirectoryInfo.Types.ps1xml delete mode 100644 src/types/FileInfo.Types.ps1xml delete mode 100644 src/variables/private/PrivateVariables.ps1 delete mode 100644 src/variables/public/Moons.ps1 delete mode 100644 src/variables/public/Planets.ps1 delete mode 100644 src/variables/public/SolarSystems.ps1 create mode 100644 tests/Lua.Tests.ps1 delete mode 100644 tests/PSModuleTest.Tests.ps1 create mode 100644 tests/data/Arrays.json create mode 100644 tests/data/Arrays.lua create mode 100644 tests/data/Strings.json create mode 100644 tests/data/Strings.lua create mode 100644 tests/data/TestStructure.json create mode 100644 tests/data/TestStructure.lua diff --git a/README.md b/README.md index 6319793..69f934c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ -# {{ NAME }} +# Lua -{{ DESCRIPTION }} +A PowerShell module for converting between PowerShell objects and Lua table notation. ## Prerequisites This uses the following external resources: + - The [PSModule framework](https://github.com/PSModule/Process-PSModule) for building, testing and publishing the module. ## Installation @@ -12,58 +13,94 @@ This uses the following external resources: To install the module from the PowerShell Gallery, you can use the following command: ```powershell -Install-PSResource -Name {{ NAME }} -Import-Module -Name {{ NAME }} +Install-PSResource -Name Lua +Import-Module -Name Lua ``` ## Usage -Here is a list of example that are typical use cases for the module. +Here is a list of examples that are typical use cases for the module. + +### Example 1: Convert a PowerShell hashtable to Lua + +```powershell +@{ name = "ElvUI"; version = "13.74"; enabled = $true } | ConvertTo-Lua + +{ + name = "ElvUI", + version = "13.74", + enabled = true +} +``` + +### Example 2: Convert a Lua table string to a PowerShell object + +```powershell +$lua = '{ name = "ElvUI", version = "13.74", enabled = true }' +$config = $lua | ConvertFrom-Lua +$config.name # ElvUI +$config.enabled # True +``` + +### Example 3: Read a Lua file and convert to PowerShell -### Example 1: Greet an entity +```powershell +$luaContent = Get-Content -Path 'config.lua' -Raw +$config = ConvertFrom-Lua -InputObject $luaContent +$config.unitframes.playerWidth # 270 +``` -Provide examples for typical commands that a user would like to do with the module. +### Example 4: Convert a PowerShell object to compressed Lua ```powershell -Greet-Entity -Name 'World' -Hello, World! +@(1, 2, 3) | ConvertTo-Lua -Compress + +{1,2,3} ``` -### Example 2 +### Example 5: Round-trip JSON to Lua + +```powershell +$data = Get-Content -Path 'settings.json' -Raw | ConvertFrom-Json +$luaOutput = $data | ConvertTo-Lua +$luaOutput | Set-Content -Path 'settings.lua' +``` -Provide examples for typical commands that a user would like to do with the module. +### Example 6: Convert Lua to PSCustomObject ```powershell -Import-Module -Name PSModuleTemplate +$result = '{ server = "localhost", port = 8080 }' | ConvertFrom-Lua -AsObject +$result.server # localhost +$result.port # 8080 ``` ### Find more examples To find more examples of how to use the module, please refer to the [examples](examples) folder. -Alternatively, you can use the Get-Command -Module 'This module' to find more commands that are available in the module. -To find examples of each of the commands you can use Get-Help -Examples 'CommandName'. +Alternatively, you can use `Get-Command -Module 'Lua'` to find commands available in the module. +To find examples of each command, use `Get-Help -Examples 'CommandName'`. ## Documentation -Link to further documentation if available, or describe where in the repository users can find more detailed documentation about -the module's functions and features. +For detailed documentation on each function, use the built-in help system: + +```powershell +Get-Help ConvertTo-Lua -Full +Get-Help ConvertFrom-Lua -Full +``` ## Contributing Coder or not, you can contribute to the project! We welcome all contributions. -### For Users +### For users If you don't code, you still sit on valuable information that can make this project even better. If you experience that the product does unexpected things, throw errors or is missing functionality, you can help by submitting bugs and feature requests. Please see the issues tab on this project and submit a new issue that matches your needs. -### For Developers +### For developers If you do code, we'd love to have your contributions. Please read the [Contribution guidelines](CONTRIBUTING.md) for more information. You can either help by picking up an existing issue or submit a new one if you have an idea for a new feature or improvement. - -## Acknowledgements - -Here is a list of people and projects that helped this project in some way. diff --git a/examples/General.ps1 b/examples/General.ps1 index e193423..0f2dacb 100644 --- a/examples/General.ps1 +++ b/examples/General.ps1 @@ -1,19 +1,43 @@ <# - .SYNOPSIS - This is a general example of how to use the module. + .SYNOPSIS + Examples of how to use the Lua module. #> # Import the module -Import-Module -Name 'PSModule' +Import-Module -Name 'Lua' -# Define the path to the font file -$FontFilePath = 'C:\Fonts\CodeNewRoman\CodeNewRomanNerdFontPropo-Regular.tff' +# Convert a PowerShell hashtable to Lua table notation +$config = [ordered]@{ + name = 'ElvUI' + version = '13.74' + enabled = $true + scaling = 0.85 + authors = @('Elv', 'Simpy', 'Blazeflack') +} +$luaOutput = $config | ConvertTo-Lua +Write-Output $luaOutput -# Install the font -Install-Font -Path $FontFilePath -Verbose +# Convert a Lua table string to a PowerShell object +$luaString = @' +{ + name = "ElvUI", + version = "13.74", + enabled = true, + unitframes = { + playerWidth = 270, + playerHeight = 54 + } +} +'@ +$result = $luaString | ConvertFrom-Lua +Write-Output "Name: $($result.name)" +Write-Output "Player Width: $($result.unitframes.playerWidth)" -# List installed fonts -Get-Font -Name 'CodeNewRomanNerdFontPropo-Regular' +# Convert Lua to PSCustomObject +$obj = '{ server = "localhost", port = 8080 }' | ConvertFrom-Lua -AsObject +Write-Output "Server: $($obj.server), Port: $($obj.port)" + +# Compressed output +$compressed = @(1, 2, 3, 4, 5) | ConvertTo-Lua -Compress +Write-Output "Compressed: $compressed" -# Uninstall the font -Get-Font -Name 'CodeNewRomanNerdFontPropo-Regular' | Uninstall-Font -Verbose diff --git a/src/assemblies/LsonLib.dll b/src/assemblies/LsonLib.dll deleted file mode 100644 index 36618070d5c9f5131ec66720aa0565c13e86d23f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43520 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 - - - - System.Globalization.CultureInfo - - System.Globalization.CultureInfo - - - - - 16 - - - 16 - - - - - - - - LCID - - - Name - - - DisplayName - - - - - - - - diff --git a/src/formats/Mygciview.Format.ps1xml b/src/formats/Mygciview.Format.ps1xml deleted file mode 100644 index 4c972c2..0000000 --- a/src/formats/Mygciview.Format.ps1xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - mygciview - - System.IO.DirectoryInfo - System.IO.FileInfo - - - PSParentPath - - - - - - 7 - Left - - - - 26 - Right - - - - 26 - Right - - - - 14 - Right - - - - Left - - - - - - - - ModeWithoutHardLink - - - LastWriteTime - - - CreationTime - - - Length - - - Name - - - - - - - - diff --git a/src/functions/private/ConvertFrom-LuaTable.ps1 b/src/functions/private/ConvertFrom-LuaTable.ps1 new file mode 100644 index 0000000..7540b45 --- /dev/null +++ b/src/functions/private/ConvertFrom-LuaTable.ps1 @@ -0,0 +1,443 @@ +function ConvertFrom-LuaTable { + <# + .SYNOPSIS + Parses a Lua table string into a PowerShell object. + + .DESCRIPTION + Takes a Lua table string and converts it to PowerShell hashtables, arrays, + and primitive types. This is the internal parsing engine used by ConvertFrom-Lua. + + Supports: + - Lua tables with string or identifier keys (converted to hashtables) + - Lua arrays/sequences (converted to arrays) + - Mixed tables (keys become hashtable entries, sequential values get numeric keys) + - Strings (single and double quoted, with escape sequences) + - Numbers (integers and floats) + - Booleans (true/false) + - nil (converted to $null) + - Single-line comments (-- ...) + - Multi-line comments (--[[ ... ]]) + #> + [OutputType([object])] + [CmdletBinding()] + param( + # The Lua table string to parse. + [Parameter(Mandatory)] + [string] $InputString, + + # Whether to output PSCustomObjects instead of hashtables. + [Parameter()] + [switch] $AsPSCustomObject + ) + + begin {} + + process { + $script:luaString = $InputString + $script:luaPos = 0 + $script:luaAsPSCustomObject = $AsPSCustomObject.IsPresent + + Skip-LuaWhitespace + $result = Read-LuaValue + + return $result + } + + end {} +} + +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 --[[ ... ]] + if ($script:luaPos + 1 -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -eq '[' -and + $script:luaString[$script:luaPos + 1] -eq '[') { + $script:luaPos += 2 + while ($script:luaPos + 1 -lt $script:luaString.Length) { + if ($script:luaString[$script:luaPos] -eq ']' -and + $script:luaString[$script:luaPos + 1] -eq ']') { + $script:luaPos += 2 + break + } + $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 {} +} + +function Read-LuaValue { + <# + .SYNOPSIS + Reads a single Lua value from the current parser position. + #> + [OutputType([object])] + [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 [[ ... ]] + if ($char -eq '[' -and $script:luaPos + 1 -lt $script:luaString.Length -and + $script:luaString[$script:luaPos + 1] -eq '[') { + return Read-LuaMultiLineString + } + + # Number or negative number + 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]')) { + return Read-LuaNumber + } + + # Keywords: true, false, nil + $remaining = $script:luaString.Substring($script:luaPos) + if ($remaining -match '^true\b') { + $script:luaPos += 4 + return $true + } + if ($remaining -match '^false\b') { + $script:luaPos += 5 + return $false + } + if ($remaining -match '^nil\b') { + $script:luaPos += 3 + return $null + } + + throw "Unexpected character '$char' at position $($script:luaPos)." + } + + end {} +} + +function Read-LuaString { + <# + .SYNOPSIS + Reads a quoted Lua string. + #> + [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) { + 'n' { $null = $result.Append("`n") } + 'r' { $null = $result.Append("`r") } + 't' { $null = $result.Append("`t") } + '\' { $null = $result.Append('\') } + '"' { $null = $result.Append('"') } + "'" { $null = $result.Append("'") } + default { $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 {} +} + +function Read-LuaMultiLineString { + <# + .SYNOPSIS + Reads a multi-line Lua string delimited by [[ and ]]. + #> + [OutputType([string])] + [CmdletBinding()] + param() + + begin {} + + process { + $script:luaPos += 2 # skip [[ + $result = [System.Text.StringBuilder]::new() + + while ($script:luaPos + 1 -lt $script:luaString.Length) { + if ($script:luaString[$script:luaPos] -eq ']' -and $script:luaString[$script:luaPos + 1] -eq ']') { + $script:luaPos += 2 + return $result.ToString() + } + $null = $result.Append($script:luaString[$script:luaPos]) + $script:luaPos++ + } + + throw 'Unterminated multi-line string.' + } + + end {} +} + +function Read-LuaNumber { + <# + .SYNOPSIS + Reads a Lua number (integer or float). + #> + [OutputType([object])] + [CmdletBinding()] + param() + + begin {} + + process { + $start = $script:luaPos + $isFloat = $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]') { + $script:luaPos += 2 + while ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -match '[0-9a-fA-F]') { + $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) { + return [double]::Parse($numStr, [System.Globalization.CultureInfo]::InvariantCulture) + } + if ($numStr -match '^-?0[xX]') { + return [int]::Parse($numStr.Substring($numStr.IndexOf('x') + 1), [System.Globalization.NumberStyles]::HexNumber) + } + $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 {} +} + +function Read-LuaTable { + <# + .SYNOPSIS + Reads a Lua table and returns either an array or hashtable. + #> + [OutputType([object])] + [CmdletBinding()] + param() + + begin {} + + process { + $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 + if ($script:luaString[$script:luaPos] -eq '[') { + $script:luaPos++ # skip [ + Skip-LuaWhitespace + $key = Read-LuaValue + Skip-LuaWhitespace + if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq ']') { + $script:luaPos++ # skip ] + } + Skip-LuaWhitespace + if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq '=') { + $script:luaPos++ # skip = + } + Skip-LuaWhitespace + $value = Read-LuaValue + $entries.Add(@{ Key = [string]$key; Value = $value }) + $hasStringKeys = $true + } + # Check for identifier key: key = value + elseif ($script:luaString[$script:luaPos] -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) + + 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 (true/false/nil) + $resolvedValue = switch ($ident) { + 'true' { $true } + 'false' { $false } + 'nil' { $null } + default { $ident } + } + $arrayValues.Add($resolvedValue) + $hasArrayValues = $true + } + } else { + # Array value + $value = Read-LuaValue + $arrayValues.Add($value) + $hasArrayValues = $true + } + + Skip-LuaWhitespace + + # Skip comma or semicolon separator + if ($script:luaPos -lt $script:luaString.Length -and ($script:luaString[$script:luaPos] -eq ',' -or $script:luaString[$script:luaPos] -eq ';')) { + $script:luaPos++ + } + } + + if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq '}') { + $script:luaPos++ # skip } + } + + # Pure array (no string keys) + if ($hasArrayValues -and -not $hasStringKeys) { + return , [object[]]$arrayValues.ToArray() + } + + # Build hashtable or PSCustomObject + $table = [ordered]@{} + $arrayIndex = 1 + foreach ($entry in $entries) { + $table[$entry.Key] = $entry.Value + } + foreach ($value in $arrayValues) { + $table[[string]$arrayIndex] = $value + $arrayIndex++ + } + + if ($script:luaAsPSCustomObject) { + return [pscustomobject]$table + } + return $table + } + + end {} +} diff --git a/src/functions/private/ConvertTo-LuaTable.ps1 b/src/functions/private/ConvertTo-LuaTable.ps1 new file mode 100644 index 0000000..c75b3ec --- /dev/null +++ b/src/functions/private/ConvertTo-LuaTable.ps1 @@ -0,0 +1,115 @@ +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 string. This is the internal serialization engine used by ConvertTo-Lua. + #> + [OutputType([string])] + [CmdletBinding()] + param( + # The object to convert to a Lua table string. + [Parameter(Mandatory)] + [AllowNull()] + [object] $InputObject, + + # The current indentation depth for formatting. + [Parameter()] + [int] $Depth = 0, + + # Number of spaces per indentation level. + [Parameter()] + [int] $IndentSize = 4, + + # Whether to compress the output (no newlines or indentation). + [Parameter()] + [switch] $Compress + ) + + begin { + $indent = if ($Compress) { '' } else { ' ' * ($IndentSize * $Depth) } + $childIndent = if ($Compress) { '' } else { ' ' * ($IndentSize * ($Depth + 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' + } + } + + if ($InputObject -is [int] -or $InputObject -is [long] -or + $InputObject -is [float] -or $InputObject -is [double] -or + $InputObject -is [decimal] -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] -or + $InputObject -is [single]) { + return $InputObject.ToString([System.Globalization.CultureInfo]::InvariantCulture) + } + + if ($InputObject -is [string]) { + $escaped = $InputObject -replace '\\', '\\' -replace '"', '\"' -replace "`n", '\n' -replace "`r", '\r' -replace "`t", '\t' + return "`"$escaped`"" + } + + if ($InputObject -is [System.Collections.IList]) { + if ($InputObject.Count -eq 0) { + return '{}' + } + $items = [System.Collections.Generic.List[string]]::new() + foreach ($item in $InputObject) { + $value = ConvertTo-LuaTable -InputObject $item -Depth ($Depth + 1) -IndentSize $IndentSize -Compress:$Compress + $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) { + $value = ConvertTo-LuaTable -InputObject $InputObject[$key] -Depth ($Depth + 1) -IndentSize $IndentSize -Compress:$Compress + $luaKey = Format-LuaKey -Key ([string]$key) + $space = if ($Compress) { '' } else { ' ' } + $entries.Add("$childIndent$luaKey$space=${space}$value") + } + return "{$newline$($entries -join $separator)$newline$indent}" + } + + # Handle PSCustomObject (from ConvertFrom-Json etc.) + 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) { + $value = ConvertTo-LuaTable -InputObject $prop.Value -Depth ($Depth + 1) -IndentSize $IndentSize -Compress:$Compress + $luaKey = Format-LuaKey -Key $prop.Name + $space = if ($Compress) { '' } else { ' ' } + $entries.Add("$childIndent$luaKey$space=${space}$value") + } + return "{$newline$($entries -join $separator)$newline$indent}" + } + + # Fallback: convert to string + $escaped = ($InputObject.ToString()) -replace '\\', '\\\\' -replace '"', '\"' + return "`"$escaped`"" + } + + end {} +} diff --git a/src/functions/private/Format-LuaKey.ps1 b/src/functions/private/Format-LuaKey.ps1 new file mode 100644 index 0000000..d4b70d3 --- /dev/null +++ b/src/functions/private/Format-LuaKey.ps1 @@ -0,0 +1,29 @@ +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, + otherwise wraps it in bracket-quote notation: ["key"]. + #> + [OutputType([string])] + [CmdletBinding()] + param( + # The key string to format. + [Parameter(Mandatory)] + [string] $Key + ) + + begin {} + + process { + if ($Key -match '^[a-zA-Z_][a-zA-Z0-9_]*$') { + return $Key + } + $escaped = $Key -replace '\\', '\\\\' -replace '"', '\"' + return "[`"$escaped`"]" + } + + end {} +} diff --git a/src/functions/private/Get-InternalPSModule.ps1 b/src/functions/private/Get-InternalPSModule.ps1 deleted file mode 100644 index 89f053c..0000000 --- a/src/functions/private/Get-InternalPSModule.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -function Get-InternalPSModule { - <# - .SYNOPSIS - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - #> - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/functions/private/Set-InternalPSModule.ps1 b/src/functions/private/Set-InternalPSModule.ps1 deleted file mode 100644 index cf870ba..0000000 --- a/src/functions/private/Set-InternalPSModule.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -function Set-InternalPSModule { - <# - .SYNOPSIS - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - #> - [Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', - Justification = 'Reason for suppressing' - )] - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/functions/public/Lua/ConvertFrom-Lua.ps1 b/src/functions/public/Lua/ConvertFrom-Lua.ps1 new file mode 100644 index 0000000..6128521 --- /dev/null +++ b/src/functions/public/Lua/ConvertFrom-Lua.ps1 @@ -0,0 +1,82 @@ +function ConvertFrom-Lua { + <# + .SYNOPSIS + Converts a Lua table string to a PowerShell object. + + .DESCRIPTION + Takes a Lua table string and parses it into PowerShell objects. Lua tables with + string keys become ordered hashtables (or PSCustomObjects with -AsObject), Lua + sequences become arrays, and Lua primitives are converted to their PowerShell + equivalents. + + Supports the following Lua to PowerShell type mappings: + - Lua table (key = value) -> [ordered] hashtable or [PSCustomObject] + - 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 Value + ---- ----- + name Alice + age 30 + ``` + + .EXAMPLE + ```powershell + ConvertFrom-Lua -InputObject '{ 1, 2, 3 }' + + 1 + 2 + 3 + ``` + + .EXAMPLE + ```powershell + '{ server = "localhost", port = 8080, enabled = true }' | ConvertFrom-Lua -AsObject + + server port enabled + ------ ---- ------- + localhost 8080 True + ``` + + .NOTES + [Lua Table Documentation](https://www.lua.org/pil/2.5.html) + + .LINK + https://psmodule.io/Lua/Functions/ConvertFrom-Lua/ + + .LINK + https://www.lua.org/pil/2.5.html + #> + [OutputType([object])] + [CmdletBinding()] + param( + # The Lua table string to convert to a PowerShell object. + [Parameter(Mandatory, ValueFromPipeline)] + [ValidateNotNullOrEmpty()] + [string] $InputObject, + + # Output PSCustomObjects instead of hashtables for Lua tables with string keys. + [Parameter()] + [switch] $AsObject + ) + + begin {} + + process { + ConvertFrom-LuaTable -InputString $InputObject -AsPSCustomObject:$AsObject + } + + end {} +} diff --git a/src/functions/public/Lua/ConvertTo-Lua.ps1 b/src/functions/public/Lua/ConvertTo-Lua.ps1 new file mode 100644 index 0000000..8e28bcd --- /dev/null +++ b/src/functions/public/Lua/ConvertTo-Lua.ps1 @@ -0,0 +1,82 @@ +function ConvertTo-Lua { + <# + .SYNOPSIS + Converts a PowerShell object to a Lua table string. + + .DESCRIPTION + Takes a PowerShell object (hashtable, PSCustomObject, array, or primitive value) and + converts it to a Lua table string representation. Nested structures are recursively + converted with proper 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] / [double] / [decimal] -> Lua number + - [bool] -> Lua boolean (true/false) + - $null -> nil + + .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 + [PSCustomObject]@{ server = "localhost"; port = 8080; enabled = $true } | ConvertTo-Lua + + { + server = "localhost", + port = 8080, + enabled = true + } + ``` + + .NOTES + [Lua Table Documentation](https://www.lua.org/pil/2.5.html) + + .LINK + https://psmodule.io/Lua/Functions/ConvertTo-Lua/ + + .LINK + https://www.lua.org/pil/2.5.html + #> + [OutputType([string])] + [CmdletBinding()] + param( + # The object to convert to a Lua table string. + [Parameter(Mandatory, ValueFromPipeline)] + [AllowNull()] + [object] $InputObject, + + # Number of spaces per indentation level. + [Parameter()] + [ValidateRange(1, 16)] + [int] $Depth = 4, + + # Whether to compress the output by removing whitespace and newlines. + [Parameter()] + [switch] $Compress + ) + + begin {} + + process { + ConvertTo-LuaTable -InputObject $InputObject -Depth 0 -IndentSize $Depth -Compress:$Compress + } + + end {} +} diff --git a/src/functions/public/PSModule/Get-PSModuleTest.ps1 b/src/functions/public/PSModule/Get-PSModuleTest.ps1 deleted file mode 100644 index a07d05b..0000000 --- a/src/functions/public/PSModule/Get-PSModuleTest.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -#Requires -Modules Utilities -#Requires -Modules @{ ModuleName = 'PSSemVer'; RequiredVersion = '1.1.4' } -#Requires -Modules @{ ModuleName = 'DynamicParams'; ModuleVersion = '1.1.8' } -#Requires -Modules @{ ModuleName = 'Store'; ModuleVersion = '0.3.1' } - -function Get-PSModuleTest { - <# - .SYNOPSIS - Performs tests on a module. - - .DESCRIPTION - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - #> - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/functions/public/PSModule/New-PSModuleTest.ps1 b/src/functions/public/PSModule/New-PSModuleTest.ps1 deleted file mode 100644 index e003841..0000000 --- a/src/functions/public/PSModule/New-PSModuleTest.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -#Requires -Modules @{ModuleName='PSSemVer'; ModuleVersion='1.1.4'} - -function New-PSModuleTest { - <# - .SYNOPSIS - Performs tests on a module. - - .DESCRIPTION - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - - .NOTES - Testing if a module can have a [Markdown based link](https://example.com). - !"#¤%&/()=?`´^¨*'-_+§½{[]}<>|@£$€¥¢:;.," - \[This is a test\] - #> - [Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', - Justification = 'Reason for suppressing' - )] - [Alias('New-PSModuleTestAlias1')] - [Alias('New-PSModuleTestAlias2')] - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} - -New-Alias New-PSModuleTestAlias3 New-PSModuleTest -New-Alias -Name New-PSModuleTestAlias4 -Value New-PSModuleTest - - -Set-Alias New-PSModuleTestAlias5 New-PSModuleTest diff --git a/src/functions/public/PSModule/PSModule.md b/src/functions/public/PSModule/PSModule.md deleted file mode 100644 index a657773..0000000 --- a/src/functions/public/PSModule/PSModule.md +++ /dev/null @@ -1,3 +0,0 @@ -# PSModule - -This is a sub page for PSModule. diff --git a/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 b/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 deleted file mode 100644 index 23ec98e..0000000 --- a/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -function Set-PSModuleTest { - <# - .SYNOPSIS - Performs tests on a module. - - .DESCRIPTION - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - #> - [Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', - Justification = 'Reason for suppressing' - )] - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/functions/public/SomethingElse/SomethingElse.md b/src/functions/public/SomethingElse/SomethingElse.md deleted file mode 100644 index d9f7e9e..0000000 --- a/src/functions/public/SomethingElse/SomethingElse.md +++ /dev/null @@ -1 +0,0 @@ -# This is SomethingElse diff --git a/src/functions/public/Test-PSModuleTest.ps1 b/src/functions/public/Test-PSModuleTest.ps1 deleted file mode 100644 index 0c27510..0000000 --- a/src/functions/public/Test-PSModuleTest.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -function Test-PSModuleTest { - <# - .SYNOPSIS - Performs tests on a module. - - .DESCRIPTION - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - #> - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/functions/public/completers.ps1 b/src/functions/public/completers.ps1 deleted file mode 100644 index 6b1adbb..0000000 --- a/src/functions/public/completers.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -Register-ArgumentCompleter -CommandName New-PSModuleTest -ParameterName Name -ScriptBlock { - param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) - $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters - - 'Alice', 'Bob', 'Charlie' | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { - [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) - } -} diff --git a/src/header.ps1 b/src/header.ps1 index cc1fde9..3247a0e 100644 --- a/src/header.ps1 +++ b/src/header.ps1 @@ -1,3 +1,2 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains long links.')] -[CmdletBinding()] +[CmdletBinding()] param() diff --git a/src/init/initializer.ps1 b/src/init/initializer.ps1 deleted file mode 100644 index 28396fb..0000000 --- a/src/init/initializer.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -Write-Verbose '-------------------------------' -Write-Verbose '--- THIS IS AN INITIALIZER ---' -Write-Verbose '-------------------------------' diff --git a/src/manifest.psd1 b/src/manifest.psd1 index ff720bd..25c1842 100644 --- a/src/manifest.psd1 +++ b/src/manifest.psd1 @@ -1,5 +1,6 @@ # This file always wins! # Use this file to override any of the framework defaults and generated values. @{ - ModuleVersion = '0.0.0' + ModuleVersion = '0.0.1' + Description = 'A PowerShell module for converting between PowerShell objects and Lua table notation.' } diff --git a/src/modules/OtherPSModule.psm1 b/src/modules/OtherPSModule.psm1 deleted file mode 100644 index 5d6af8e..0000000 --- a/src/modules/OtherPSModule.psm1 +++ /dev/null @@ -1,19 +0,0 @@ -function Get-OtherPSModule { - <# - .SYNOPSIS - Performs tests on a module. - - .DESCRIPTION - A longer description of the function. - - .EXAMPLE - Get-OtherPSModule -Name 'World' - #> - [CmdletBinding()] - param( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/scripts/loader.ps1 b/src/scripts/loader.ps1 deleted file mode 100644 index 973735a..0000000 --- a/src/scripts/loader.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -Write-Verbose '-------------------------' -Write-Verbose '--- THIS IS A LOADER ---' -Write-Verbose '-------------------------' diff --git a/src/types/DirectoryInfo.Types.ps1xml b/src/types/DirectoryInfo.Types.ps1xml deleted file mode 100644 index aef538b..0000000 --- a/src/types/DirectoryInfo.Types.ps1xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - System.IO.FileInfo - - - Status - Success - - - - - System.IO.DirectoryInfo - - - Status - Success - - - - diff --git a/src/types/FileInfo.Types.ps1xml b/src/types/FileInfo.Types.ps1xml deleted file mode 100644 index 4cfaf6b..0000000 --- a/src/types/FileInfo.Types.ps1xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - System.IO.FileInfo - - - Age - - ((Get-Date) - ($this.CreationTime)).Days - - - - - diff --git a/src/variables/private/PrivateVariables.ps1 b/src/variables/private/PrivateVariables.ps1 deleted file mode 100644 index f1fc2c3..0000000 --- a/src/variables/private/PrivateVariables.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -$script:HabitablePlanets = @( - @{ - Name = 'Earth' - Mass = 5.97 - Diameter = 12756 - DayLength = 24.0 - }, - @{ - Name = 'Mars' - Mass = 0.642 - Diameter = 6792 - DayLength = 24.7 - }, - @{ - Name = 'Proxima Centauri b' - Mass = 1.17 - Diameter = 11449 - DayLength = 5.15 - }, - @{ - Name = 'Kepler-442b' - Mass = 2.34 - Diameter = 11349 - DayLength = 5.7 - }, - @{ - Name = 'Kepler-452b' - Mass = 5.0 - Diameter = 17340 - DayLength = 20.0 - } -) - -$script:InhabitedPlanets = @( - @{ - Name = 'Earth' - Mass = 5.97 - Diameter = 12756 - DayLength = 24.0 - }, - @{ - Name = 'Mars' - Mass = 0.642 - Diameter = 6792 - DayLength = 24.7 - } -) diff --git a/src/variables/public/Moons.ps1 b/src/variables/public/Moons.ps1 deleted file mode 100644 index dd0f33c..0000000 --- a/src/variables/public/Moons.ps1 +++ /dev/null @@ -1,6 +0,0 @@ -$script:Moons = @( - @{ - Planet = 'Earth' - Name = 'Moon' - } -) diff --git a/src/variables/public/Planets.ps1 b/src/variables/public/Planets.ps1 deleted file mode 100644 index 5927bc5..0000000 --- a/src/variables/public/Planets.ps1 +++ /dev/null @@ -1,20 +0,0 @@ -$script:Planets = @( - @{ - Name = 'Mercury' - Mass = 0.330 - Diameter = 4879 - DayLength = 4222.6 - }, - @{ - Name = 'Venus' - Mass = 4.87 - Diameter = 12104 - DayLength = 2802.0 - }, - @{ - Name = 'Earth' - Mass = 5.97 - Diameter = 12756 - DayLength = 24.0 - } -) diff --git a/src/variables/public/SolarSystems.ps1 b/src/variables/public/SolarSystems.ps1 deleted file mode 100644 index acbcedf..0000000 --- a/src/variables/public/SolarSystems.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -$script:SolarSystems = @( - @{ - Name = 'Solar System' - Planets = $script:Planets - Moons = $script:Moons - }, - @{ - Name = 'Alpha Centauri' - Planets = @() - Moons = @() - }, - @{ - Name = 'Sirius' - Planets = @() - Moons = @() - } -) diff --git a/tests/Lua.Tests.ps1 b/tests/Lua.Tests.ps1 new file mode 100644 index 0000000..170c96b --- /dev/null +++ b/tests/Lua.Tests.ps1 @@ -0,0 +1,490 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester tests' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester tests' +)] +[CmdletBinding()] +param() + +Describe 'ConvertFrom-Lua' { + BeforeAll { + $dataPath = Join-Path -Path $PSScriptRoot -ChildPath 'data' + } + Context 'Primitives' { + It 'Converts a Lua string to a PowerShell string' { + $result = ConvertFrom-Lua -InputObject '"hello"' + $result | Should -Be 'hello' + } + + It 'Converts a Lua integer to a PowerShell int' { + $result = ConvertFrom-Lua -InputObject '42' + $result | Should -Be 42 + $result | Should -BeOfType [int] + } + + It 'Converts a negative Lua integer' { + $result = ConvertFrom-Lua -InputObject '-7' + $result | Should -Be -7 + } + + It 'Converts a Lua float to a PowerShell double' { + $result = ConvertFrom-Lua -InputObject '3.14' + $result | Should -Be 3.14 + $result | Should -BeOfType [double] + } + + It 'Converts Lua true to PowerShell $true' { + $result = ConvertFrom-Lua -InputObject 'true' + $result | Should -BeTrue + } + + It 'Converts Lua false to PowerShell $false' { + $result = ConvertFrom-Lua -InputObject 'false' + $result | Should -BeFalse + } + + It 'Converts Lua nil to PowerShell $null' { + $result = ConvertFrom-Lua -InputObject 'nil' + $result | Should -BeNullOrEmpty + } + } + + Context 'Strings' { + It 'Handles double-quoted strings' { + $result = ConvertFrom-Lua -InputObject '"hello world"' + $result | Should -Be 'hello world' + } + + It 'Handles single-quoted strings' { + $result = ConvertFrom-Lua -InputObject "'hello world'" + $result | Should -Be 'hello world' + } + + It 'Handles escape sequences in strings' { + $result = ConvertFrom-Lua -InputObject '"line1\nline2"' + $result | Should -Be "line1`nline2" + } + + It 'Handles escaped quotes in strings' { + $result = ConvertFrom-Lua -InputObject '"she said \"hi\""' + $result | Should -Be 'she said "hi"' + } + + It 'Handles escaped backslashes' { + $result = ConvertFrom-Lua -InputObject '"path\\to\\file"' + $result | Should -Be 'path\to\file' + } + + It 'Handles multi-line strings with [[ ]]' { + $result = ConvertFrom-Lua -InputObject '[[hello world]]' + $result | Should -Be 'hello world' + } + } + + Context 'Arrays (sequences)' { + It 'Converts a simple integer array' { + $result = ConvertFrom-Lua -InputObject '{1, 2, 3}' + $result.Count | Should -Be 3 + $result[0] | Should -Be 1 + $result[1] | Should -Be 2 + $result[2] | Should -Be 3 + } + + It 'Converts a string array' { + $result = ConvertFrom-Lua -InputObject '{"a", "b", "c"}' + $result.Count | Should -Be 3 + $result[0] | Should -Be 'a' + $result[1] | Should -Be 'b' + $result[2] | Should -Be 'c' + } + + It 'Converts an empty table to an empty hashtable' { + $result = ConvertFrom-Lua -InputObject '{}' + $result.Count | Should -Be 0 + } + + It 'Converts nested arrays' { + $result = ConvertFrom-Lua -InputObject '{{1, 2}, {3, 4}}' + $result.Count | Should -Be 2 + $result[0].Count | Should -Be 2 + $result[0][0] | Should -Be 1 + $result[1][1] | Should -Be 4 + } + } + + Context 'Tables (dictionaries)' { + It 'Converts a simple key-value table' { + $result = ConvertFrom-Lua -InputObject '{ name = "Alice", age = 30 }' + $result.name | Should -Be 'Alice' + $result.age | Should -Be 30 + } + + It 'Converts bracket-quoted keys' { + $result = ConvertFrom-Lua -InputObject '{ ["special key"] = "value" }' + $result['special key'] | Should -Be 'value' + } + + It 'Converts nested tables' { + $result = ConvertFrom-Lua -InputObject '{ inner = { x = 1, y = 2 } }' + $result.inner.x | Should -Be 1 + $result.inner.y | Should -Be 2 + } + + It 'Handles boolean values in tables' { + $result = ConvertFrom-Lua -InputObject '{ enabled = true, debug = false }' + $result.enabled | Should -BeTrue + $result.debug | Should -BeFalse + } + + It 'Returns PSCustomObject when -AsObject is used' { + $result = ConvertFrom-Lua -InputObject '{ name = "Alice" }' -AsObject + $result | Should -BeOfType [PSCustomObject] + $result.name | Should -Be 'Alice' + } + } + + Context 'Comments' { + It 'Ignores single-line comments' { + $lua = @' +{ + -- This is a comment + name = "Alice", + age = 30 -- inline comment +} +'@ + $result = ConvertFrom-Lua -InputObject $lua + $result.name | Should -Be 'Alice' + $result.age | Should -Be 30 + } + + It 'Ignores multi-line comments' { + $lua = @' +{ + --[[ This is a + multi-line comment ]] + name = "Bob" +} +'@ + $result = ConvertFrom-Lua -InputObject $lua + $result.name | Should -Be 'Bob' + } + } + + Context 'Pipeline input' { + It 'Accepts input from the pipeline' { + $result = '{ x = 10 }' | ConvertFrom-Lua + $result.x | Should -Be 10 + } + } + + Context 'File-based test: Strings' { + BeforeAll { + $luaContent = Get-Content -Path (Join-Path $dataPath 'Strings.lua') -Raw + $expected = Get-Content -Path (Join-Path $dataPath 'Strings.json') -Raw | ConvertFrom-Json + } + + It 'Parses string test file and matches JSON reference' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.simpleString | Should -Be $expected.simpleString + $result.escapedQuote | Should -Be $expected.escapedQuote + $result.newlineString | Should -Be $expected.newlineString + $result.tabString | Should -Be $expected.tabString + $result.backslash | Should -Be $expected.backslash + } + } + + Context 'File-based test: Arrays' { + BeforeAll { + $luaContent = Get-Content -Path (Join-Path $dataPath 'Arrays.lua') -Raw + $expected = Get-Content -Path (Join-Path $dataPath 'Arrays.json') -Raw | ConvertFrom-Json + } + + It 'Parses integer arrays correctly' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.integers.Count | Should -Be $expected.integers.Count + for ($i = 0; $i -lt $expected.integers.Count; $i++) { + $result.integers[$i] | Should -Be $expected.integers[$i] + } + } + + It 'Parses float arrays correctly' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.floats.Count | Should -Be $expected.floats.Count + for ($i = 0; $i -lt $expected.floats.Count; $i++) { + $result.floats[$i] | Should -Be $expected.floats[$i] + } + } + + It 'Parses boolean arrays correctly' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.booleans[0] | Should -BeTrue + $result.booleans[1] | Should -BeFalse + } + } + + Context 'File-based test: TestStructure' { + BeforeAll { + $luaContent = Get-Content -Path (Join-Path $dataPath 'TestStructure.lua') -Raw + $expected = Get-Content -Path (Join-Path $dataPath 'TestStructure.json') -Raw | ConvertFrom-Json + } + + It 'Parses top-level string properties' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.name | Should -Be $expected.name + $result.version | Should -Be $expected.version + $result.description | Should -Be $expected.description + } + + It 'Parses top-level boolean properties' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.enabled | Should -Be $expected.enabled + $result.debug | Should -Be $expected.debug + } + + It 'Parses top-level numeric properties' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.maxRetries | Should -Be $expected.maxRetries + $result.scaling | Should -Be $expected.scaling + } + + It 'Parses array properties' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.authors.Count | Should -Be $expected.authors.Count + $result.authors[0] | Should -Be $expected.authors[0] + $result.authors[1] | Should -Be $expected.authors[1] + $result.authors[2] | Should -Be $expected.authors[2] + } + + It 'Parses nested table properties' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.unitframes.enabled | Should -Be $expected.unitframes.enabled + $result.unitframes.playerWidth | Should -Be $expected.unitframes.playerWidth + $result.unitframes.playerHeight | Should -Be $expected.unitframes.playerHeight + } + + It 'Parses deeply nested structures' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.unitframes.colors.health.Count | Should -Be 3 + $result.unitframes.colors.health[0] | Should -Be $expected.unitframes.colors.health[0] + } + + It 'Parses the chat section' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.chat.fontSize | Should -Be $expected.chat.fontSize + $result.chat.panelWidth | Should -Be $expected.chat.panelWidth + $result.chat.fadeChat | Should -Be $expected.chat.fadeChat + $result.chat.keywords | Should -Be $expected.chat.keywords + } + + It 'Parses actionbar nested tables' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.actionbars.bar1.enabled | Should -Be $expected.actionbars.bar1.enabled + $result.actionbars.bar1.buttons | Should -Be $expected.actionbars.bar1.buttons + $result.actionbars.bar2.buttonSize | Should -Be $expected.actionbars.bar2.buttonSize + } + + It 'Parses bracket-quoted keys' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result['specialKey'] | Should -Be $expected.specialKey + } + + It 'Parses unicode strings' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.unicodeNote | Should -Be $expected.unicodeNote + } + } +} + +Describe 'ConvertTo-Lua' { + BeforeAll { + $dataPath = Join-Path -Path $PSScriptRoot -ChildPath 'data' + } + + Context 'Primitives' { + It 'Converts a string to Lua string' { + $result = ConvertTo-Lua -InputObject 'hello' + $result | Should -Be '"hello"' + } + + It 'Converts an integer to Lua number' { + $result = ConvertTo-Lua -InputObject 42 + $result | Should -Be '42' + } + + It 'Converts a negative integer' { + $result = ConvertTo-Lua -InputObject (-7) + $result | Should -Be '-7' + } + + It 'Converts a double to Lua number' { + $result = ConvertTo-Lua -InputObject 3.14 + $result | Should -Be '3.14' + } + + It 'Converts $true to Lua true' { + $result = ConvertTo-Lua -InputObject $true + $result | Should -Be 'true' + } + + It 'Converts $false to Lua false' { + $result = ConvertTo-Lua -InputObject $false + $result | Should -Be 'false' + } + + It 'Converts $null to Lua nil' { + $result = ConvertTo-Lua -InputObject $null + $result | Should -Be 'nil' + } + } + + Context 'String escaping' { + It 'Escapes double quotes in strings' { + $result = ConvertTo-Lua -InputObject 'she said "hi"' + $result | Should -Be '"she said \"hi\""' + } + + It 'Escapes backslashes in strings' { + $result = ConvertTo-Lua -InputObject 'path\to\file' + $result | Should -Be '"path\\to\\file"' + } + + It 'Escapes newlines in strings' { + $result = ConvertTo-Lua -InputObject "line1`nline2" + $result | Should -Be '"line1\nline2"' + } + + It 'Escapes tabs in strings' { + $result = ConvertTo-Lua -InputObject "col1`tcol2" + $result | Should -Be '"col1\tcol2"' + } + } + + Context 'Arrays' { + It 'Converts a simple array (compressed)' { + $result = ConvertTo-Lua -InputObject @(1, 2, 3) -Compress + $result | Should -Be '{1,2,3}' + } + + It 'Converts an empty array' { + $result = ConvertTo-Lua -InputObject @() -Compress + $result | Should -Be '{}' + } + + It 'Converts a string array (compressed)' { + $result = ConvertTo-Lua -InputObject @('a', 'b') -Compress + $result | Should -Be '{"a","b"}' + } + + It 'Converts an array with indentation' { + $result = ConvertTo-Lua -InputObject @(1, 2) + $result | Should -Match '^\{' + $result | Should -Match '1' + $result | Should -Match '2' + $result | Should -Match '\}$' + } + } + + Context 'Hashtables' { + It 'Converts a simple hashtable (compressed)' { + $result = ConvertTo-Lua -InputObject @{ x = 1 } -Compress + $result | Should -Be '{x=1}' + } + + It 'Converts an empty hashtable' { + $result = ConvertTo-Lua -InputObject @{} -Compress + $result | Should -Be '{}' + } + + It 'Converts nested hashtables (compressed)' { + $result = ConvertTo-Lua -InputObject ([ordered]@{ inner = ([ordered]@{ a = 1 }) }) -Compress + $result | Should -Be '{inner={a=1}}' + } + + It 'Handles keys with special characters' { + $result = ConvertTo-Lua -InputObject @{ 'my key' = 'value' } -Compress + $result | Should -Be '{["my key"]="value"}' + } + } + + Context 'PSCustomObject' { + It 'Converts a PSCustomObject (compressed)' { + $obj = [PSCustomObject]@{ name = 'test'; value = 42 } + $result = ConvertTo-Lua -InputObject $obj -Compress + $result | Should -Match 'name="test"' + $result | Should -Match 'value=42' + } + } + + Context 'Pipeline input' { + It 'Accepts input from the pipeline' { + $result = @{ x = 10 } | ConvertTo-Lua -Compress + $result | Should -Be '{x=10}' + } + } + + Context 'Round-trip conversion' { + It 'Round-trips a simple hashtable' { + $original = [ordered]@{ name = 'test'; count = 5; active = $true } + $lua = ConvertTo-Lua -InputObject $original + $result = ConvertFrom-Lua -InputObject $lua + $result.name | Should -Be $original.name + $result.count | Should -Be $original.count + $result.active | Should -Be $original.active + } + + It 'Round-trips an array' { + $original = @(1, 2, 3, 4, 5) + $lua = ConvertTo-Lua -InputObject $original + $result = ConvertFrom-Lua -InputObject $lua + $result.Count | Should -Be 5 + for ($i = 0; $i -lt 5; $i++) { + $result[$i] | Should -Be $original[$i] + } + } + + It 'Round-trips nested structures' { + $original = [ordered]@{ + server = 'localhost' + port = 8080 + options = [ordered]@{ + debug = $false + verbose = $true + } + } + $lua = ConvertTo-Lua -InputObject $original + $result = ConvertFrom-Lua -InputObject $lua + $result.server | Should -Be 'localhost' + $result.port | Should -Be 8080 + $result.options.debug | Should -BeFalse + $result.options.verbose | Should -BeTrue + } + + It 'Round-trips from JSON reference to Lua and back' { + $jsonPath = Join-Path $dataPath 'TestStructure.json' + $expected = Get-Content -Path $jsonPath -Raw | ConvertFrom-Json + $lua = ConvertTo-Lua -InputObject $expected + $result = ConvertFrom-Lua -InputObject $lua + $result.name | Should -Be $expected.name + $result.enabled | Should -Be $expected.enabled + $result.maxRetries | Should -Be $expected.maxRetries + $result.authors.Count | Should -Be $expected.authors.Count + } + } + + Context 'Indentation' { + It 'Uses custom indent size' { + $result = ConvertTo-Lua -InputObject @(1) -Depth 2 + $lines = $result -split "`n" + $lines[1] | Should -Match '^ 1$' + } + + It 'Compress removes all whitespace and newlines' { + $result = ConvertTo-Lua -InputObject ([ordered]@{ a = 1; b = 2 }) -Compress + $result | Should -Not -Match "`n" + $result | Should -Not -Match ' ' + } + } +} diff --git a/tests/PSModuleTest.Tests.ps1 b/tests/PSModuleTest.Tests.ps1 deleted file mode 100644 index b856855..0000000 --- a/tests/PSModuleTest.Tests.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester tests' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester tests' -)] -[CmdletBinding()] -param() - -Describe 'Module' { - It 'Function: Get-PSModuleTest' { - Get-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' - } - It 'Function: New-PSModuleTest' { - New-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' - } - It 'Function: Set-PSModuleTest' { - Set-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' - } - It 'Function: Test-PSModuleTest' { - Test-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' - } -} diff --git a/tests/data/Arrays.json b/tests/data/Arrays.json new file mode 100644 index 0000000..8d2b3e5 --- /dev/null +++ b/tests/data/Arrays.json @@ -0,0 +1,26 @@ +{ + "integers": [ + 1, + 2, + 3, + 42, + -7, + 0 + ], + "floats": [ + 3.14, + -2.5, + 0.001, + 1e10 + ], + "booleans": [ + true, + false + ], + "mixed": [ + 1, + "two", + true, + null + ] +} diff --git a/tests/data/Arrays.lua b/tests/data/Arrays.lua new file mode 100644 index 0000000..f673a7f --- /dev/null +++ b/tests/data/Arrays.lua @@ -0,0 +1,6 @@ +{ + integers = {1, 2, 3, 42, -7, 0}, + floats = {3.14, -2.5, 0.001, 1e10}, + booleans = {true, false}, + mixed = {1, "two", true, nil} +} diff --git a/tests/data/Strings.json b/tests/data/Strings.json new file mode 100644 index 0000000..3fad475 --- /dev/null +++ b/tests/data/Strings.json @@ -0,0 +1,7 @@ +{ + "simpleString": "hello", + "escapedQuote": "she said \"hi\"", + "newlineString": "line1\nline2", + "tabString": "col1\tcol2", + "backslash": "path\\to\\file" +} diff --git a/tests/data/Strings.lua b/tests/data/Strings.lua new file mode 100644 index 0000000..a58e126 --- /dev/null +++ b/tests/data/Strings.lua @@ -0,0 +1,7 @@ +{ + simpleString = "hello", + escapedQuote = "she said \"hi\"", + newlineString = "line1\nline2", + tabString = "col1\tcol2", + backslash = "path\\to\\file" +} diff --git a/tests/data/TestStructure.json b/tests/data/TestStructure.json new file mode 100644 index 0000000..dcbc533 --- /dev/null +++ b/tests/data/TestStructure.json @@ -0,0 +1,77 @@ +{ + "name": "ElvUI", + "version": "13.74", + "enabled": true, + "debug": false, + "maxRetries": 3, + "scaling": 0.85, + "description": "A user interface replacement for World of Warcraft", + "authors": [ + "Elv", + "Simpy", + "Blazeflack" + ], + "emptyList": [], + "unitframes": { + "enabled": true, + "playerWidth": 270, + "playerHeight": 54, + "targetWidth": 270, + "targetHeight": 54, + "colors": { + "health": [ + 0.31, + 0.45, + 0.63 + ], + "power": [ + 0.0, + 0.44, + 0.87 + ], + "castbar": [ + 0.86, + 0.86, + 0.0 + ] + } + }, + "chat": { + "fontSize": 12, + "tabFontSize": 12, + "panelWidth": 412, + "panelHeight": 180, + "fadeChat": true, + "keywords": "ElvUI,Raid,Guild" + }, + "actionbars": { + "bar1": { + "enabled": true, + "buttons": 12, + "buttonsPerRow": 12, + "buttonSize": 30, + "buttonSpacing": 4, + "backdrop": false + }, + "bar2": { + "enabled": true, + "buttons": 12, + "buttonsPerRow": 12, + "buttonSize": 30, + "buttonSpacing": 4, + "backdrop": false + } + }, + "minimap": { + "size": 175, + "locationText": "SHOW" + }, + "tooltip": { + "cursorAnchor": false, + "healthBar": true, + "playerTitles": true, + "guildRanks": true + }, + "specialKey": "key with spaces", + "unicodeNote": "Héllo Wörld" +} diff --git a/tests/data/TestStructure.lua b/tests/data/TestStructure.lua new file mode 100644 index 0000000..575ead5 --- /dev/null +++ b/tests/data/TestStructure.lua @@ -0,0 +1,65 @@ +{ + name = "ElvUI", + version = "13.74", + enabled = true, + debug = false, + maxRetries = 3, + scaling = 0.85, + description = "A user interface replacement for World of Warcraft", + authors = { + "Elv", + "Simpy", + "Blazeflack" + }, + emptyList = {}, + unitframes = { + enabled = true, + playerWidth = 270, + playerHeight = 54, + targetWidth = 270, + targetHeight = 54, + colors = { + health = {0.31, 0.45, 0.63}, + power = {0.0, 0.44, 0.87}, + castbar = {0.86, 0.86, 0.0} + } + }, + chat = { + fontSize = 12, + tabFontSize = 12, + panelWidth = 412, + panelHeight = 180, + fadeChat = true, + keywords = "ElvUI,Raid,Guild" + }, + actionbars = { + bar1 = { + enabled = true, + buttons = 12, + buttonsPerRow = 12, + buttonSize = 30, + buttonSpacing = 4, + backdrop = false + }, + bar2 = { + enabled = true, + buttons = 12, + buttonsPerRow = 12, + buttonSize = 30, + buttonSpacing = 4, + backdrop = false + } + }, + minimap = { + size = 175, + locationText = "SHOW" + }, + tooltip = { + cursorAnchor = false, + healthBar = true, + playerTitles = true, + guildRanks = true + }, + ["specialKey"] = "key with spaces", + unicodeNote = "Héllo Wörld" +} From ec56f8eff0562bd440d947edc4de9c9077c5ed37 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 13 Apr 2026 18:59:39 +0200 Subject: [PATCH 02/24] Add ConvertTo-Lua and ConvertFrom-Lua with tests and test data --- .../private/ConvertFrom-LuaTable.ps1 | 241 +++++- src/functions/private/ConvertTo-LuaTable.ps1 | 79 +- src/functions/private/Format-LuaKey.ps1 | 16 +- src/functions/public/Lua/ConvertFrom-Lua.ps1 | 53 +- src/functions/public/Lua/ConvertTo-Lua.ps1 | 49 +- tests/Lua.Tests.ps1 | 720 +++++++++++++++++- tests/data/DeepStructure.json | 97 +++ tests/data/DeepStructure.lua | 97 +++ 8 files changed, 1223 insertions(+), 129 deletions(-) create mode 100644 tests/data/DeepStructure.json create mode 100644 tests/data/DeepStructure.lua diff --git a/src/functions/private/ConvertFrom-LuaTable.ps1 b/src/functions/private/ConvertFrom-LuaTable.ps1 index 7540b45..545ed55 100644 --- a/src/functions/private/ConvertFrom-LuaTable.ps1 +++ b/src/functions/private/ConvertFrom-LuaTable.ps1 @@ -1,18 +1,18 @@ function ConvertFrom-LuaTable { <# .SYNOPSIS - Parses a Lua table string into a PowerShell object. + Parses a Lua table constructor string into a PowerShell object. .DESCRIPTION - Takes a Lua table string and converts it to PowerShell hashtables, arrays, + 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. Supports: - - Lua tables with string or identifier keys (converted to hashtables) + - Lua tables with string or identifier keys (converted to hashtables or PSCustomObjects) - Lua arrays/sequences (converted to arrays) - Mixed tables (keys become hashtable entries, sequential values get numeric keys) - - Strings (single and double quoted, with escape sequences) - - Numbers (integers and floats) + - Strings (single and double quoted, multi-line, with all escape sequences per §3.1) + - Numbers (integers, floats, hex, scientific notation, hex floats) - Booleans (true/false) - nil (converted to $null) - Single-line comments (-- ...) @@ -27,7 +27,11 @@ # Whether to output PSCustomObjects instead of hashtables. [Parameter()] - [switch] $AsPSCustomObject + [switch] $AsPSCustomObject, + + # Maximum allowed nesting depth. + [Parameter()] + [int] $MaxDepth = 1024 ) begin {} @@ -36,6 +40,8 @@ $script:luaString = $InputString $script:luaPos = 0 $script:luaAsPSCustomObject = $AsPSCustomObject.IsPresent + $script:luaMaxDepth = $MaxDepth + $script:luaCurrentDepth = 0 Skip-LuaWhitespace $result = Read-LuaValue @@ -143,23 +149,26 @@ function Read-LuaValue { } # Number or negative number - 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]')) { + 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.]')) { return Read-LuaNumber } - # Keywords: true, false, nil - $remaining = $script:luaString.Substring($script:luaPos) - if ($remaining -match '^true\b') { - $script:luaPos += 4 - return $true - } - if ($remaining -match '^false\b') { - $script:luaPos += 5 - return $false - } - if ($remaining -match '^nil\b') { - $script:luaPos += 3 - return $null + # 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. Only true, false, and nil are valid in a data-only context." + } + } } throw "Unexpected character '$char' at position $($script:luaPos)." @@ -171,7 +180,7 @@ function Read-LuaValue { function Read-LuaString { <# .SYNOPSIS - Reads a quoted Lua string. + Reads a quoted Lua string with full escape sequence support per §3.1. #> [OutputType([string])] [CmdletBinding()] @@ -196,13 +205,64 @@ function Read-LuaString { } $nextChar = $script:luaString[$script:luaPos] switch ($nextChar) { - 'n' { $null = $result.Append("`n") } - 'r' { $null = $result.Append("`r") } - 't' { $null = $result.Append("`t") } + 'a' { $null = $result.Append([char]7) } # bell + 'b' { $null = $result.Append("`b") } # backspace + 'f' { $null = $result.Append([char]12) } # form feed + 'n' { $null = $result.Append("`n") } # newline + 'r' { $null = $result.Append("`r") } # carriage return + 't' { $null = $result.Append("`t") } # tab + 'v' { $null = $result.Append([char]11) } # vertical tab '\' { $null = $result.Append('\') } '"' { $null = $result.Append('"') } "'" { $null = $result.Append("'") } - default { $null = $result.Append($nextChar) } + '0' { $null = $result.Append([char]0) } # null byte + 'x' { + # \xXX - two hex digits + $script:luaPos++ + if ($script:luaPos + 1 -lt $script:luaString.Length) { + $hexStr = $script:luaString.Substring($script:luaPos, 2) + $null = $result.Append([char][Convert]::ToInt32($hexStr, 16)) + $script:luaPos += 2 + continue + } + 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 } + continue + } + throw 'Invalid \u escape sequence.' + } + 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) + continue + } + # Unknown escape - just pass through + $null = $result.Append($nextChar) + } } $script:luaPos++ continue @@ -238,6 +298,15 @@ function Read-LuaMultiLineString { $script:luaPos += 2 # skip [[ $result = [System.Text.StringBuilder]::new() + # Per Lua spec, a newline immediately after [[ 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 + 1 -lt $script:luaString.Length) { if ($script:luaString[$script:luaPos] -eq ']' -and $script:luaString[$script:luaPos + 1] -eq ']') { $script:luaPos += 2 @@ -256,7 +325,7 @@ function Read-LuaMultiLineString { function Read-LuaNumber { <# .SYNOPSIS - Reads a Lua number (integer or float). + Reads a Lua number (integer, float, hex, hex float, scientific notation). #> [OutputType([object])] [CmdletBinding()] @@ -267,6 +336,7 @@ function Read-LuaNumber { process { $start = $script:luaPos $isFloat = $false + $isHex = $false if ($script:luaString[$script:luaPos] -eq '-') { $script:luaPos++ @@ -276,10 +346,30 @@ function Read-LuaNumber { 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++ @@ -305,11 +395,23 @@ function Read-LuaNumber { } $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 ($numStr -match '^-?0[xX]') { - return [int]::Parse($numStr.Substring($numStr.IndexOf('x') + 1), [System.Globalization.NumberStyles]::HexNumber) + 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)) { @@ -324,10 +426,51 @@ function Read-LuaNumber { end {} } +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 {} +} + function Read-LuaTable { <# .SYNOPSIS - Reads a Lua table and returns either an array or hashtable. + Reads a Lua table and returns either an array, hashtable, or PSCustomObject. #> [OutputType([object])] [CmdletBinding()] @@ -336,6 +479,11 @@ function Read-LuaTable { begin {} process { + $script:luaCurrentDepth++ + if ($script:luaCurrentDepth -gt $script:luaMaxDepth) { + throw "Maximum nesting depth ($($script:luaMaxDepth)) exceeded at position $($script:luaPos)." + } + $script:luaPos++ # skip { Skip-LuaWhitespace @@ -351,8 +499,9 @@ function Read-LuaTable { break } - # Check for bracket key: ["key"] = value - if ($script:luaString[$script:luaPos] -eq '[') { + # 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 @@ -388,14 +537,15 @@ function Read-LuaTable { $entries.Add(@{ Key = $ident; Value = $value }) $hasStringKeys = $true } else { - # Bare identifier as keyword value (true/false/nil) - $resolvedValue = switch ($ident) { - 'true' { $true } - 'false' { $false } - 'nil' { $null } - default { $ident } + # Bare identifier as keyword value (true/false/nil) or error + switch ($ident) { + 'true' { $arrayValues.Add($true) } + 'false' { $arrayValues.Add($false) } + 'nil' { $arrayValues.Add($null) } + default { + throw "Unexpected bare identifier '$ident' at position $identStart. Only true, false, and nil are valid in a data-only context." + } } - $arrayValues.Add($resolvedValue) $hasArrayValues = $true } } else { @@ -417,17 +567,28 @@ function Read-LuaTable { $script:luaPos++ # skip } } + $script:luaCurrentDepth-- + # Pure array (no string keys) if ($hasArrayValues -and -not $hasStringKeys) { return , [object[]]$arrayValues.ToArray() } - # Build hashtable or PSCustomObject + # Empty table + if (-not $hasArrayValues -and -not $hasStringKeys) { + if ($script:luaAsPSCustomObject) { + return [pscustomobject]@{} + } + return [ordered]@{} + } + + # Build ordered hashtable (or PSCustomObject) $table = [ordered]@{} - $arrayIndex = 1 foreach ($entry in $entries) { $table[$entry.Key] = $entry.Value } + # Mixed table: sequential values get integer keys starting at 1 + $arrayIndex = 1 foreach ($value in $arrayValues) { $table[[string]$arrayIndex] = $value $arrayIndex++ diff --git a/src/functions/private/ConvertTo-LuaTable.ps1 b/src/functions/private/ConvertTo-LuaTable.ps1 index c75b3ec..7e62ed7 100644 --- a/src/functions/private/ConvertTo-LuaTable.ps1 +++ b/src/functions/private/ConvertTo-LuaTable.ps1 @@ -5,7 +5,10 @@ .DESCRIPTION Recursively converts a PowerShell object (hashtable, array, PSCustomObject, or primitive) - into a Lua table string. This is the internal serialization engine used by ConvertTo-Lua. + 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()] @@ -15,22 +18,26 @@ [AllowNull()] [object] $InputObject, - # The current indentation depth for formatting. + # The current recursion depth. [Parameter()] - [int] $Depth = 0, + [int] $CurrentDepth = 0, - # Number of spaces per indentation level. + # Maximum allowed recursion depth. [Parameter()] - [int] $IndentSize = 4, + [int] $MaxDepth = 2, # Whether to compress the output (no newlines or indentation). [Parameter()] - [switch] $Compress + [switch] $Compress, + + # Serialize enum values as their string name instead of numeric value. + [Parameter()] + [switch] $EnumsAsStrings ) begin { - $indent = if ($Compress) { '' } else { ' ' * ($IndentSize * $Depth) } - $childIndent = if ($Compress) { '' } else { ' ' * ($IndentSize * ($Depth + 1)) } + $indent = if ($Compress) { '' } else { ' ' * (4 * $CurrentDepth) } + $childIndent = if ($Compress) { '' } else { ' ' * (4 * ($CurrentDepth + 1)) } $newline = if ($Compress) { '' } else { "`n" } $separator = if ($Compress) { ',' } else { ",`n" } } @@ -48,28 +55,47 @@ } } + # 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 [float] -or $InputObject -is [double] -or - $InputObject -is [decimal] -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] -or - $InputObject -is [single]) { + $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 "`n", '\n' -replace "`r", '\r' -replace "`t", '\t' + $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 at depth $CurrentDepth. 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) { - $value = ConvertTo-LuaTable -InputObject $item -Depth ($Depth + 1) -IndentSize $IndentSize -Compress:$Compress + $value = ConvertTo-LuaTable -InputObject $item -CurrentDepth ($CurrentDepth + 1) -MaxDepth $MaxDepth -Compress:$Compress -EnumsAsStrings:$EnumsAsStrings $items.Add("$childIndent$value") } return "{$newline$($items -join $separator)$newline$indent}" @@ -82,15 +108,23 @@ } $entries = [System.Collections.Generic.List[string]]::new() foreach ($key in $InputObject.Keys) { - $value = ConvertTo-LuaTable -InputObject $InputObject[$key] -Depth ($Depth + 1) -IndentSize $IndentSize -Compress:$Compress + $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 (from ConvertFrom-Json etc.) + # Handle PSCustomObject if ($InputObject -is [psobject]) { $properties = $InputObject.PSObject.Properties | Where-Object { $_.MemberType -eq 'NoteProperty' } if (-not $properties) { @@ -98,11 +132,18 @@ } $entries = [System.Collections.Generic.List[string]]::new() foreach ($prop in $properties) { - $value = ConvertTo-LuaTable -InputObject $prop.Value -Depth ($Depth + 1) -IndentSize $IndentSize -Compress:$Compress + # 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}" } diff --git a/src/functions/private/Format-LuaKey.ps1 b/src/functions/private/Format-LuaKey.ps1 index d4b70d3..83a0756 100644 --- a/src/functions/private/Format-LuaKey.ps1 +++ b/src/functions/private/Format-LuaKey.ps1 @@ -4,8 +4,8 @@ Formats a string as a valid Lua table key. .DESCRIPTION - Returns the key as a bare identifier if it matches Lua identifier rules, - otherwise wraps it in bracket-quote notation: ["key"]. + 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()] @@ -15,10 +15,18 @@ [string] $Key ) - begin {} + 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_]*$') { + if ($Key -match '^[a-zA-Z_][a-zA-Z0-9_]*$' -and $Key -notin $reservedWords) { return $Key } $escaped = $Key -replace '\\', '\\\\' -replace '"', '\"' diff --git a/src/functions/public/Lua/ConvertFrom-Lua.ps1 b/src/functions/public/Lua/ConvertFrom-Lua.ps1 index 6128521..4019446 100644 --- a/src/functions/public/Lua/ConvertFrom-Lua.ps1 +++ b/src/functions/public/Lua/ConvertFrom-Lua.ps1 @@ -1,16 +1,15 @@ function ConvertFrom-Lua { <# .SYNOPSIS - Converts a Lua table string to a PowerShell object. + Converts a Lua table constructor string to a PowerShell object. .DESCRIPTION - Takes a Lua table string and parses it into PowerShell objects. Lua tables with - string keys become ordered hashtables (or PSCustomObjects with -AsObject), Lua - sequences become arrays, and Lua primitives are converted to their PowerShell - equivalents. + 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) -> [ordered] hashtable or [PSCustomObject] + - Lua table (key = value) -> [PSCustomObject] or [ordered] hashtable - Lua sequence (array) -> [object[]] - Lua double-quoted string -> [string] - Lua single-quoted string -> [string] @@ -26,10 +25,9 @@ ```powershell '{ name = "Alice", age = 30 }' | ConvertFrom-Lua - Name Value - ---- ----- - name Alice - age 30 + name age + ---- --- + Alice 30 ``` .EXAMPLE @@ -43,39 +41,52 @@ .EXAMPLE ```powershell - '{ server = "localhost", port = 8080, enabled = true }' | ConvertFrom-Lua -AsObject + '{ name = "Alice" }' | ConvertFrom-Lua -AsHashtable - server port enabled - ------ ---- ------- - localhost 8080 True + Name Value + ---- ----- + name Alice ``` .NOTES - [Lua Table Documentation](https://www.lua.org/pil/2.5.html) + [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/pil/2.5.html + https://www.lua.org/manual/5.4/manual.html#3.4.9 #> [OutputType([object])] [CmdletBinding()] param( - # The Lua table string to convert to a PowerShell object. - [Parameter(Mandatory, ValueFromPipeline)] + # The Lua table constructor string to convert to a PowerShell object. + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string] $InputObject, - # Output PSCustomObjects instead of hashtables for Lua tables with string keys. + # 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()] + [int] $Depth = 1024, + + # Output arrays as a single object instead of enumerating elements through the pipeline. [Parameter()] - [switch] $AsObject + [switch] $NoEnumerate ) begin {} process { - ConvertFrom-LuaTable -InputString $InputObject -AsPSCustomObject:$AsObject + $result = ConvertFrom-LuaTable -InputString $InputObject -AsPSCustomObject:(-not $AsHashtable) -MaxDepth $Depth + if ($NoEnumerate) { + Write-Output -NoEnumerate $result + } else { + $result + } } end {} diff --git a/src/functions/public/Lua/ConvertTo-Lua.ps1 b/src/functions/public/Lua/ConvertTo-Lua.ps1 index 8e28bcd..12c2412 100644 --- a/src/functions/public/Lua/ConvertTo-Lua.ps1 +++ b/src/functions/public/Lua/ConvertTo-Lua.ps1 @@ -1,21 +1,22 @@ function ConvertTo-Lua { <# .SYNOPSIS - Converts a PowerShell object to a Lua table string. + 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 string representation. Nested structures are recursively - converted with proper indentation. + 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] / [double] / [decimal] -> Lua number + - [int] / [long] -> Lua integer + - [float] / [double] -> Lua float - [bool] -> Lua boolean (true/false) - - $null -> nil + - $null -> omitted (nil means absent in Lua) .EXAMPLE ```powershell @@ -36,46 +37,56 @@ .EXAMPLE ```powershell - [PSCustomObject]@{ server = "localhost"; port = 8080; enabled = $true } | ConvertTo-Lua + "hello" | ConvertTo-Lua -AsArray { - server = "localhost", - port = 8080, - enabled = true + "hello" } ``` .NOTES - [Lua Table Documentation](https://www.lua.org/pil/2.5.html) + [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/pil/2.5.html + https://www.lua.org/manual/5.4/manual.html#3.4.9 #> [OutputType([string])] [CmdletBinding()] param( - # The object to convert to a Lua table string. - [Parameter(Mandatory, ValueFromPipeline)] + # The object to convert to a Lua table constructor string. + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] [AllowNull()] [object] $InputObject, - # Number of spaces per indentation level. + # Max recursion depth for nested object serialization. Emits a warning when exceeded. [Parameter()] - [ValidateRange(1, 16)] - [int] $Depth = 4, + [ValidateRange(0, 100)] + [int] $Depth = 2, - # Whether to compress the output by removing whitespace and newlines. + # Omit whitespace and indentation. [Parameter()] - [switch] $Compress + [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 { - ConvertTo-LuaTable -InputObject $InputObject -Depth 0 -IndentSize $Depth -Compress:$Compress + $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 {} diff --git a/tests/Lua.Tests.ps1 b/tests/Lua.Tests.ps1 index 170c96b..05bcd4b 100644 --- a/tests/Lua.Tests.ps1 +++ b/tests/Lua.Tests.ps1 @@ -13,6 +13,7 @@ Describe 'ConvertFrom-Lua' { BeforeAll { $dataPath = Join-Path -Path $PSScriptRoot -ChildPath 'data' } + Context 'Primitives' { It 'Converts a Lua string to a PowerShell string' { $result = ConvertFrom-Lua -InputObject '"hello"' @@ -30,6 +31,12 @@ Describe 'ConvertFrom-Lua' { $result | Should -Be -7 } + It 'Converts a large integer to long' { + $result = ConvertFrom-Lua -InputObject '3000000000' + $result | Should -Be 3000000000 + $result | Should -BeOfType [long] + } + It 'Converts a Lua float to a PowerShell double' { $result = ConvertFrom-Lua -InputObject '3.14' $result | Should -Be 3.14 @@ -63,11 +70,21 @@ Describe 'ConvertFrom-Lua' { $result | Should -Be 'hello world' } - It 'Handles escape sequences in strings' { + It 'Handles escape sequences: \n \r \t' { $result = ConvertFrom-Lua -InputObject '"line1\nline2"' $result | Should -Be "line1`nline2" } + It 'Handles \r escape' { + $result = ConvertFrom-Lua -InputObject '"a\rb"' + $result | Should -Be "a`rb" + } + + It 'Handles \t escape' { + $result = ConvertFrom-Lua -InputObject '"col1\tcol2"' + $result | Should -Be "col1`tcol2" + } + It 'Handles escaped quotes in strings' { $result = ConvertFrom-Lua -InputObject '"she said \"hi\""' $result | Should -Be 'she said "hi"' @@ -78,10 +95,104 @@ Describe 'ConvertFrom-Lua' { $result | Should -Be 'path\to\file' } + It 'Handles \a (bell) escape' { + $result = ConvertFrom-Lua -InputObject '"test\abell"' + $result | Should -Be "test$([char]7)bell" + } + + It 'Handles \b (backspace) escape' { + $result = ConvertFrom-Lua -InputObject '"test\bback"' + $result | Should -Be "test`bback" + } + + It 'Handles \f (form feed) escape' { + $result = ConvertFrom-Lua -InputObject '"test\ffeed"' + $result | Should -Be "test$([char]12)feed" + } + + It 'Handles \v (vertical tab) escape' { + $result = ConvertFrom-Lua -InputObject '"test\vtab"' + $result | Should -Be "test$([char]11)tab" + } + + It 'Handles \xXX hex escape' { + $result = ConvertFrom-Lua -InputObject '"test\x41char"' + $result | Should -Be 'testAchar' + } + + It 'Handles \ddd decimal escape' { + $result = ConvertFrom-Lua -InputObject '"test\065char"' + $result | Should -Be 'testAchar' + } + + It 'Handles \u{XXXX} unicode escape' { + $result = ConvertFrom-Lua -InputObject '"test\u{0041}char"' + $result | Should -Be 'testAchar' + } + It 'Handles multi-line strings with [[ ]]' { $result = ConvertFrom-Lua -InputObject '[[hello world]]' $result | Should -Be 'hello world' } + + It 'Multi-line string strips leading newline' { + $lua = "[[$([System.Environment]::NewLine)hello]]" + $result = ConvertFrom-Lua -InputObject $lua + $result | Should -Be 'hello' + } + + It 'Handles escaped single quote in single-quoted string' { + $result = ConvertFrom-Lua -InputObject "'it'\''s'" + # Actually Lua uses \' inside single-quoted strings + $result2 = ConvertFrom-Lua -InputObject "'it\''s'" + $result2 | Should -Be "it's" + } + } + + Context 'Numbers' { + It 'Parses hex integer 0xFF' { + $result = ConvertFrom-Lua -InputObject '0xFF' + $result | Should -Be 255 + $result | Should -BeOfType [int] + } + + It 'Parses hex integer 0x1A' { + $result = ConvertFrom-Lua -InputObject '0x1A' + $result | Should -Be 26 + } + + It 'Parses scientific notation 1e10' { + $result = ConvertFrom-Lua -InputObject '1e10' + $result | Should -Be 10000000000.0 + $result | Should -BeOfType [double] + } + + It 'Parses scientific notation with negative exponent 1.5e-3' { + $result = ConvertFrom-Lua -InputObject '1.5e-3' + $result | Should -Be 0.0015 + } + + It 'Parses hex float 0x1.fp10' { + $result = ConvertFrom-Lua -InputObject '0x1.fp10' + # 0x1.f = 1 + 15/16 = 1.9375, * 2^10 = 1984 + $result | Should -Be 1984.0 + } + + It 'Parses negative hex -0xFF' { + $result = ConvertFrom-Lua -InputObject '-0xFF' + $result | Should -Be -255 + } + + It 'Parses zero' { + $result = ConvertFrom-Lua -InputObject '0' + $result | Should -Be 0 + $result | Should -BeOfType [int] + } + + It 'Parses negative float' { + $result = ConvertFrom-Lua -InputObject '-2.5' + $result | Should -Be -2.5 + } } Context 'Arrays (sequences)' { @@ -101,9 +212,13 @@ Describe 'ConvertFrom-Lua' { $result[2] | Should -Be 'c' } - It 'Converts an empty table to an empty hashtable' { + It 'Converts an empty table' { $result = ConvertFrom-Lua -InputObject '{}' - $result.Count | Should -Be 0 + if ($result -is [System.Collections.IDictionary]) { + $result.Count | Should -Be 0 + } else { + $result.PSObject.Properties.Count | Should -Be 0 + } } It 'Converts nested arrays' { @@ -113,18 +228,46 @@ Describe 'ConvertFrom-Lua' { $result[0][0] | Should -Be 1 $result[1][1] | Should -Be 4 } + + It 'Converts deeply nested arrays (3 levels)' { + $result = ConvertFrom-Lua -InputObject '{{{1, 2}, {3, 4}}, {{5, 6}}}' + $result.Count | Should -Be 2 + $result[0].Count | Should -Be 2 + $result[0][0].Count | Should -Be 2 + $result[0][0][0] | Should -Be 1 + $result[0][1][1] | Should -Be 4 + $result[1][0][0] | Should -Be 5 + } + + It 'Handles semicolons as separators' { + $result = ConvertFrom-Lua -InputObject '{1; 2; 3}' + $result.Count | Should -Be 3 + $result[0] | Should -Be 1 + $result[2] | Should -Be 3 + } + + It 'Handles trailing separator' { + $result = ConvertFrom-Lua -InputObject '{1, 2, 3,}' + $result.Count | Should -Be 3 + } + + It 'Handles mixed separators (comma and semicolon)' { + $result = ConvertFrom-Lua -InputObject '{1, 2; 3}' + $result.Count | Should -Be 3 + } } - Context 'Tables (dictionaries)' { - It 'Converts a simple key-value table' { + Context 'Tables (dictionaries) - default PSCustomObject output' { + It 'Converts a simple key-value table to PSCustomObject' { $result = ConvertFrom-Lua -InputObject '{ name = "Alice", age = 30 }' + $result | Should -BeOfType [PSCustomObject] $result.name | Should -Be 'Alice' $result.age | Should -Be 30 } It 'Converts bracket-quoted keys' { $result = ConvertFrom-Lua -InputObject '{ ["special key"] = "value" }' - $result['special key'] | Should -Be 'value' + $result.'special key' | Should -Be 'value' } It 'Converts nested tables' { @@ -138,12 +281,37 @@ Describe 'ConvertFrom-Lua' { $result.enabled | Should -BeTrue $result.debug | Should -BeFalse } + } - It 'Returns PSCustomObject when -AsObject is used' { - $result = ConvertFrom-Lua -InputObject '{ name = "Alice" }' -AsObject - $result | Should -BeOfType [PSCustomObject] + Context 'Tables - AsHashtable output' { + It 'Returns ordered hashtable when -AsHashtable is used' { + $result = ConvertFrom-Lua -InputObject '{ name = "Alice" }' -AsHashtable + $result | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.name | Should -Be 'Alice' } + + It 'AsHashtable preserves key order' { + $result = ConvertFrom-Lua -InputObject '{ a = 1, b = 2, c = 3 }' -AsHashtable + $keys = @($result.Keys) + $keys[0] | Should -Be 'a' + $keys[1] | Should -Be 'b' + $keys[2] | Should -Be 'c' + } + } + + Context 'Mixed tables' { + It 'Handles mixed tables with sequential and named keys' { + $result = ConvertFrom-Lua -InputObject '{ "a", name = "x" }' + $result.'1' | Should -Be 'a' + $result.name | Should -Be 'x' + } + + It 'Mixed table sequential keys start at 1' { + $result = ConvertFrom-Lua -InputObject '{ "first", "second", key = "val" }' -AsHashtable + $result['1'] | Should -Be 'first' + $result['2'] | Should -Be 'second' + $result['key'] | Should -Be 'val' + } } Context 'Comments' { @@ -171,6 +339,61 @@ Describe 'ConvertFrom-Lua' { $result = ConvertFrom-Lua -InputObject $lua $result.name | Should -Be 'Bob' } + + It 'Handles comment before closing brace' { + $lua = @' +{ + x = 1 + -- trailing comment +} +'@ + $result = ConvertFrom-Lua -InputObject $lua + $result.x | Should -Be 1 + } + } + + Context 'Depth limiting' { + It 'Throws when nesting exceeds -Depth' { + $lua = '{ a = { b = { c = 1 } } }' + { ConvertFrom-Lua -InputObject $lua -Depth 2 } | Should -Throw '*depth*' + } + + It 'Allows nesting within -Depth limit' { + $lua = '{ a = { b = 1 } }' + $result = ConvertFrom-Lua -InputObject $lua -Depth 5 + $result.a.b | Should -Be 1 + } + } + + Context 'NoEnumerate' { + It 'Without -NoEnumerate, arrays are enumerated' { + $result = @(ConvertFrom-Lua -InputObject '{1, 2, 3}') + $result.Count | Should -Be 3 + } + + It 'With -NoEnumerate, arrays are returned as single object' { + $result = ConvertFrom-Lua -InputObject '{1, 2, 3}' -NoEnumerate + , $result | Should -HaveCount 1 + $result.Count | Should -Be 3 + } + } + + Context 'Error cases' { + It 'Throws on bare identifier (variable reference)' { + { ConvertFrom-Lua -InputObject 'someVariable' } | Should -Throw '*bare identifier*' + } + + It 'Throws on bare identifier inside table' { + { ConvertFrom-Lua -InputObject '{ myVar }' } | Should -Throw '*bare identifier*' + } + + It 'Throws on unterminated string' { + { ConvertFrom-Lua -InputObject '"hello' } | Should -Throw '*Unterminated*' + } + + It 'Throws on unterminated multi-line string' { + { ConvertFrom-Lua -InputObject '[[hello' } | Should -Throw '*Unterminated*' + } } Context 'Pipeline input' { @@ -288,7 +511,7 @@ Describe 'ConvertFrom-Lua' { It 'Parses bracket-quoted keys' { $result = ConvertFrom-Lua -InputObject $luaContent - $result['specialKey'] | Should -Be $expected.specialKey + $result.specialKey | Should -Be $expected.specialKey } It 'Parses unicode strings' { @@ -296,6 +519,84 @@ Describe 'ConvertFrom-Lua' { $result.unicodeNote | Should -Be $expected.unicodeNote } } + + Context 'File-based test: DeepStructure' { + BeforeAll { + $luaContent = Get-Content -Path (Join-Path $dataPath 'DeepStructure.lua') -Raw + $expected = Get-Content -Path (Join-Path $dataPath 'DeepStructure.json') -Raw | ConvertFrom-Json + } + + It 'Parses 5-level deep nested value' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.level1.level2.level3.level4.level5.value | Should -Be 'deep' + $result.level1.level2.level3.level4.level5.count | Should -Be 42 + $result.level1.level2.level3.level4.level5.active | Should -BeTrue + } + + It 'Parses sibling at level 4' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.level1.level2.level3.level4.sibling | Should -Be 'level4-sibling' + } + + It 'Parses arrays of objects inside deep nesting' { + $result = ConvertFrom-Lua -InputObject $luaContent + $items = $result.level1.level2.level3.items + $items.Count | Should -Be 2 + $items[0].id | Should -Be 1 + $items[0].name | Should -Be 'item1' + $items[0].tags.Count | Should -Be 2 + $items[0].tags[0] | Should -Be 'alpha' + $items[1].tags[0] | Should -Be 'gamma' + } + + It 'Parses nested metadata' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.level1.level2.metadata.created | Should -Be '2024-01-15' + $result.level1.level2.metadata.modified | Should -Be '2024-06-20' + } + + It 'Parses deep config with parallel branches' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.config.database.primary.host | Should -Be 'db1.example.com' + $result.config.database.primary.port | Should -Be 5432 + $result.config.database.primary.options.ssl | Should -BeTrue + $result.config.database.primary.options.pool.min | Should -Be 5 + $result.config.database.primary.options.pool.max | Should -Be 20 + + $result.config.database.replica.host | Should -Be 'db2.example.com' + $result.config.database.replica.options.pool.min | Should -Be 2 + $result.config.database.replica.options.pool.idle | Should -Be 30000 + } + + It 'Parses cache config with array of backend objects' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.config.cache.enabled | Should -BeTrue + $result.config.cache.ttl | Should -Be 3600 + $result.config.cache.backends.Count | Should -Be 2 + $result.config.cache.backends[0].type | Should -Be 'memory' + $result.config.cache.backends[0].maxSize | Should -Be 1048576 + $result.config.cache.backends[1].type | Should -Be 'redis' + $result.config.cache.backends[1].host | Should -Be 'cache.example.com' + } + + It 'Parses matrix (array of arrays)' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.matrix.Count | Should -Be 3 + $result.matrix[0].Count | Should -Be 3 + $result.matrix[0][0] | Should -Be 1 + $result.matrix[1][1] | Should -Be 5 + $result.matrix[2][2] | Should -Be 9 + } + + It 'Parses mixed-depth structure' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.mixedDepth.shallow | Should -Be 'yes' + $result.mixedDepth.deep.deeper.deepest.array.Count | Should -Be 3 + $result.mixedDepth.deep.deeper.deepest.array[0] | Should -Be 10 + $result.mixedDepth.deep.deeper.deepest.nested.flag | Should -BeFalse + $result.mixedDepth.deep.deeper.deepest.nested.label | Should -Be 'end' + } + } } Describe 'ConvertTo-Lua' { @@ -360,6 +661,31 @@ Describe 'ConvertTo-Lua' { $result = ConvertTo-Lua -InputObject "col1`tcol2" $result | Should -Be '"col1\tcol2"' } + + It 'Escapes carriage returns' { + $result = ConvertTo-Lua -InputObject "a`rb" + $result | Should -Be '"a\rb"' + } + } + + Context 'Null omission' { + It 'Omits $null values from hashtable output' { + $result = ConvertTo-Lua -InputObject ([ordered]@{ a = 1; b = $null; c = 3 }) -Compress + $result | Should -Be '{a=1,c=3}' + } + + It 'Omits $null values from PSCustomObject output' { + $obj = [PSCustomObject]@{ name = 'test'; removed = $null; value = 5 } + $result = ConvertTo-Lua -InputObject $obj -Compress + $result | Should -Match 'name="test"' + $result | Should -Match 'value=5' + $result | Should -Not -Match 'removed' + } + + It 'All-null hashtable becomes empty table' { + $result = ConvertTo-Lua -InputObject @{ a = $null; b = $null } -Compress + $result | Should -Be '{}' + } } Context 'Arrays' { @@ -409,6 +735,30 @@ Describe 'ConvertTo-Lua' { } } + Context 'Reserved words as keys' { + It 'Uses bracket notation for Lua reserved word keys' { + $result = ConvertTo-Lua -InputObject ([ordered]@{ 'return' = 1; 'end' = 2; name = 'ok' }) -Compress + $result | Should -Match '\["return"\]=1' + $result | Should -Match '\["end"\]=2' + $result | Should -Match 'name="ok"' + } + + It 'Uses bracket notation for "true" as a key' { + $result = ConvertTo-Lua -InputObject @{ 'true' = 'yes' } -Compress + $result | Should -Be '{["true"]="yes"}' + } + + It 'Uses bracket notation for "nil" as a key' { + $result = ConvertTo-Lua -InputObject @{ 'nil' = 'nothing' } -Compress + $result | Should -Be '{["nil"]="nothing"}' + } + + It 'Uses bracket notation for "while" as a key' { + $result = ConvertTo-Lua -InputObject @{ 'while' = 'loop' } -Compress + $result | Should -Be '{["while"]="loop"}' + } + } + Context 'PSCustomObject' { It 'Converts a PSCustomObject (compressed)' { $obj = [PSCustomObject]@{ name = 'test'; value = 42 } @@ -418,18 +768,91 @@ Describe 'ConvertTo-Lua' { } } + Context 'Depth limiting' { + It 'Serializes up to max depth without warning' { + $obj = [ordered]@{ a = [ordered]@{ b = 1 } } + # Depth 2 allows two levels of nesting + $result = ConvertTo-Lua -InputObject $obj -Depth 2 -Compress + $result | Should -Be '{a={b=1}}' + } + + It 'Emits warning and truncates when depth exceeded' { + $obj = [ordered]@{ a = [ordered]@{ b = [ordered]@{ c = 1 } } } + $result = ConvertTo-Lua -InputObject $obj -Depth 1 -Compress 3>&1 + # The result should contain the warning and the truncated output + $warnings = @($result | Where-Object { $_ -is [System.Management.Automation.WarningRecord] }) + $warnings.Count | Should -BeGreaterThan 0 + } + + It 'Depth 0 serializes only primitives, truncates complex types' { + $obj = [ordered]@{ a = 1 } + $result = ConvertTo-Lua -InputObject $obj -Depth 0 -Compress 3>&1 + $warnings = @($result | Where-Object { $_ -is [System.Management.Automation.WarningRecord] }) + $warnings.Count | Should -BeGreaterThan 0 + } + } + + Context 'AsArray' { + It 'Wraps a single string value in a sequence table' { + $result = ConvertTo-Lua -InputObject 'hello' -AsArray -Compress + $result | Should -Be '{"hello"}' + } + + It 'Wraps a single integer in a sequence table' { + $result = ConvertTo-Lua -InputObject 42 -AsArray -Compress + $result | Should -Be '{42}' + } + + It 'Does not double-wrap an array' { + $result = ConvertTo-Lua -InputObject @(1, 2) -AsArray -Compress + $result | Should -Be '{1,2}' + } + } + + Context 'EnumsAsStrings' { + It 'Serializes enum as numeric value by default' { + $result = ConvertTo-Lua -InputObject ([System.DayOfWeek]::Monday) + $result | Should -Be '1' + } + + It 'Serializes enum as string with -EnumsAsStrings' { + $result = ConvertTo-Lua -InputObject ([System.DayOfWeek]::Monday) -EnumsAsStrings + $result | Should -Be '"Monday"' + } + } + + Context 'Indentation' { + It 'Uses 4-space indentation by default' { + $result = ConvertTo-Lua -InputObject @(1) + $lines = $result -split "`n" + $lines[1] | Should -Match '^ 1$' + } + + It 'Compress removes all whitespace and newlines' { + $result = ConvertTo-Lua -InputObject ([ordered]@{ a = 1; b = 2 }) -Compress + $result | Should -Not -Match "`n" + $result | Should -Not -Match ' ' + } + } + Context 'Pipeline input' { It 'Accepts input from the pipeline' { $result = @{ x = 10 } | ConvertTo-Lua -Compress $result | Should -Be '{x=10}' } } +} + +Describe 'Round-trip conversion' { + BeforeAll { + $dataPath = Join-Path -Path $PSScriptRoot -ChildPath 'data' + } - Context 'Round-trip conversion' { + Context 'Simple round-trips' { It 'Round-trips a simple hashtable' { $original = [ordered]@{ name = 'test'; count = 5; active = $true } $lua = ConvertTo-Lua -InputObject $original - $result = ConvertFrom-Lua -InputObject $lua + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable $result.name | Should -Be $original.name $result.count | Should -Be $original.count $result.active | Should -Be $original.active @@ -445,7 +868,41 @@ Describe 'ConvertTo-Lua' { } } - It 'Round-trips nested structures' { + It 'Round-trips a string array' { + $original = @('alpha', 'beta', 'gamma') + $lua = ConvertTo-Lua -InputObject $original + $result = ConvertFrom-Lua -InputObject $lua + $result.Count | Should -Be 3 + $result[0] | Should -Be 'alpha' + $result[2] | Should -Be 'gamma' + } + + It 'Round-trips booleans in an array' { + $original = @($true, $false, $true) + $lua = ConvertTo-Lua -InputObject $original + $result = ConvertFrom-Lua -InputObject $lua + $result[0] | Should -BeTrue + $result[1] | Should -BeFalse + $result[2] | Should -BeTrue + } + + It 'Round-trips empty hashtable' { + $original = [ordered]@{} + $lua = ConvertTo-Lua -InputObject $original + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.Count | Should -Be 0 + } + + It 'Round-trips empty array' { + $original = @() + $lua = ConvertTo-Lua -InputObject $original + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.Count | Should -Be 0 + } + } + + Context 'Nested round-trips' { + It 'Round-trips 2-level nested hashtable' { $original = [ordered]@{ server = 'localhost' port = 8080 @@ -455,36 +912,247 @@ Describe 'ConvertTo-Lua' { } } $lua = ConvertTo-Lua -InputObject $original - $result = ConvertFrom-Lua -InputObject $lua + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable $result.server | Should -Be 'localhost' $result.port | Should -Be 8080 $result.options.debug | Should -BeFalse $result.options.verbose | Should -BeTrue } - It 'Round-trips from JSON reference to Lua and back' { + It 'Round-trips 3-level nested structure' { + $original = [ordered]@{ + a = [ordered]@{ + b = [ordered]@{ + c = 'deep' + num = 99 + flag = $true + } + } + } + $lua = ConvertTo-Lua -InputObject $original -Depth 5 + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.a.b.c | Should -Be 'deep' + $result.a.b.num | Should -Be 99 + $result.a.b.flag | Should -BeTrue + } + + It 'Round-trips 5-level deep structure' { + $original = [ordered]@{ + l1 = [ordered]@{ + l2 = [ordered]@{ + l3 = [ordered]@{ + l4 = [ordered]@{ + l5 = 'bottom' + } + } + } + } + } + $lua = ConvertTo-Lua -InputObject $original -Depth 10 + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.l1.l2.l3.l4.l5 | Should -Be 'bottom' + } + + It 'Round-trips nested arrays of arrays' { + $original = @( + @(1, 2, 3), + @(4, 5, 6), + @(7, 8, 9) + ) + $lua = ConvertTo-Lua -InputObject $original -Depth 5 + $result = ConvertFrom-Lua -InputObject $lua + $result.Count | Should -Be 3 + $result[0].Count | Should -Be 3 + $result[0][0] | Should -Be 1 + $result[1][1] | Should -Be 5 + $result[2][2] | Should -Be 9 + } + + It 'Round-trips array of hashtables' { + $original = @( + [ordered]@{ id = 1; name = 'first' }, + [ordered]@{ id = 2; name = 'second' } + ) + $lua = ConvertTo-Lua -InputObject $original -Depth 5 + $result = ConvertFrom-Lua -InputObject $lua + $result.Count | Should -Be 2 + $result[0].id | Should -Be 1 + $result[0].name | Should -Be 'first' + $result[1].id | Should -Be 2 + $result[1].name | Should -Be 'second' + } + + It 'Round-trips hashtable with array values at multiple levels' { + $original = [ordered]@{ + tags = @('a', 'b', 'c') + nested = [ordered]@{ + scores = @(10, 20, 30) + deep = [ordered]@{ + items = @('x', 'y') + } + } + } + $lua = ConvertTo-Lua -InputObject $original -Depth 10 + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.tags.Count | Should -Be 3 + $result.tags[0] | Should -Be 'a' + $result.nested.scores.Count | Should -Be 3 + $result.nested.scores[1] | Should -Be 20 + $result.nested.deep.items.Count | Should -Be 2 + $result.nested.deep.items[0] | Should -Be 'x' + } + } + + Context 'Complex deep structure round-trips' { + It 'Round-trips a full config-like structure (5+ levels)' { + $original = [ordered]@{ + app = [ordered]@{ + name = 'MyApp' + version = '2.0' + modules = [ordered]@{ + auth = [ordered]@{ + enabled = $true + provider = [ordered]@{ + type = 'oauth' + settings = [ordered]@{ + clientId = 'abc123' + scopes = @('read', 'write', 'admin') + } + } + } + logging = [ordered]@{ + level = 'info' + outputs = @( + [ordered]@{ type = 'console'; colored = $true }, + [ordered]@{ type = 'file'; path = '/var/log/app.log' } + ) + } + } + } + database = [ordered]@{ + connections = @( + [ordered]@{ + name = 'primary' + options = [ordered]@{ + pool = [ordered]@{ min = 5; max = 20 } + } + } + ) + } + } + $lua = ConvertTo-Lua -InputObject $original -Depth 20 + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.app.name | Should -Be 'MyApp' + $result.app.modules.auth.enabled | Should -BeTrue + $result.app.modules.auth.provider.type | Should -Be 'oauth' + $result.app.modules.auth.provider.settings.clientId | Should -Be 'abc123' + $result.app.modules.auth.provider.settings.scopes.Count | Should -Be 3 + $result.app.modules.auth.provider.settings.scopes[2] | Should -Be 'admin' + $result.app.modules.logging.outputs.Count | Should -Be 2 + $result.app.modules.logging.outputs[0].type | Should -Be 'console' + $result.app.modules.logging.outputs[1].path | Should -Be '/var/log/app.log' + $result.database.connections[0].name | Should -Be 'primary' + $result.database.connections[0].options.pool.min | Should -Be 5 + } + + It 'Round-trips from JSON DeepStructure to Lua and back' { + $jsonPath = Join-Path $dataPath 'DeepStructure.json' + $expected = Get-Content -Path $jsonPath -Raw | ConvertFrom-Json + $lua = ConvertTo-Lua -InputObject $expected -Depth 20 + $result = ConvertFrom-Lua -InputObject $lua + $result.level1.level2.level3.level4.level5.value | Should -Be $expected.level1.level2.level3.level4.level5.value + $result.level1.level2.level3.level4.level5.count | Should -Be $expected.level1.level2.level3.level4.level5.count + $result.level1.level2.level3.level4.sibling | Should -Be $expected.level1.level2.level3.level4.sibling + $result.config.database.primary.options.pool.max | Should -Be $expected.config.database.primary.options.pool.max + $result.config.database.replica.options.pool.idle | Should -Be $expected.config.database.replica.options.pool.idle + $result.config.cache.backends.Count | Should -Be 2 + $result.matrix.Count | Should -Be 3 + $result.matrix[1][1] | Should -Be 5 + $result.mixedDepth.deep.deeper.deepest.nested.flag | Should -BeFalse + } + + It 'Round-trips from JSON TestStructure to Lua and back' { $jsonPath = Join-Path $dataPath 'TestStructure.json' $expected = Get-Content -Path $jsonPath -Raw | ConvertFrom-Json - $lua = ConvertTo-Lua -InputObject $expected + $lua = ConvertTo-Lua -InputObject $expected -Depth 10 $result = ConvertFrom-Lua -InputObject $lua $result.name | Should -Be $expected.name $result.enabled | Should -Be $expected.enabled $result.maxRetries | Should -Be $expected.maxRetries $result.authors.Count | Should -Be $expected.authors.Count + $result.unitframes.colors.health.Count | Should -Be 3 + $result.actionbars.bar1.buttons | Should -Be $expected.actionbars.bar1.buttons } - } - Context 'Indentation' { - It 'Uses custom indent size' { - $result = ConvertTo-Lua -InputObject @(1) -Depth 2 - $lines = $result -split "`n" - $lines[1] | Should -Match '^ 1$' + It 'Round-trips compressed output' { + $original = [ordered]@{ + a = @(1, 2) + b = [ordered]@{ c = 'x' } + } + $lua = ConvertTo-Lua -InputObject $original -Compress -Depth 5 + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.a.Count | Should -Be 2 + $result.a[0] | Should -Be 1 + $result.b.c | Should -Be 'x' } - It 'Compress removes all whitespace and newlines' { - $result = ConvertTo-Lua -InputObject ([ordered]@{ a = 1; b = 2 }) -Compress - $result | Should -Not -Match "`n" - $result | Should -Not -Match ' ' + It 'Round-trips with -NoEnumerate preserving array wrapper' { + $lua = '{1, 2, 3}' + $result = ConvertFrom-Lua -InputObject $lua -NoEnumerate + $roundTripped = ConvertTo-Lua -InputObject $result -Compress + $roundTripped | Should -Be '{1,2,3}' + } + + It 'Round-trips strings with special characters' { + $original = [ordered]@{ + escaped = "line1`nline2`ttab" + quoted = 'she said "hi"' + backslash = 'C:\path\to\file' + } + $lua = ConvertTo-Lua -InputObject $original -Depth 5 + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.escaped | Should -Be "line1`nline2`ttab" + $result.quoted | Should -Be 'she said "hi"' + $result.backslash | Should -Be 'C:\path\to\file' + } + + It 'Round-trips unicode strings' { + $original = [ordered]@{ + greeting = 'Héllo Wörld' + emoji = 'test' + cjk = '日本語' + } + $lua = ConvertTo-Lua -InputObject $original -Depth 5 + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.greeting | Should -Be 'Héllo Wörld' + $result.cjk | Should -Be '日本語' + } + + It 'Round-trips deeply nested array-of-objects-with-arrays' { + $original = @( + [ordered]@{ + name = 'group1' + items = @( + [ordered]@{ id = 1; tags = @('a', 'b') }, + [ordered]@{ id = 2; tags = @('c') } + ) + }, + [ordered]@{ + name = 'group2' + items = @( + [ordered]@{ id = 3; tags = @('d', 'e', 'f') } + ) + } + ) + $lua = ConvertTo-Lua -InputObject $original -Depth 10 + $result = ConvertFrom-Lua -InputObject $lua + $result.Count | Should -Be 2 + $result[0].name | Should -Be 'group1' + $result[0].items.Count | Should -Be 2 + $result[0].items[0].tags.Count | Should -Be 2 + $result[0].items[0].tags[0] | Should -Be 'a' + $result[1].items[0].tags.Count | Should -Be 3 + $result[1].items[0].tags[2] | Should -Be 'f' } } } diff --git a/tests/data/DeepStructure.json b/tests/data/DeepStructure.json new file mode 100644 index 0000000..d36e87d --- /dev/null +++ b/tests/data/DeepStructure.json @@ -0,0 +1,97 @@ +{ + "level1": { + "level2": { + "level3": { + "level4": { + "level5": { + "value": "deep", + "count": 42, + "active": true + }, + "sibling": "level4-sibling" + }, + "items": [ + { + "id": 1, + "name": "item1", + "tags": ["alpha", "beta"] + }, + { + "id": 2, + "name": "item2", + "tags": ["gamma"] + } + ] + }, + "metadata": { + "created": "2024-01-15", + "modified": "2024-06-20" + } + }, + "name": "root-child" + }, + "config": { + "database": { + "primary": { + "host": "db1.example.com", + "port": 5432, + "options": { + "ssl": true, + "timeout": 30, + "pool": { + "min": 5, + "max": 20, + "idle": 10000 + } + } + }, + "replica": { + "host": "db2.example.com", + "port": 5432, + "options": { + "ssl": true, + "timeout": 60, + "pool": { + "min": 2, + "max": 10, + "idle": 30000 + } + } + } + }, + "cache": { + "enabled": true, + "ttl": 3600, + "backends": [ + { + "type": "memory", + "maxSize": 1048576 + }, + { + "type": "redis", + "host": "cache.example.com", + "port": 6379 + } + ] + } + }, + "matrix": [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] + ], + "mixedDepth": { + "shallow": "yes", + "deep": { + "deeper": { + "deepest": { + "array": [10, 20, 30], + "nested": { + "flag": false, + "label": "end" + } + } + } + } + } +} diff --git a/tests/data/DeepStructure.lua b/tests/data/DeepStructure.lua new file mode 100644 index 0000000..14ce88e --- /dev/null +++ b/tests/data/DeepStructure.lua @@ -0,0 +1,97 @@ +{ + level1 = { + level2 = { + level3 = { + level4 = { + level5 = { + value = "deep", + count = 42, + active = true + }, + sibling = "level4-sibling" + }, + items = { + { + id = 1, + name = "item1", + tags = {"alpha", "beta"} + }, + { + id = 2, + name = "item2", + tags = {"gamma"} + } + } + }, + metadata = { + created = "2024-01-15", + modified = "2024-06-20" + } + }, + name = "root-child" + }, + config = { + database = { + primary = { + host = "db1.example.com", + port = 5432, + options = { + ssl = true, + timeout = 30, + pool = { + min = 5, + max = 20, + idle = 10000 + } + } + }, + replica = { + host = "db2.example.com", + port = 5432, + options = { + ssl = true, + timeout = 60, + pool = { + min = 2, + max = 10, + idle = 30000 + } + } + } + }, + cache = { + enabled = true, + ttl = 3600, + backends = { + { + type = "memory", + maxSize = 1048576 + }, + { + type = "redis", + host = "cache.example.com", + port = 6379 + } + } + } + }, + matrix = { + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9} + }, + mixedDepth = { + shallow = "yes", + deep = { + deeper = { + deepest = { + array = {10, 20, 30}, + nested = { + flag = false, + label = "end" + } + } + } + } + } +} From 00ae38df22387dbaadb256c170726428352b831a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:37:34 +0200 Subject: [PATCH 03/24] Bump PSModule/Process-PSModule/.github/workflows/workflow.yml (#1) Bumps [PSModule/Process-PSModule/.github/workflows/workflow.yml](https://github.com/psmodule/process-psmodule) from 5.4.3 to 5.4.7. - [Release notes](https://github.com/psmodule/process-psmodule/releases) - [Commits](https://github.com/psmodule/process-psmodule/compare/60bdf8a5a4c92c53fcf2a8d23f7d5f5c93e6864e...11117919e65242d3388727819a751f74ad24ea9e) --- updated-dependencies: - dependency-name: PSModule/Process-PSModule/.github/workflows/workflow.yml dependency-version: 5.4.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/Process-PSModule.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml index f442eda..dceb2e0 100644 --- a/.github/workflows/Process-PSModule.yml +++ b/.github/workflows/Process-PSModule.yml @@ -27,6 +27,6 @@ permissions: jobs: Process-PSModule: - uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@60bdf8a5a4c92c53fcf2a8d23f7d5f5c93e6864e # v5.4.3 + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@11117919e65242d3388727819a751f74ad24ea9e # v5.5.0 secrets: APIKEY: ${{ secrets.APIKEY }} From 9f0a48a274e0aa2d669c870d092ff9ebb501d2cd Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 13 Apr 2026 23:27:11 +0200 Subject: [PATCH 04/24] Refactor Lua conversion functions and add new parsing capabilities - Improved string escaping in ConvertTo-LuaTable for better handling of special characters. - Enhanced depth warning messages for better clarity. - Simplified parameter passing in ConvertTo-LuaTable calls. - Updated ConvertFrom-Lua to use array syntax for output. - Refactored ConvertTo-Lua to improve readability and maintainability. - Removed obsolete header and manifest files. - Updated tests for better clarity and consistency. - Added new Lua parsing functions: Read-LuaHexFloat, Read-LuaMultiLineString, Read-LuaNumber, Read-LuaString, Read-LuaTable, Read-LuaValue, and Skip-LuaWhitespace for comprehensive Lua support. - Adjusted data files to conform to new structure and formatting. --- .github/linters/.codespellrc | 2 +- src/README.md | 3 - .../private/ConvertFrom-LuaTable.ps1 | 573 +----------------- src/functions/private/ConvertTo-LuaTable.ps1 | 39 +- src/functions/private/Read-LuaHexFloat.ps1 | 56 ++ .../private/Read-LuaMultiLineString.ps1 | 40 ++ src/functions/private/Read-LuaNumber.ps1 | 131 ++++ src/functions/private/Read-LuaString.ps1 | 145 +++++ src/functions/private/Read-LuaTable.ps1 | 151 +++++ src/functions/private/Read-LuaValue.ps1 | 82 +++ src/functions/private/Skip-LuaWhitespace.ps1 | 55 ++ src/functions/public/Lua/ConvertFrom-Lua.ps1 | 2 +- src/functions/public/Lua/ConvertTo-Lua.ps1 | 6 +- src/header.ps1 | 2 - src/manifest.psd1 | 6 - tests/Lua.Tests.ps1 | 35 +- tests/data/Arrays.lua | 2 +- tests/data/DeepStructure.json | 33 +- tests/data/DeepStructure.lua | 2 +- tests/data/Strings.lua | 2 +- tests/data/TestStructure.lua | 2 +- 21 files changed, 760 insertions(+), 609 deletions(-) delete mode 100644 src/README.md create mode 100644 src/functions/private/Read-LuaHexFloat.ps1 create mode 100644 src/functions/private/Read-LuaMultiLineString.ps1 create mode 100644 src/functions/private/Read-LuaNumber.ps1 create mode 100644 src/functions/private/Read-LuaString.ps1 create mode 100644 src/functions/private/Read-LuaTable.ps1 create mode 100644 src/functions/private/Read-LuaValue.ps1 create mode 100644 src/functions/private/Skip-LuaWhitespace.ps1 delete mode 100644 src/header.ps1 delete mode 100644 src/manifest.psd1 diff --git a/.github/linters/.codespellrc b/.github/linters/.codespellrc index 351e9a0..b327649 100644 --- a/.github/linters/.codespellrc +++ b/.github/linters/.codespellrc @@ -1,3 +1,3 @@ [codespell] skip = ./.github/linters -ignore-words-list = afterall +ignore-words-list = afterall,simpy diff --git a/src/README.md b/src/README.md deleted file mode 100644 index af76160..0000000 --- a/src/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Details - -For more info about the expected structure of a module repository, please refer to [Build-PSModule](https://github.com/PSModule/Build-PSModule) diff --git a/src/functions/private/ConvertFrom-LuaTable.ps1 b/src/functions/private/ConvertFrom-LuaTable.ps1 index 545ed55..84b372a 100644 --- a/src/functions/private/ConvertFrom-LuaTable.ps1 +++ b/src/functions/private/ConvertFrom-LuaTable.ps1 @@ -4,19 +4,9 @@ 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. - - Supports: - - Lua tables with string or identifier keys (converted to hashtables or PSCustomObjects) - - Lua arrays/sequences (converted to arrays) - - Mixed tables (keys become hashtable entries, sequential values get numeric keys) - - Strings (single and double quoted, multi-line, with all escape sequences per §3.1) - - Numbers (integers, floats, hex, scientific notation, hex floats) - - Booleans (true/false) - - nil (converted to $null) - - Single-line comments (-- ...) - - Multi-line comments (--[[ ... ]]) + 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()] @@ -44,560 +34,21 @@ $script:luaCurrentDepth = 0 Skip-LuaWhitespace - $result = Read-LuaValue - - return $result - } - - end {} -} - -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 --[[ ... ]] - if ($script:luaPos + 1 -lt $script:luaString.Length -and - $script:luaString[$script:luaPos] -eq '[' -and - $script:luaString[$script:luaPos + 1] -eq '[') { - $script:luaPos += 2 - while ($script:luaPos + 1 -lt $script:luaString.Length) { - if ($script:luaString[$script:luaPos] -eq ']' -and - $script:luaString[$script:luaPos + 1] -eq ']') { - $script:luaPos += 2 - break - } - $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 {} -} - -function Read-LuaValue { - <# - .SYNOPSIS - Reads a single Lua value from the current parser position. - #> - [OutputType([object])] - [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 [[ ... ]] - if ($char -eq '[' -and $script:luaPos + 1 -lt $script:luaString.Length -and - $script:luaString[$script:luaPos + 1] -eq '[') { - return Read-LuaMultiLineString - } - - # Number or negative number - 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.]')) { - 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. Only true, false, and nil are valid in a data-only context." - } - } - } - - throw "Unexpected character '$char' at position $($script:luaPos)." - } - - end {} -} - -function Read-LuaString { - <# - .SYNOPSIS - Reads a quoted Lua string with full escape sequence support per §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) } # bell - 'b' { $null = $result.Append("`b") } # backspace - 'f' { $null = $result.Append([char]12) } # form feed - 'n' { $null = $result.Append("`n") } # newline - 'r' { $null = $result.Append("`r") } # carriage return - 't' { $null = $result.Append("`t") } # tab - 'v' { $null = $result.Append([char]11) } # vertical tab - '\' { $null = $result.Append('\') } - '"' { $null = $result.Append('"') } - "'" { $null = $result.Append("'") } - '0' { $null = $result.Append([char]0) } # null byte - 'x' { - # \xXX - two hex digits - $script:luaPos++ - if ($script:luaPos + 1 -lt $script:luaString.Length) { - $hexStr = $script:luaString.Substring($script:luaPos, 2) - $null = $result.Append([char][Convert]::ToInt32($hexStr, 16)) - $script:luaPos += 2 - continue - } - 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 } - continue - } - throw 'Invalid \u escape sequence.' - } - 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) - continue - } - # 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 {} -} - -function Read-LuaMultiLineString { - <# - .SYNOPSIS - Reads a multi-line Lua string delimited by [[ and ]]. - #> - [OutputType([string])] - [CmdletBinding()] - param() - - begin {} - - process { - $script:luaPos += 2 # skip [[ - $result = [System.Text.StringBuilder]::new() - - # Per Lua spec, a newline immediately after [[ 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 + 1 -lt $script:luaString.Length) { - if ($script:luaString[$script:luaPos] -eq ']' -and $script:luaString[$script:luaPos + 1] -eq ']') { - $script:luaPos += 2 - return $result.ToString() - } - $null = $result.Append($script:luaString[$script:luaPos]) - $script:luaPos++ - } - - throw 'Unterminated multi-line string.' - } - - end {} -} - -function Read-LuaNumber { - <# - .SYNOPSIS - Reads a Lua number (integer, float, hex, hex float, scientific notation). - #> - [OutputType([object])] - [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 {} -} - -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 {} -} - -function Read-LuaTable { - <# - .SYNOPSIS - Reads a Lua table and returns either an array, hashtable, or PSCustomObject. - #> - [OutputType([object])] - [CmdletBinding()] - param() - - begin {} - - process { - $script:luaCurrentDepth++ - if ($script:luaCurrentDepth -gt $script:luaMaxDepth) { - throw "Maximum nesting depth ($($script:luaMaxDepth)) exceeded at position $($script:luaPos)." - } - - $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 -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq ']') { - $script:luaPos++ # skip ] - } - Skip-LuaWhitespace - if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq '=') { - $script:luaPos++ # skip = - } + # 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 - $value = Read-LuaValue - $entries.Add(@{ Key = [string]$key; Value = $value }) - $hasStringKeys = $true - } - # Check for identifier key: key = value - elseif ($script:luaString[$script:luaPos] -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) - - 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 (true/false/nil) or error - switch ($ident) { - 'true' { $arrayValues.Add($true) } - 'false' { $arrayValues.Add($false) } - 'nil' { $arrayValues.Add($null) } - default { - throw "Unexpected bare identifier '$ident' at position $identStart. Only true, false, and nil are valid in a data-only context." - } - } - $hasArrayValues = $true - } - } else { - # Array value - $value = Read-LuaValue - $arrayValues.Add($value) - $hasArrayValues = $true - } - - Skip-LuaWhitespace - - # Skip comma or semicolon separator - if ($script:luaPos -lt $script:luaString.Length -and ($script:luaString[$script:luaPos] -eq ',' -or $script:luaString[$script:luaPos] -eq ';')) { - $script:luaPos++ } } - 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 ($value in $arrayValues) { - $table[[string]$arrayIndex] = $value - $arrayIndex++ - } + $result = Read-LuaValue - if ($script:luaAsPSCustomObject) { - return [pscustomobject]$table - } - return $table + return $result } end {} diff --git a/src/functions/private/ConvertTo-LuaTable.ps1 b/src/functions/private/ConvertTo-LuaTable.ps1 index 7e62ed7..1da24c5 100644 --- a/src/functions/private/ConvertTo-LuaTable.ps1 +++ b/src/functions/private/ConvertTo-LuaTable.ps1 @@ -78,14 +78,26 @@ } 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' + $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 at depth $CurrentDepth. Serializing remaining object as string." - $str = $InputObject.ToString() -replace '\\', '\\\\' -replace '"', '\"' + Write-Warning "Depth limit ($MaxDepth) exceeded. Serializing remaining object as string." + $str = $InputObject.ToString() ` + -replace '\\', '\\\\' ` + -replace '"', '\"' return "`"$str`"" } @@ -95,7 +107,14 @@ } $items = [System.Collections.Generic.List[string]]::new() foreach ($item in $InputObject) { - $value = ConvertTo-LuaTable -InputObject $item -CurrentDepth ($CurrentDepth + 1) -MaxDepth $MaxDepth -Compress:$Compress -EnumsAsStrings:$EnumsAsStrings + $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}" @@ -113,7 +132,11 @@ if ($null -eq $val) { continue } - $value = ConvertTo-LuaTable -InputObject $val -CurrentDepth ($CurrentDepth + 1) -MaxDepth $MaxDepth -Compress:$Compress -EnumsAsStrings:$EnumsAsStrings + $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") @@ -136,7 +159,11 @@ if ($null -eq $prop.Value) { continue } - $value = ConvertTo-LuaTable -InputObject $prop.Value -CurrentDepth ($CurrentDepth + 1) -MaxDepth $MaxDepth -Compress:$Compress -EnumsAsStrings:$EnumsAsStrings + $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") diff --git a/src/functions/private/Read-LuaHexFloat.ps1 b/src/functions/private/Read-LuaHexFloat.ps1 new file mode 100644 index 0000000..41c8e88 --- /dev/null +++ b/src/functions/private/Read-LuaHexFloat.ps1 @@ -0,0 +1,56 @@ +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 {} +} diff --git a/src/functions/private/Read-LuaMultiLineString.ps1 b/src/functions/private/Read-LuaMultiLineString.ps1 new file mode 100644 index 0000000..6002df8 --- /dev/null +++ b/src/functions/private/Read-LuaMultiLineString.ps1 @@ -0,0 +1,40 @@ +function Read-LuaMultiLineString { + <# + .SYNOPSIS + Reads a multi-line Lua string delimited by [[ and ]]. + #> + [OutputType([string])] + [CmdletBinding()] + param() + + begin {} + + process { + $script:luaPos += 2 # skip [[ + $result = [System.Text.StringBuilder]::new() + + # Per Lua spec, a newline immediately after [[ 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 + 1 -lt $script:luaString.Length) { + if ($script:luaString[$script:luaPos] -eq ']' -and + $script:luaString[$script:luaPos + 1] -eq ']') { + $script:luaPos += 2 + return $result.ToString() + } + $null = $result.Append($script:luaString[$script:luaPos]) + $script:luaPos++ + } + + throw 'Unterminated multi-line string.' + } + + end {} +} diff --git a/src/functions/private/Read-LuaNumber.ps1 b/src/functions/private/Read-LuaNumber.ps1 new file mode 100644 index 0000000..d44409c --- /dev/null +++ b/src/functions/private/Read-LuaNumber.ps1 @@ -0,0 +1,131 @@ +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 {} +} diff --git a/src/functions/private/Read-LuaString.ps1 b/src/functions/private/Read-LuaString.ps1 new file mode 100644 index 0000000..3251180 --- /dev/null +++ b/src/functions/private/Read-LuaString.ps1 @@ -0,0 +1,145 @@ +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.' + } + } + 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 {} +} diff --git a/src/functions/private/Read-LuaTable.ps1 b/src/functions/private/Read-LuaTable.ps1 new file mode 100644 index 0000000..2dc272d --- /dev/null +++ b/src/functions/private/Read-LuaTable.ps1 @@ -0,0 +1,151 @@ +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 -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -eq ']') { + $script:luaPos++ # skip ] + } + Skip-LuaWhitespace + if ($script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -eq '=') { + $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 + + # Skip comma or semicolon separator + if ($script:luaPos -lt $script:luaString.Length -and + ($script:luaString[$script:luaPos] -eq ',' -or + $script:luaString[$script:luaPos] -eq ';')) { + $script:luaPos++ + } + } + + 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 {} +} diff --git a/src/functions/private/Read-LuaValue.ps1 b/src/functions/private/Read-LuaValue.ps1 new file mode 100644 index 0000000..b7b9195 --- /dev/null +++ b/src/functions/private/Read-LuaValue.ps1 @@ -0,0 +1,82 @@ +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 [[ ... ]] + if ($char -eq '[' -and + $script:luaPos + 1 -lt $script:luaString.Length -and + $script:luaString[$script:luaPos + 1] -eq '[') { + return Read-LuaMultiLineString + } + + # Number or negative number + 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.]')) { + 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 {} +} diff --git a/src/functions/private/Skip-LuaWhitespace.ps1 b/src/functions/private/Skip-LuaWhitespace.ps1 new file mode 100644 index 0000000..1deed44 --- /dev/null +++ b/src/functions/private/Skip-LuaWhitespace.ps1 @@ -0,0 +1,55 @@ +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 --[[ ... ]] + if ($script:luaPos + 1 -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -eq '[' -and + $script:luaString[$script:luaPos + 1] -eq '[') { + $script:luaPos += 2 + while ($script:luaPos + 1 -lt $script:luaString.Length) { + if ($script:luaString[$script:luaPos] -eq ']' -and + $script:luaString[$script:luaPos + 1] -eq ']') { + $script:luaPos += 2 + break + } + $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 {} +} diff --git a/src/functions/public/Lua/ConvertFrom-Lua.ps1 b/src/functions/public/Lua/ConvertFrom-Lua.ps1 index 4019446..dea0161 100644 --- a/src/functions/public/Lua/ConvertFrom-Lua.ps1 +++ b/src/functions/public/Lua/ConvertFrom-Lua.ps1 @@ -83,7 +83,7 @@ process { $result = ConvertFrom-LuaTable -InputString $InputObject -AsPSCustomObject:(-not $AsHashtable) -MaxDepth $Depth if ($NoEnumerate) { - Write-Output -NoEnumerate $result + , $result } else { $result } diff --git a/src/functions/public/Lua/ConvertTo-Lua.ps1 b/src/functions/public/Lua/ConvertTo-Lua.ps1 index 12c2412..71a2d04 100644 --- a/src/functions/public/Lua/ConvertTo-Lua.ps1 +++ b/src/functions/public/Lua/ConvertTo-Lua.ps1 @@ -86,7 +86,11 @@ if ($AsArray -and $InputObject -isnot [System.Collections.IList]) { $objectToConvert = @(, $InputObject) } - ConvertTo-LuaTable -InputObject $objectToConvert -CurrentDepth 0 -MaxDepth $Depth -Compress:$Compress -EnumsAsStrings:$EnumsAsStrings + ConvertTo-LuaTable -InputObject $objectToConvert ` + -CurrentDepth 0 ` + -MaxDepth $Depth ` + -Compress:$Compress ` + -EnumsAsStrings:$EnumsAsStrings } end {} diff --git a/src/header.ps1 b/src/header.ps1 deleted file mode 100644 index 3247a0e..0000000 --- a/src/header.ps1 +++ /dev/null @@ -1,2 +0,0 @@ -[CmdletBinding()] -param() diff --git a/src/manifest.psd1 b/src/manifest.psd1 deleted file mode 100644 index 25c1842..0000000 --- a/src/manifest.psd1 +++ /dev/null @@ -1,6 +0,0 @@ -# This file always wins! -# Use this file to override any of the framework defaults and generated values. -@{ - ModuleVersion = '0.0.1' - Description = 'A PowerShell module for converting between PowerShell objects and Lua table notation.' -} diff --git a/tests/Lua.Tests.ps1 b/tests/Lua.Tests.ps1 index 05bcd4b..f40e4db 100644 --- a/tests/Lua.Tests.ps1 +++ b/tests/Lua.Tests.ps1 @@ -142,10 +142,9 @@ Describe 'ConvertFrom-Lua' { } It 'Handles escaped single quote in single-quoted string' { - $result = ConvertFrom-Lua -InputObject "'it'\''s'" - # Actually Lua uses \' inside single-quoted strings - $result2 = ConvertFrom-Lua -InputObject "'it\''s'" - $result2 | Should -Be "it's" + # Lua uses \' inside single-quoted strings + $result = ConvertFrom-Lua -InputObject "'it\'s'" + $result | Should -Be "it's" } } @@ -217,7 +216,7 @@ Describe 'ConvertFrom-Lua' { if ($result -is [System.Collections.IDictionary]) { $result.Count | Should -Be 0 } else { - $result.PSObject.Properties.Count | Should -Be 0 + @($result.PSObject.Properties).Count | Should -Be 0 } } @@ -1007,21 +1006,21 @@ Describe 'Round-trip conversion' { It 'Round-trips a full config-like structure (5+ levels)' { $original = [ordered]@{ app = [ordered]@{ - name = 'MyApp' + name = 'MyApp' version = '2.0' modules = [ordered]@{ auth = [ordered]@{ - enabled = $true + enabled = $true provider = [ordered]@{ - type = 'oauth' + type = 'oauth' settings = [ordered]@{ clientId = 'abc123' - scopes = @('read', 'write', 'admin') + scopes = @('read', 'write', 'admin') } } } logging = [ordered]@{ - level = 'info' + level = 'info' outputs = @( [ordered]@{ type = 'console'; colored = $true }, [ordered]@{ type = 'file'; path = '/var/log/app.log' } @@ -1105,8 +1104,8 @@ Describe 'Round-trip conversion' { It 'Round-trips strings with special characters' { $original = [ordered]@{ - escaped = "line1`nline2`ttab" - quoted = 'she said "hi"' + escaped = "line1`nline2`ttab" + quoted = 'she said "hi"' backslash = 'C:\path\to\file' } $lua = ConvertTo-Lua -InputObject $original -Depth 5 @@ -1119,8 +1118,8 @@ Describe 'Round-trip conversion' { It 'Round-trips unicode strings' { $original = [ordered]@{ greeting = 'Héllo Wörld' - emoji = 'test' - cjk = '日本語' + emoji = 'test' + cjk = '日本語' } $lua = ConvertTo-Lua -InputObject $original -Depth 5 $result = ConvertFrom-Lua -InputObject $lua -AsHashtable @@ -1131,15 +1130,15 @@ Describe 'Round-trip conversion' { It 'Round-trips deeply nested array-of-objects-with-arrays' { $original = @( [ordered]@{ - name = 'group1' - items = @( + name = 'group1' + items = @( [ordered]@{ id = 1; tags = @('a', 'b') }, [ordered]@{ id = 2; tags = @('c') } ) }, [ordered]@{ - name = 'group2' - items = @( + name = 'group2' + items = @( [ordered]@{ id = 3; tags = @('d', 'e', 'f') } ) } diff --git a/tests/data/Arrays.lua b/tests/data/Arrays.lua index f673a7f..a6c7eaf 100644 --- a/tests/data/Arrays.lua +++ b/tests/data/Arrays.lua @@ -1,4 +1,4 @@ -{ +return { integers = {1, 2, 3, 42, -7, 0}, floats = {3.14, -2.5, 0.001, 1e10}, booleans = {true, false}, diff --git a/tests/data/DeepStructure.json b/tests/data/DeepStructure.json index d36e87d..f056acc 100644 --- a/tests/data/DeepStructure.json +++ b/tests/data/DeepStructure.json @@ -14,12 +14,17 @@ { "id": 1, "name": "item1", - "tags": ["alpha", "beta"] + "tags": [ + "alpha", + "beta" + ] }, { "id": 2, "name": "item2", - "tags": ["gamma"] + "tags": [ + "gamma" + ] } ] }, @@ -76,16 +81,32 @@ } }, "matrix": [ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9] + [ + 1, + 2, + 3 + ], + [ + 4, + 5, + 6 + ], + [ + 7, + 8, + 9 + ] ], "mixedDepth": { "shallow": "yes", "deep": { "deeper": { "deepest": { - "array": [10, 20, 30], + "array": [ + 10, + 20, + 30 + ], "nested": { "flag": false, "label": "end" diff --git a/tests/data/DeepStructure.lua b/tests/data/DeepStructure.lua index 14ce88e..c238340 100644 --- a/tests/data/DeepStructure.lua +++ b/tests/data/DeepStructure.lua @@ -1,4 +1,4 @@ -{ +return { level1 = { level2 = { level3 = { diff --git a/tests/data/Strings.lua b/tests/data/Strings.lua index a58e126..028b8c1 100644 --- a/tests/data/Strings.lua +++ b/tests/data/Strings.lua @@ -1,4 +1,4 @@ -{ +return { simpleString = "hello", escapedQuote = "she said \"hi\"", newlineString = "line1\nline2", diff --git a/tests/data/TestStructure.lua b/tests/data/TestStructure.lua index 575ead5..8f7eb5e 100644 --- a/tests/data/TestStructure.lua +++ b/tests/data/TestStructure.lua @@ -1,4 +1,4 @@ -{ +return { name = "ElvUI", version = "13.74", enabled = true, From c43ada97536e8016e776f5d41bc704ee51dab2f7 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 13 Apr 2026 23:38:45 +0200 Subject: [PATCH 05/24] Fix negative value handling in Read-LuaHexFloat and Read-LuaNumber; improve formatting in Lua.Tests --- src/functions/private/Read-LuaHexFloat.ps1 | 2 +- src/functions/private/Read-LuaNumber.ps1 | 2 +- src/functions/private/Read-LuaValue.ps1 | 4 ++-- src/functions/public/Lua/ConvertFrom-Lua.ps1 | 1 + tests/Lua.Tests.ps1 | 22 ++++++++++---------- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/functions/private/Read-LuaHexFloat.ps1 b/src/functions/private/Read-LuaHexFloat.ps1 index 41c8e88..9274645 100644 --- a/src/functions/private/Read-LuaHexFloat.ps1 +++ b/src/functions/private/Read-LuaHexFloat.ps1 @@ -48,7 +48,7 @@ } $result = ($intPart + $fracValue) * [Math]::Pow(2, $exponent) - if ($isNegative) { $result = -$result } + if ($isNegative) { $result = (-$result) } return $result } diff --git a/src/functions/private/Read-LuaNumber.ps1 b/src/functions/private/Read-LuaNumber.ps1 index d44409c..9b4346e 100644 --- a/src/functions/private/Read-LuaNumber.ps1 +++ b/src/functions/private/Read-LuaNumber.ps1 @@ -106,7 +106,7 @@ $numStr.Substring(2) } $longVal = [Convert]::ToInt64($hexPart, 16) - if ($isNegative) { $longVal = -$longVal } + if ($isNegative) { $longVal = (-$longVal) } if ($longVal -ge [int]::MinValue -and $longVal -le [int]::MaxValue) { return [int]$longVal diff --git a/src/functions/private/Read-LuaValue.ps1 b/src/functions/private/Read-LuaValue.ps1 index b7b9195..8d7fb7d 100644 --- a/src/functions/private/Read-LuaValue.ps1 +++ b/src/functions/private/Read-LuaValue.ps1 @@ -48,8 +48,8 @@ # Number or negative number 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.]')) { + $script:luaPos + 1 -lt $script:luaString.Length -and + $script:luaString[$script:luaPos + 1] -match '[0-9.]')) { return Read-LuaNumber } diff --git a/src/functions/public/Lua/ConvertFrom-Lua.ps1 b/src/functions/public/Lua/ConvertFrom-Lua.ps1 index dea0161..0b1cf00 100644 --- a/src/functions/public/Lua/ConvertFrom-Lua.ps1 +++ b/src/functions/public/Lua/ConvertFrom-Lua.ps1 @@ -58,6 +58,7 @@ 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. diff --git a/tests/Lua.Tests.ps1 b/tests/Lua.Tests.ps1 index f40e4db..229436f 100644 --- a/tests/Lua.Tests.ps1 +++ b/tests/Lua.Tests.ps1 @@ -1006,21 +1006,21 @@ Describe 'Round-trip conversion' { It 'Round-trips a full config-like structure (5+ levels)' { $original = [ordered]@{ app = [ordered]@{ - name = 'MyApp' + name = 'MyApp' version = '2.0' modules = [ordered]@{ auth = [ordered]@{ - enabled = $true + enabled = $true provider = [ordered]@{ - type = 'oauth' + type = 'oauth' settings = [ordered]@{ clientId = 'abc123' - scopes = @('read', 'write', 'admin') + scopes = @('read', 'write', 'admin') } } } logging = [ordered]@{ - level = 'info' + level = 'info' outputs = @( [ordered]@{ type = 'console'; colored = $true }, [ordered]@{ type = 'file'; path = '/var/log/app.log' } @@ -1104,8 +1104,8 @@ Describe 'Round-trip conversion' { It 'Round-trips strings with special characters' { $original = [ordered]@{ - escaped = "line1`nline2`ttab" - quoted = 'she said "hi"' + escaped = "line1`nline2`ttab" + quoted = 'she said "hi"' backslash = 'C:\path\to\file' } $lua = ConvertTo-Lua -InputObject $original -Depth 5 @@ -1118,8 +1118,8 @@ Describe 'Round-trip conversion' { It 'Round-trips unicode strings' { $original = [ordered]@{ greeting = 'Héllo Wörld' - emoji = 'test' - cjk = '日本語' + emoji = 'test' + cjk = '日本語' } $lua = ConvertTo-Lua -InputObject $original -Depth 5 $result = ConvertFrom-Lua -InputObject $lua -AsHashtable @@ -1130,14 +1130,14 @@ Describe 'Round-trip conversion' { It 'Round-trips deeply nested array-of-objects-with-arrays' { $original = @( [ordered]@{ - name = 'group1' + name = 'group1' items = @( [ordered]@{ id = 1; tags = @('a', 'b') }, [ordered]@{ id = 2; tags = @('c') } ) }, [ordered]@{ - name = 'group2' + name = 'group2' items = @( [ordered]@{ id = 3; tags = @('d', 'e', 'f') } ) From 4fefa8d1154287a6929590744a030e6fa5d8fd12 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 13 Apr 2026 23:47:32 +0200 Subject: [PATCH 06/24] Improve formatting in complex deep structure round-trip tests --- tests/Lua.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Lua.Tests.ps1 b/tests/Lua.Tests.ps1 index 229436f..8741165 100644 --- a/tests/Lua.Tests.ps1 +++ b/tests/Lua.Tests.ps1 @@ -1005,11 +1005,11 @@ Describe 'Round-trip conversion' { Context 'Complex deep structure round-trips' { It 'Round-trips a full config-like structure (5+ levels)' { $original = [ordered]@{ - app = [ordered]@{ + app = [ordered]@{ name = 'MyApp' version = '2.0' modules = [ordered]@{ - auth = [ordered]@{ + auth = [ordered]@{ enabled = $true provider = [ordered]@{ type = 'oauth' From e298f64dc2ce085d50af2f9ce5e859e47dc140d4 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Tue, 14 Apr 2026 00:09:10 +0200 Subject: [PATCH 07/24] Refactor Lua conversion functions and improve error handling; update examples for ConvertFrom-Lua --- README.md | 2 +- examples/General.ps1 | 2 +- .../private/ConvertFrom-LuaTable.ps1 | 6 +++ .../private/Read-LuaMultiLineString.ps1 | 31 +++++++++++---- src/functions/private/Read-LuaString.ps1 | 29 ++++++++++++++ src/functions/private/Read-LuaTable.ps1 | 27 +++++++------ src/functions/private/Read-LuaValue.ps1 | 10 +++-- src/functions/private/Skip-LuaWhitespace.ps1 | 39 +++++++++++++------ src/functions/public/Lua/ConvertFrom-Lua.ps1 | 1 + 9 files changed, 113 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 69f934c..65909fe 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ $luaOutput | Set-Content -Path 'settings.lua' ### Example 6: Convert Lua to PSCustomObject ```powershell -$result = '{ server = "localhost", port = 8080 }' | ConvertFrom-Lua -AsObject +$result = '{ server = "localhost", port = 8080 }' | ConvertFrom-Lua $result.server # localhost $result.port # 8080 ``` diff --git a/examples/General.ps1 b/examples/General.ps1 index 0f2dacb..6373801 100644 --- a/examples/General.ps1 +++ b/examples/General.ps1 @@ -34,7 +34,7 @@ Write-Output "Name: $($result.name)" Write-Output "Player Width: $($result.unitframes.playerWidth)" # Convert Lua to PSCustomObject -$obj = '{ server = "localhost", port = 8080 }' | ConvertFrom-Lua -AsObject +$obj = '{ server = "localhost", port = 8080 }' | ConvertFrom-Lua Write-Output "Server: $($obj.server), Port: $($obj.port)" # Compressed output diff --git a/src/functions/private/ConvertFrom-LuaTable.ps1 b/src/functions/private/ConvertFrom-LuaTable.ps1 index 84b372a..d50a7b3 100644 --- a/src/functions/private/ConvertFrom-LuaTable.ps1 +++ b/src/functions/private/ConvertFrom-LuaTable.ps1 @@ -48,6 +48,12 @@ $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 } diff --git a/src/functions/private/Read-LuaMultiLineString.ps1 b/src/functions/private/Read-LuaMultiLineString.ps1 index 6002df8..66ed9e2 100644 --- a/src/functions/private/Read-LuaMultiLineString.ps1 +++ b/src/functions/private/Read-LuaMultiLineString.ps1 @@ -1,7 +1,7 @@ function Read-LuaMultiLineString { <# .SYNOPSIS - Reads a multi-line Lua string delimited by [[ and ]]. + Reads a multi-line Lua string delimited by long brackets [[ ]], [=[ ]=], [==[ ]==], etc. #> [OutputType([string])] [CmdletBinding()] @@ -10,10 +10,27 @@ begin {} process { - $script:luaPos += 2 # skip [[ + # 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 [[ is ignored + # 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++ @@ -23,10 +40,10 @@ $script:luaPos += 2 } - while ($script:luaPos + 1 -lt $script:luaString.Length) { - if ($script:luaString[$script:luaPos] -eq ']' -and - $script:luaString[$script:luaPos + 1] -eq ']') { - $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]) diff --git a/src/functions/private/Read-LuaString.ps1 b/src/functions/private/Read-LuaString.ps1 index 3251180..14419a1 100644 --- a/src/functions/private/Read-LuaString.ps1 +++ b/src/functions/private/Read-LuaString.ps1 @@ -104,6 +104,35 @@ 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]') { diff --git a/src/functions/private/Read-LuaTable.ps1 b/src/functions/private/Read-LuaTable.ps1 index 2dc272d..5b5e5df 100644 --- a/src/functions/private/Read-LuaTable.ps1 +++ b/src/functions/private/Read-LuaTable.ps1 @@ -43,15 +43,17 @@ Skip-LuaWhitespace $key = Read-LuaValue Skip-LuaWhitespace - if ($script:luaPos -lt $script:luaString.Length -and - $script:luaString[$script:luaPos] -eq ']') { - $script:luaPos++ # skip ] + 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 -lt $script:luaString.Length -and - $script:luaString[$script:luaPos] -eq '=') { - $script:luaPos++ # skip = + 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 }) @@ -101,11 +103,14 @@ Skip-LuaWhitespace - # Skip comma or semicolon separator - if ($script:luaPos -lt $script:luaString.Length -and - ($script:luaString[$script:luaPos] -eq ',' -or - $script:luaString[$script:luaPos] -eq ';')) { - $script:luaPos++ + # 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." + } } } diff --git a/src/functions/private/Read-LuaValue.ps1 b/src/functions/private/Read-LuaValue.ps1 index 8d7fb7d..ec3b677 100644 --- a/src/functions/private/Read-LuaValue.ps1 +++ b/src/functions/private/Read-LuaValue.ps1 @@ -38,15 +38,19 @@ return Read-LuaString -QuoteChar "'" } - # Multi-line string [[ ... ]] + # Multi-line string [[ ... ]] or [=[ ... ]=] if ($char -eq '[' -and $script:luaPos + 1 -lt $script:luaString.Length -and - $script:luaString[$script:luaPos + 1] -eq '[') { + ($script:luaString[$script:luaPos + 1] -eq '[' -or + $script:luaString[$script:luaPos + 1] -eq '=')) { return Read-LuaMultiLineString } - # Number or negative number + # 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.]')) { diff --git a/src/functions/private/Skip-LuaWhitespace.ps1 b/src/functions/private/Skip-LuaWhitespace.ps1 index 1deed44..0ac67e1 100644 --- a/src/functions/private/Skip-LuaWhitespace.ps1 +++ b/src/functions/private/Skip-LuaWhitespace.ps1 @@ -24,18 +24,35 @@ $script:luaString[$script:luaPos + 1] -eq '-') { $script:luaPos += 2 - # Multi-line comment --[[ ... ]] - if ($script:luaPos + 1 -lt $script:luaString.Length -and - $script:luaString[$script:luaPos] -eq '[' -and - $script:luaString[$script:luaPos + 1] -eq '[') { - $script:luaPos += 2 - while ($script:luaPos + 1 -lt $script:luaString.Length) { - if ($script:luaString[$script:luaPos] -eq ']' -and - $script:luaString[$script:luaPos + 1] -eq ']') { - $script:luaPos += 2 - break + # 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++ } - $script:luaPos++ } } else { # Single-line comment diff --git a/src/functions/public/Lua/ConvertFrom-Lua.ps1 b/src/functions/public/Lua/ConvertFrom-Lua.ps1 index 0b1cf00..a6bc0fc 100644 --- a/src/functions/public/Lua/ConvertFrom-Lua.ps1 +++ b/src/functions/public/Lua/ConvertFrom-Lua.ps1 @@ -72,6 +72,7 @@ # 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. From 0b81abac4912b2415b103f766170d31665d94943 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Tue, 14 Apr 2026 00:41:18 +0200 Subject: [PATCH 08/24] Add support for parsing assignment statements in ConvertFrom-Lua; enhance tests for various assignment scenarios --- .../private/ConvertFrom-LuaTable.ps1 | 77 +++++++++++ tests/Lua.Tests.ps1 | 129 +++++++++++++++++- tests/data/Assignments.json | 12 ++ tests/data/Assignments.lua | 14 ++ tests/data/WoWSavedVariables.json | 92 +++++++++++++ tests/data/WoWSavedVariables.lua | 91 ++++++++++++ 6 files changed, 412 insertions(+), 3 deletions(-) create mode 100644 tests/data/Assignments.json create mode 100644 tests/data/Assignments.lua create mode 100644 tests/data/WoWSavedVariables.json create mode 100644 tests/data/WoWSavedVariables.lua diff --git a/src/functions/private/ConvertFrom-LuaTable.ps1 b/src/functions/private/ConvertFrom-LuaTable.ps1 index d50a7b3..30b6154 100644 --- a/src/functions/private/ConvertFrom-LuaTable.ps1 +++ b/src/functions/private/ConvertFrom-LuaTable.ps1 @@ -46,6 +46,83 @@ } } + # Detect assignment statements: Name = value (common in Lua data/config files) + # A chunk in Lua is a block of statements; assignment is: varlist '=' explist + # We support one or more simple assignments: Name = value + $assignmentDetected = $false + $savedPos = $script:luaPos + if ($script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -match '[a-zA-Z_]') { + # Try to read an identifier + $tryPos = $script:luaPos + while ($tryPos -lt $script:luaString.Length -and + $script:luaString[$tryPos] -match '[a-zA-Z0-9_]') { + $tryPos++ + } + $tryIdent = $script:luaString.Substring($script:luaPos, $tryPos - $script:luaPos) + # Check it's not a keyword that starts a value (true/false/nil) + if ($tryIdent -notin 'true', 'false', 'nil') { + # Skip whitespace after identifier to check for '=' + $peekPos = $tryPos + while ($peekPos -lt $script:luaString.Length -and + $script:luaString[$peekPos] -match '\s') { + $peekPos++ + } + # Check for '=' but not '==' + if ($peekPos -lt $script:luaString.Length -and + $script:luaString[$peekPos] -eq '=' -and + ($peekPos + 1 -ge $script:luaString.Length -or + $script:luaString[$peekPos + 1] -ne '=')) { + $assignmentDetected = $true + } + } + } + + if ($assignmentDetected) { + # Parse one or more assignment statements into an ordered dictionary + $assignments = [ordered]@{} + while ($script:luaPos -lt $script:luaString.Length) { + Skip-LuaWhitespace + if ($script:luaPos -ge $script:luaString.Length) { + break + } + + # Read variable name + $identStart = $script:luaPos + if ($script:luaString[$script:luaPos] -notmatch '[a-zA-Z_]') { + throw "Expected variable name at position $($script:luaPos)." + } + while ($script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -match '[a-zA-Z0-9_]') { + $script:luaPos++ + } + $varName = $script:luaString.Substring($identStart, $script:luaPos - $identStart) + + Skip-LuaWhitespace + + # Expect '=' + if ($script:luaPos -ge $script:luaString.Length -or + $script:luaString[$script:luaPos] -ne '=') { + throw "Expected '=' after variable name '$varName' at position $($script:luaPos)." + } + $script:luaPos++ # skip '=' + + # Read value + $value = Read-LuaValue + $assignments[$varName] = $value + + Skip-LuaWhitespace + } + + if ($script:luaAsPSCustomObject) { + return [PSCustomObject]$assignments + } + return $assignments + } + + # Reset position (no assignment detected, or it was a keyword value) + $script:luaPos = $savedPos + $result = Read-LuaValue Skip-LuaWhitespace diff --git a/tests/Lua.Tests.ps1 b/tests/Lua.Tests.ps1 index 8741165..620c9b9 100644 --- a/tests/Lua.Tests.ps1 +++ b/tests/Lua.Tests.ps1 @@ -377,11 +377,94 @@ Describe 'ConvertFrom-Lua' { } } - Context 'Error cases' { - It 'Throws on bare identifier (variable reference)' { - { ConvertFrom-Lua -InputObject 'someVariable' } | Should -Throw '*bare identifier*' + Context 'Assignment statements' { + It 'Parses a single assignment statement' { + $lua = 'MyDB = { name = "test", count = 42 }' + $result = ConvertFrom-Lua -InputObject $lua + $result.MyDB.name | Should -Be 'test' + $result.MyDB.count | Should -Be 42 + } + + It 'Parses multiple assignment statements' { + $lua = @' +DB1 = { x = 1 } +DB2 = { y = 2 } +'@ + $result = ConvertFrom-Lua -InputObject $lua + $result.DB1.x | Should -Be 1 + $result.DB2.y | Should -Be 2 + } + + It 'Parses assignment with string value' { + $lua = 'myVar = "hello"' + $result = ConvertFrom-Lua -InputObject $lua + $result.myVar | Should -Be 'hello' + } + + It 'Parses assignment with array value' { + $lua = 'myArr = { 1, 2, 3 }' + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.myArr.Count | Should -Be 3 + $result.myArr[0] | Should -Be 1 + } + + It 'Parses assignment with boolean value' { + $lua = 'flag = true' + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.flag | Should -BeTrue + } + + It 'Parses assignment with nil value' { + $lua = 'empty = nil' + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.empty | Should -BeNullOrEmpty } + It 'Parses assignment returning PSCustomObject by default' { + $lua = 'X = { a = 1 }' + $result = ConvertFrom-Lua -InputObject $lua + $result | Should -BeOfType [PSCustomObject] + $result.X.a | Should -Be 1 + } + + It 'Parses assignment returning ordered hashtable with -AsHashtable' { + $lua = 'X = { a = 1 }' + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + $result.X.a | Should -Be 1 + } + + It 'Handles comments between assignments' { + $lua = @' +-- First variable +A = { val = 1 } +-- Second variable +B = { val = 2 } +'@ + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.A.val | Should -Be 1 + $result.B.val | Should -Be 2 + } + + It 'Handles variable names with underscores' { + $lua = 'My_Addon_DB = { enabled = true }' + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.My_Addon_DB.enabled | Should -BeTrue + } + + It 'Does not treat true/false/nil as assignments' { + $result = ConvertFrom-Lua -InputObject 'true' + $result | Should -BeTrue + + $result = ConvertFrom-Lua -InputObject 'false' + $result | Should -BeFalse + + $result = ConvertFrom-Lua -InputObject 'nil' + $result | Should -BeNullOrEmpty + } + } + + Context 'Error cases' { It 'Throws on bare identifier inside table' { { ConvertFrom-Lua -InputObject '{ myVar }' } | Should -Throw '*bare identifier*' } @@ -596,6 +679,46 @@ Describe 'ConvertFrom-Lua' { $result.mixedDepth.deep.deeper.deepest.nested.label | Should -Be 'end' } } + + Context 'File-based test: Assignments (SavedVariables style)' { + BeforeAll { + $luaContent = Get-Content -Path (Join-Path $dataPath 'Assignments.lua') -Raw + $expected = Get-Content -Path (Join-Path $dataPath 'Assignments.json') -Raw | ConvertFrom-Json + } + + It 'Parses multiple top-level variable assignments' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.MyAddonDB | Should -Not -BeNullOrEmpty + $result.MyAddonOptions | Should -Not -BeNullOrEmpty + } + + It 'Parses first assignment values correctly' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.MyAddonDB.enabled | Should -Be $expected.MyAddonDB.enabled + $result.MyAddonDB.fontSize | Should -Be $expected.MyAddonDB.fontSize + $result.MyAddonDB.name | Should -Be $expected.MyAddonDB.name + } + + It 'Parses second assignment values correctly' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.MyAddonOptions.showTooltips | Should -Be $expected.MyAddonOptions.showTooltips + $result.MyAddonOptions.scale | Should -Be $expected.MyAddonOptions.scale + } + + It 'Parses nested array in assignment' { + $result = ConvertFrom-Lua -InputObject $luaContent + $result.MyAddonOptions.colors.Count | Should -Be 3 + $result.MyAddonOptions.colors[0] | Should -Be $expected.MyAddonOptions.colors[0] + } + + It 'Returns ordered hashtable with -AsHashtable' { + $result = ConvertFrom-Lua -InputObject $luaContent -AsHashtable + $result | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + $keys = @($result.Keys) + $keys[0] | Should -Be 'MyAddonDB' + $keys[1] | Should -Be 'MyAddonOptions' + } + } } Describe 'ConvertTo-Lua' { diff --git a/tests/data/Assignments.json b/tests/data/Assignments.json new file mode 100644 index 0000000..899b6d8 --- /dev/null +++ b/tests/data/Assignments.json @@ -0,0 +1,12 @@ +{ + "MyAddonDB": { + "enabled": true, + "fontSize": 14, + "name": "TestAddon" + }, + "MyAddonOptions": { + "showTooltips": false, + "scale": 0.85, + "colors": [0.5, 0.8, 1.0] + } +} diff --git a/tests/data/Assignments.lua b/tests/data/Assignments.lua new file mode 100644 index 0000000..a2b9972 --- /dev/null +++ b/tests/data/Assignments.lua @@ -0,0 +1,14 @@ +MyAddonDB = { + ["enabled"] = true, + ["fontSize"] = 14, + ["name"] = "TestAddon", +} +MyAddonOptions = { + ["showTooltips"] = false, + ["scale"] = 0.85, + ["colors"] = { + 0.5, + 0.8, + 1.0, + }, +} diff --git a/tests/data/WoWSavedVariables.json b/tests/data/WoWSavedVariables.json new file mode 100644 index 0000000..47b161f --- /dev/null +++ b/tests/data/WoWSavedVariables.json @@ -0,0 +1,92 @@ +{ + "WildDB": { + "tooltip": { + "enabled": true, + "lines": { + "itemID": "always", + "sellPrice": "settings", + "quality": "settings", + "itemLevel": "settings", + "bindType": "settings" + } + }, + "vendorSellRules": {}, + "lootAutoLoot": true, + "screenCenterCircle": true, + "screenCenterCircleSize": 48, + "screenCenterCircleThickness": 5, + "lfg": { + "autoAcceptQueue": false, + "autoConfirmRole": true, + "autoAcceptRoleCheck": true, + "quickApply": true, + "keystoneButtons": true, + "filters": { + "enabled": false, + "hideDelisted": true, + "maxMembers": 0, + "hideIneligible": false, + "hidePvP": false, + "minMembers": 0 + } + }, + "craftingOrders": { + "enabled": true, + "maxLevel": 0, + "currentExpansionOnly": true, + "minLevel": 0, + "filters": { + "3": true, + "6": false, + "7": true, + "8": true + } + }, + "datastore": { + "characters": { + "Clawe-Tarren Mill": { + "bags": [ + { + "name": "Hearthstone", + "itemID": 6948, + "count": 1, + "isBound": true, + "quality": 1, + "bag": 0, + "slot": 1 + }, + { + "name": "Mythic Keystone", + "itemID": 180653, + "count": 1, + "isBound": true, + "quality": 4, + "bag": 1, + "slot": 2 + } + ], + "bagsUpdated": 1776093709, + "bankUpdated": 1776093705 + } + } + }, + "eventTrace": [ + "[1049155.942] GUILD_ROSTER_UPDATE | false", + "[1049156.293] LFG_LIST_SEARCH_RESULT_UPDATED | 1404", + "[1049156.678] COMPANION_UPDATE | MOUNT" + ] + }, + "WildDBOptions": { + "profileKeys": { + "Clawe - Tarren Mill": "Default", + "Lucretius - Tarren Mill": "Default" + }, + "profiles": { + "Default": { + "minimap": { + "hide": false + } + } + } + } +} diff --git a/tests/data/WoWSavedVariables.lua b/tests/data/WoWSavedVariables.lua new file mode 100644 index 0000000..9ce4e6c --- /dev/null +++ b/tests/data/WoWSavedVariables.lua @@ -0,0 +1,91 @@ +WildDB = { + ["tooltip"] = { + ["enabled"] = true, + ["lines"] = { + ["itemID"] = "always", + ["sellPrice"] = "settings", + ["quality"] = "settings", + ["itemLevel"] = "settings", + ["bindType"] = "settings", + }, + }, + ["vendorSellRules"] = { + }, + ["lootAutoLoot"] = true, + ["screenCenterCircle"] = true, + ["screenCenterCircleSize"] = 48, + ["screenCenterCircleThickness"] = 5, + ["lfg"] = { + ["autoAcceptQueue"] = false, + ["autoConfirmRole"] = true, + ["autoAcceptRoleCheck"] = true, + ["quickApply"] = true, + ["keystoneButtons"] = true, + ["filters"] = { + ["enabled"] = false, + ["hideDelisted"] = true, + ["maxMembers"] = 0, + ["hideIneligible"] = false, + ["hidePvP"] = false, + ["minMembers"] = 0, + }, + }, + ["craftingOrders"] = { + ["enabled"] = true, + ["maxLevel"] = 0, + ["currentExpansionOnly"] = true, + ["minLevel"] = 0, + ["filters"] = { + [3] = true, + [6] = false, + [7] = true, + [8] = true, + }, + }, + ["datastore"] = { + ["characters"] = { + ["Clawe-Tarren Mill"] = { + ["bags"] = { + { + ["name"] = "Hearthstone", + ["itemID"] = 6948, + ["count"] = 1, + ["isBound"] = true, + ["quality"] = 1, + ["bag"] = 0, + ["slot"] = 1, + }, + { + ["name"] = "Mythic Keystone", + ["itemID"] = 180653, + ["count"] = 1, + ["isBound"] = true, + ["quality"] = 4, + ["bag"] = 1, + ["slot"] = 2, + }, + }, + ["bagsUpdated"] = 1776093709, + ["bankUpdated"] = 1776093705, + }, + }, + }, + ["eventTrace"] = { + "[1049155.942] GUILD_ROSTER_UPDATE | false", + "[1049156.293] LFG_LIST_SEARCH_RESULT_UPDATED | 1404", + "[1049156.678] COMPANION_UPDATE | MOUNT", + }, +} +WildDBOptions = { + ["profileKeys"] = { + ["Clawe - Tarren Mill"] = "Default", + ["Lucretius - Tarren Mill"] = "Default", + }, + ["profiles"] = { + ["Default"] = { + ["minimap"] = { + ["hide"] = false, + }, + }, + }, +} From 85fe075d7b2aca7aee844394d8788759ad22b3d8 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Tue, 14 Apr 2026 00:41:34 +0200 Subject: [PATCH 09/24] Format JSON files for improved readability in Assignments.json and WoWSavedVariables.json --- tests/data/Assignments.json | 24 +++-- tests/data/WoWSavedVariables.json | 172 +++++++++++++++--------------- 2 files changed, 100 insertions(+), 96 deletions(-) diff --git a/tests/data/Assignments.json b/tests/data/Assignments.json index 899b6d8..c79a9dc 100644 --- a/tests/data/Assignments.json +++ b/tests/data/Assignments.json @@ -1,12 +1,16 @@ { - "MyAddonDB": { - "enabled": true, - "fontSize": 14, - "name": "TestAddon" - }, - "MyAddonOptions": { - "showTooltips": false, - "scale": 0.85, - "colors": [0.5, 0.8, 1.0] - } + "MyAddonDB": { + "enabled": true, + "fontSize": 14, + "name": "TestAddon" + }, + "MyAddonOptions": { + "showTooltips": false, + "scale": 0.85, + "colors": [ + 0.5, + 0.8, + 1.0 + ] + } } diff --git a/tests/data/WoWSavedVariables.json b/tests/data/WoWSavedVariables.json index 47b161f..22eb0e4 100644 --- a/tests/data/WoWSavedVariables.json +++ b/tests/data/WoWSavedVariables.json @@ -1,92 +1,92 @@ { - "WildDB": { - "tooltip": { - "enabled": true, - "lines": { - "itemID": "always", - "sellPrice": "settings", - "quality": "settings", - "itemLevel": "settings", - "bindType": "settings" - } - }, - "vendorSellRules": {}, - "lootAutoLoot": true, - "screenCenterCircle": true, - "screenCenterCircleSize": 48, - "screenCenterCircleThickness": 5, - "lfg": { - "autoAcceptQueue": false, - "autoConfirmRole": true, - "autoAcceptRoleCheck": true, - "quickApply": true, - "keystoneButtons": true, - "filters": { - "enabled": false, - "hideDelisted": true, - "maxMembers": 0, - "hideIneligible": false, - "hidePvP": false, - "minMembers": 0 - } - }, - "craftingOrders": { - "enabled": true, - "maxLevel": 0, - "currentExpansionOnly": true, - "minLevel": 0, - "filters": { - "3": true, - "6": false, - "7": true, - "8": true - } - }, - "datastore": { - "characters": { - "Clawe-Tarren Mill": { - "bags": [ - { - "name": "Hearthstone", - "itemID": 6948, - "count": 1, - "isBound": true, - "quality": 1, - "bag": 0, - "slot": 1 - }, - { - "name": "Mythic Keystone", - "itemID": 180653, - "count": 1, - "isBound": true, - "quality": 4, - "bag": 1, - "slot": 2 - } - ], - "bagsUpdated": 1776093709, - "bankUpdated": 1776093705 - } - } - }, - "eventTrace": [ - "[1049155.942] GUILD_ROSTER_UPDATE | false", - "[1049156.293] LFG_LIST_SEARCH_RESULT_UPDATED | 1404", - "[1049156.678] COMPANION_UPDATE | MOUNT" - ] + "WildDB": { + "tooltip": { + "enabled": true, + "lines": { + "itemID": "always", + "sellPrice": "settings", + "quality": "settings", + "itemLevel": "settings", + "bindType": "settings" + } + }, + "vendorSellRules": {}, + "lootAutoLoot": true, + "screenCenterCircle": true, + "screenCenterCircleSize": 48, + "screenCenterCircleThickness": 5, + "lfg": { + "autoAcceptQueue": false, + "autoConfirmRole": true, + "autoAcceptRoleCheck": true, + "quickApply": true, + "keystoneButtons": true, + "filters": { + "enabled": false, + "hideDelisted": true, + "maxMembers": 0, + "hideIneligible": false, + "hidePvP": false, + "minMembers": 0 + } }, - "WildDBOptions": { - "profileKeys": { - "Clawe - Tarren Mill": "Default", - "Lucretius - Tarren Mill": "Default" - }, - "profiles": { - "Default": { - "minimap": { - "hide": false - } + "craftingOrders": { + "enabled": true, + "maxLevel": 0, + "currentExpansionOnly": true, + "minLevel": 0, + "filters": { + "3": true, + "6": false, + "7": true, + "8": true + } + }, + "datastore": { + "characters": { + "Clawe-Tarren Mill": { + "bags": [ + { + "name": "Hearthstone", + "itemID": 6948, + "count": 1, + "isBound": true, + "quality": 1, + "bag": 0, + "slot": 1 + }, + { + "name": "Mythic Keystone", + "itemID": 180653, + "count": 1, + "isBound": true, + "quality": 4, + "bag": 1, + "slot": 2 } + ], + "bagsUpdated": 1776093709, + "bankUpdated": 1776093705 + } + } + }, + "eventTrace": [ + "[1049155.942] GUILD_ROSTER_UPDATE | false", + "[1049156.293] LFG_LIST_SEARCH_RESULT_UPDATED | 1404", + "[1049156.678] COMPANION_UPDATE | MOUNT" + ] + }, + "WildDBOptions": { + "profileKeys": { + "Clawe - Tarren Mill": "Default", + "Lucretius - Tarren Mill": "Default" + }, + "profiles": { + "Default": { + "minimap": { + "hide": false } + } } + } } From 18ece068c3545505799e70baad22ed265f50d782 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Tue, 14 Apr 2026 21:20:18 +0200 Subject: [PATCH 10/24] Enhance type handling in ConvertTo-LuaTable; improve parsing logic in Read-LuaTable and update output types in ConvertFrom-LuaTable --- src/functions/private/ConvertFrom-LuaTable.ps1 | 3 ++- src/functions/private/ConvertTo-LuaTable.ps1 | 9 ++++++++- src/functions/private/Read-LuaTable.ps1 | 4 +++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/functions/private/ConvertFrom-LuaTable.ps1 b/src/functions/private/ConvertFrom-LuaTable.ps1 index 30b6154..ad8fd0e 100644 --- a/src/functions/private/ConvertFrom-LuaTable.ps1 +++ b/src/functions/private/ConvertFrom-LuaTable.ps1 @@ -9,6 +9,7 @@ engine used by ConvertFrom-Lua. #> [OutputType([object])] + [OutputType([System.Collections.Specialized.OrderedDictionary])] [CmdletBinding()] param( # The Lua table string to parse. @@ -40,7 +41,7 @@ $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:luaString[$nextPos] -notmatch '[a-zA-Z0-9_]') { $script:luaPos = $nextPos Skip-LuaWhitespace } diff --git a/src/functions/private/ConvertTo-LuaTable.ps1 b/src/functions/private/ConvertTo-LuaTable.ps1 index 1da24c5..384ee64 100644 --- a/src/functions/private/ConvertTo-LuaTable.ps1 +++ b/src/functions/private/ConvertTo-LuaTable.ps1 @@ -61,7 +61,14 @@ $escaped = $InputObject.ToString() -replace '\\', '\\\\' -replace '"', '\"' return "`"$escaped`"" } - return ([int]$InputObject).ToString([System.Globalization.CultureInfo]::InvariantCulture) + $underlyingType = [System.Enum]::GetUnderlyingType($InputObject.GetType()) + if ($underlyingType -eq [byte] -or + $underlyingType -eq [uint16] -or + $underlyingType -eq [uint32] -or + $underlyingType -eq [uint64]) { + return ([System.Convert]::ToUInt64($InputObject)).ToString([System.Globalization.CultureInfo]::InvariantCulture) + } + return ([System.Convert]::ToInt64($InputObject)).ToString([System.Globalization.CultureInfo]::InvariantCulture) } if ($InputObject -is [int] -or $InputObject -is [long] -or diff --git a/src/functions/private/Read-LuaTable.ps1 b/src/functions/private/Read-LuaTable.ps1 index 5b5e5df..ee6a5b9 100644 --- a/src/functions/private/Read-LuaTable.ps1 +++ b/src/functions/private/Read-LuaTable.ps1 @@ -36,9 +36,11 @@ } # Check for bracket key: ["key"] = value or [expr] = value + # When [ is followed by [ or =, it's a long-bracket string value, not a bracket key if ($script:luaString[$script:luaPos] -eq '[' -and $script:luaPos + 1 -lt $script:luaString.Length -and - $script:luaString[$script:luaPos + 1] -ne '[') { + $script:luaString[$script:luaPos + 1] -ne '[' -and + $script:luaString[$script:luaPos + 1] -ne '=') { $script:luaPos++ # skip [ Skip-LuaWhitespace $key = Read-LuaValue From b24139f8ad6cf46d0c91b990748c01533f4bda6d Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Tue, 14 Apr 2026 22:51:27 +0200 Subject: [PATCH 11/24] Add .luacheckrc configuration to allow defined top for Lua test files --- .github/linters/.luacheckrc | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/linters/.luacheckrc diff --git a/.github/linters/.luacheckrc b/.github/linters/.luacheckrc new file mode 100644 index 0000000..88823eb --- /dev/null +++ b/.github/linters/.luacheckrc @@ -0,0 +1,2 @@ +files["tests/data/Assignments.lua"].allow_defined_top = true +files["tests/data/WoWSavedVariables.lua"].allow_defined_top = true From 2cc5a98f6ea810aa4c66964726e519ef7fef95e9 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 15 Apr 2026 01:01:19 +0200 Subject: [PATCH 12/24] Update .luacheckrc to ignore test data files and clarify their purpose --- .github/linters/.luacheckrc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/linters/.luacheckrc b/.github/linters/.luacheckrc index 88823eb..eb66baf 100644 --- a/.github/linters/.luacheckrc +++ b/.github/linters/.luacheckrc @@ -1,2 +1,4 @@ -files["tests/data/Assignments.lua"].allow_defined_top = true -files["tests/data/WoWSavedVariables.lua"].allow_defined_top = true +-- Test data files are Lua data/config files (e.g. WoW SavedVariables format) +-- that define top-level globals and are not executed as scripts. +files["**/tests/data/Assignments.lua"].ignore = {""} +files["**/tests/data/WoWSavedVariables.lua"].ignore = {""} From 7242d0c2462659263cc97fea8b1eced52d803a37 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 15 Apr 2026 01:48:26 +0200 Subject: [PATCH 13/24] Enhance error handling in Read-LuaString and Read-LuaTable; validate escape sequences and ensure table keys are not nil --- src/functions/private/Read-LuaString.ps1 | 6 ++++++ src/functions/private/Read-LuaTable.ps1 | 3 +++ 2 files changed, 9 insertions(+) diff --git a/src/functions/private/Read-LuaString.ps1 b/src/functions/private/Read-LuaString.ps1 index 14419a1..4e2ad8a 100644 --- a/src/functions/private/Read-LuaString.ps1 +++ b/src/functions/private/Read-LuaString.ps1 @@ -73,6 +73,9 @@ $hexStr = $script:luaString.Substring( $script:luaPos, 2 ) + if ($hexStr -notmatch '^[0-9a-fA-F]{2}$') { + throw 'Invalid \x escape sequence: expected two hexadecimal digits.' + } $hexVal = [Convert]::ToInt32($hexStr, 16) $null = $result.Append([char]$hexVal) $script:luaPos += 2 @@ -91,6 +94,9 @@ $script:luaString[$script:luaPos] -ne '}') { $script:luaPos++ } + if ($script:luaPos -ge $script:luaString.Length) { + throw 'Invalid \u escape sequence: missing closing brace.' + } $hexStr = $script:luaString.Substring( $hexStart, $script:luaPos - $hexStart diff --git a/src/functions/private/Read-LuaTable.ps1 b/src/functions/private/Read-LuaTable.ps1 index ee6a5b9..53be7be 100644 --- a/src/functions/private/Read-LuaTable.ps1 +++ b/src/functions/private/Read-LuaTable.ps1 @@ -44,6 +44,9 @@ $script:luaPos++ # skip [ Skip-LuaWhitespace $key = Read-LuaValue + if ($null -eq $key) { + throw 'Lua table keys cannot be nil.' + } Skip-LuaWhitespace if ($script:luaPos -ge $script:luaString.Length -or $script:luaString[$script:luaPos] -ne ']') { From e22dffe5ccd6ef2fa17fe37fcbc9428973af6394 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 15 Apr 2026 02:24:22 +0200 Subject: [PATCH 14/24] Address PR review feedback --- .github/linters/.luacheckrc | 4 ++-- src/functions/private/Format-LuaKey.ps1 | 9 ++++++++- src/functions/private/Read-LuaString.ps1 | 6 +++++- src/functions/private/Skip-LuaWhitespace.ps1 | 5 +++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/linters/.luacheckrc b/.github/linters/.luacheckrc index eb66baf..628126f 100644 --- a/.github/linters/.luacheckrc +++ b/.github/linters/.luacheckrc @@ -1,4 +1,4 @@ -- Test data files are Lua data/config files (e.g. WoW SavedVariables format) -- that define top-level globals and are not executed as scripts. -files["**/tests/data/Assignments.lua"].ignore = {""} -files["**/tests/data/WoWSavedVariables.lua"].ignore = {""} +files["**/tests/data/Assignments.lua"].ignore = {"111", "112"} +files["**/tests/data/WoWSavedVariables.lua"].ignore = {"111", "112"} diff --git a/src/functions/private/Format-LuaKey.ps1 b/src/functions/private/Format-LuaKey.ps1 index 83a0756..e03016c 100644 --- a/src/functions/private/Format-LuaKey.ps1 +++ b/src/functions/private/Format-LuaKey.ps1 @@ -29,7 +29,14 @@ if ($Key -match '^[a-zA-Z_][a-zA-Z0-9_]*$' -and $Key -notin $reservedWords) { return $Key } - $escaped = $Key -replace '\\', '\\\\' -replace '"', '\"' + $escaped = $Key ` + -replace '\\', '\\\\' ` + -replace '"', '\"' ` + -replace "`n", '\n' ` + -replace "`r", '\r' ` + -replace "`t", '\t' ` + -replace "`b", '\b' ` + -replace "`f", '\f' return "[`"$escaped`"]" } diff --git a/src/functions/private/Read-LuaString.ps1 b/src/functions/private/Read-LuaString.ps1 index 4e2ad8a..ad568ea 100644 --- a/src/functions/private/Read-LuaString.ps1 +++ b/src/functions/private/Read-LuaString.ps1 @@ -153,7 +153,11 @@ break } } - $null = $result.Append([char][int]$numStr) + $byteValue = [int]$numStr + if ($byteValue -gt 255) { + throw "Invalid decimal escape sequence '\$numStr'. Lua decimal escapes must be in the range 0-255." + } + $null = $result.Append([char]$byteValue) } else { # Unknown escape - just pass through $null = $result.Append($nextChar) diff --git a/src/functions/private/Skip-LuaWhitespace.ps1 b/src/functions/private/Skip-LuaWhitespace.ps1 index 0ac67e1..1019b25 100644 --- a/src/functions/private/Skip-LuaWhitespace.ps1 +++ b/src/functions/private/Skip-LuaWhitespace.ps1 @@ -39,14 +39,19 @@ $script:luaPos = $eqStart + $eqCount + 1 $closePattern = ']' + ('=' * $eqCount) + ']' $closeLen = $closePattern.Length + $foundClosingDelimiter = $false 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 + $foundClosingDelimiter = $true break } $script:luaPos++ } + if (-not $foundClosingDelimiter) { + throw "Unterminated long-bracket comment." + } } else { # Not a long bracket - treat as single-line comment while ($script:luaPos -lt $script:luaString.Length -and From 3cbbee6aa46d8afff29a55b8ce63b7cd6a421bc3 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 15 Apr 2026 03:03:39 +0200 Subject: [PATCH 15/24] Fix string quotation for error message in Skip-LuaWhitespace function --- src/functions/private/Skip-LuaWhitespace.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions/private/Skip-LuaWhitespace.ps1 b/src/functions/private/Skip-LuaWhitespace.ps1 index 1019b25..5f42866 100644 --- a/src/functions/private/Skip-LuaWhitespace.ps1 +++ b/src/functions/private/Skip-LuaWhitespace.ps1 @@ -50,7 +50,7 @@ $script:luaPos++ } if (-not $foundClosingDelimiter) { - throw "Unterminated long-bracket comment." + throw 'Unterminated long-bracket comment.' } } else { # Not a long bracket - treat as single-line comment From 05714e55bda453a2240ea9f99e432cd63bafa042 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 15 Apr 2026 03:09:03 +0200 Subject: [PATCH 16/24] Address PR review feedback --- src/functions/private/Format-LuaKey.ps1 | 2 +- src/functions/public/Lua/ConvertFrom-Lua.ps1 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/functions/private/Format-LuaKey.ps1 b/src/functions/private/Format-LuaKey.ps1 index e03016c..54ee635 100644 --- a/src/functions/private/Format-LuaKey.ps1 +++ b/src/functions/private/Format-LuaKey.ps1 @@ -30,7 +30,7 @@ return $Key } $escaped = $Key ` - -replace '\\', '\\\\' ` + -replace '\\', '\\' ` -replace '"', '\"' ` -replace "`n", '\n' ` -replace "`r", '\r' ` diff --git a/src/functions/public/Lua/ConvertFrom-Lua.ps1 b/src/functions/public/Lua/ConvertFrom-Lua.ps1 index a6bc0fc..e8fb4f5 100644 --- a/src/functions/public/Lua/ConvertFrom-Lua.ps1 +++ b/src/functions/public/Lua/ConvertFrom-Lua.ps1 @@ -84,8 +84,8 @@ process { $result = ConvertFrom-LuaTable -InputString $InputObject -AsPSCustomObject:(-not $AsHashtable) -MaxDepth $Depth - if ($NoEnumerate) { - , $result + if ($NoEnumerate -and $result -is [System.Array]) { + Write-Output -NoEnumerate $result } else { $result } From 7923add1e4559bb9b5c3b1619d441ffdb41139f1 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 15 Apr 2026 03:42:49 +0200 Subject: [PATCH 17/24] Address PR review feedback --- src/functions/private/Format-LuaKey.ps1 | 3 +++ src/functions/private/Read-LuaTable.ps1 | 3 +++ src/functions/public/Lua/ConvertFrom-Lua.ps1 | 2 +- tests/Lua.Tests.ps1 | 10 ++++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/functions/private/Format-LuaKey.ps1 b/src/functions/private/Format-LuaKey.ps1 index 54ee635..49815b9 100644 --- a/src/functions/private/Format-LuaKey.ps1 +++ b/src/functions/private/Format-LuaKey.ps1 @@ -32,9 +32,12 @@ $escaped = $Key ` -replace '\\', '\\' ` -replace '"', '\"' ` + -replace "`0", '\0' ` + -replace "`a", '\a' ` -replace "`n", '\n' ` -replace "`r", '\r' ` -replace "`t", '\t' ` + -replace "`v", '\v' ` -replace "`b", '\b' ` -replace "`f", '\f' return "[`"$escaped`"]" diff --git a/src/functions/private/Read-LuaTable.ps1 b/src/functions/private/Read-LuaTable.ps1 index 53be7be..0ceadc1 100644 --- a/src/functions/private/Read-LuaTable.ps1 +++ b/src/functions/private/Read-LuaTable.ps1 @@ -122,6 +122,9 @@ if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq '}') { $script:luaPos++ # skip } + } else { + $script:luaCurrentDepth-- + throw "Unterminated Lua table constructor. Expected '}' before end of input." } $script:luaCurrentDepth-- diff --git a/src/functions/public/Lua/ConvertFrom-Lua.ps1 b/src/functions/public/Lua/ConvertFrom-Lua.ps1 index e8fb4f5..54580db 100644 --- a/src/functions/public/Lua/ConvertFrom-Lua.ps1 +++ b/src/functions/public/Lua/ConvertFrom-Lua.ps1 @@ -85,7 +85,7 @@ process { $result = ConvertFrom-LuaTable -InputString $InputObject -AsPSCustomObject:(-not $AsHashtable) -MaxDepth $Depth if ($NoEnumerate -and $result -is [System.Array]) { - Write-Output -NoEnumerate $result + Write-Output -InputObject $result -NoEnumerate } else { $result } diff --git a/tests/Lua.Tests.ps1 b/tests/Lua.Tests.ps1 index 620c9b9..30c0e3a 100644 --- a/tests/Lua.Tests.ps1 +++ b/tests/Lua.Tests.ps1 @@ -476,6 +476,10 @@ B = { val = 2 } It 'Throws on unterminated multi-line string' { { ConvertFrom-Lua -InputObject '[[hello' } | Should -Throw '*Unterminated*' } + + It 'Throws on unterminated table (missing closing brace)' { + { ConvertFrom-Lua -InputObject '{ a = 1' } | Should -Throw '*Unterminated*' + } } Context 'Pipeline input' { @@ -879,6 +883,12 @@ Describe 'ConvertTo-Lua' { $result = ConvertTo-Lua -InputObject @{ 'while' = 'loop' } -Compress $result | Should -Be '{["while"]="loop"}' } + + It 'Escapes control characters in bracket-quoted keys' { + $key = "line1`nline2" + $result = ConvertTo-Lua -InputObject @{ $key = 'value' } -Compress + $result | Should -Be '{["line1\nline2"]="value"}' + } } Context 'PSCustomObject' { From 58c59803de1ddd4fe2d1cda6bd61629f5bc5870f Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 15 Apr 2026 04:00:24 +0200 Subject: [PATCH 18/24] Address PR review feedback --- src/functions/private/ConvertTo-LuaTable.ps1 | 19 +++++++++++-- src/functions/public/Lua/ConvertTo-Lua.ps1 | 2 +- tests/Lua.Tests.ps1 | 29 ++++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/functions/private/ConvertTo-LuaTable.ps1 b/src/functions/private/ConvertTo-LuaTable.ps1 index 384ee64..75eebca 100644 --- a/src/functions/private/ConvertTo-LuaTable.ps1 +++ b/src/functions/private/ConvertTo-LuaTable.ps1 @@ -79,8 +79,21 @@ return $InputObject.ToString([System.Globalization.CultureInfo]::InvariantCulture) } - if ($InputObject -is [float] -or $InputObject -is [double] -or - $InputObject -is [decimal] -or $InputObject -is [single]) { + if ($InputObject -is [double]) { + if ([double]::IsNaN($InputObject) -or [double]::IsInfinity($InputObject)) { + throw "Cannot serialize non-finite double value '$InputObject' to Lua. Lua numeric literals do not support NaN or Infinity." + } + return $InputObject.ToString([System.Globalization.CultureInfo]::InvariantCulture) + } + + if ($InputObject -is [float] -or $InputObject -is [single]) { + if ([single]::IsNaN($InputObject) -or [single]::IsInfinity($InputObject)) { + throw "Cannot serialize non-finite single value '$InputObject' to Lua. Lua numeric literals do not support NaN or Infinity." + } + return $InputObject.ToString([System.Globalization.CultureInfo]::InvariantCulture) + } + + if ($InputObject -is [decimal]) { return $InputObject.ToString([System.Globalization.CultureInfo]::InvariantCulture) } @@ -155,7 +168,7 @@ } # Handle PSCustomObject - if ($InputObject -is [psobject]) { + if ($InputObject -is [System.Management.Automation.PSCustomObject]) { $properties = $InputObject.PSObject.Properties | Where-Object { $_.MemberType -eq 'NoteProperty' } if (-not $properties) { return '{}' diff --git a/src/functions/public/Lua/ConvertTo-Lua.ps1 b/src/functions/public/Lua/ConvertTo-Lua.ps1 index 71a2d04..17c4308 100644 --- a/src/functions/public/Lua/ConvertTo-Lua.ps1 +++ b/src/functions/public/Lua/ConvertTo-Lua.ps1 @@ -16,7 +16,7 @@ - [int] / [long] -> Lua integer - [float] / [double] -> Lua float - [bool] -> Lua boolean (true/false) - - $null -> omitted (nil means absent in Lua) + - $null -> top-level input serializes as nil; null-valued properties/keys are omitted .EXAMPLE ```powershell diff --git a/tests/Lua.Tests.ps1 b/tests/Lua.Tests.ps1 index 30c0e3a..9f2de79 100644 --- a/tests/Lua.Tests.ps1 +++ b/tests/Lua.Tests.ps1 @@ -973,6 +973,35 @@ Describe 'ConvertTo-Lua' { $result | Should -Be '{x=10}' } } + + Context 'Non-finite float values' { + It 'Throws on NaN double' { + { ConvertTo-Lua -InputObject ([double]::NaN) } | Should -Throw '*NaN*' + } + + It 'Throws on Infinity double' { + { ConvertTo-Lua -InputObject ([double]::PositiveInfinity) } | Should -Throw '*Infinity*' + } + + It 'Throws on negative Infinity double' { + { ConvertTo-Lua -InputObject ([double]::NegativeInfinity) } | Should -Throw '*Infinity*' + } + } + + Context '.NET object string fallback' { + It 'Serializes DateTime as string instead of empty table' { + $result = ConvertTo-Lua -InputObject ([datetime]'2026-01-01') -Compress + $result | Should -Not -Be '{}' + $result | Should -Match '^".*"$' + } + + It 'Serializes Guid as string instead of empty table' { + $guid = [guid]::NewGuid() + $result = ConvertTo-Lua -InputObject $guid -Compress + $result | Should -Not -Be '{}' + $result | Should -Match '^".*"$' + } + } } Describe 'Round-trip conversion' { From 3fb261ff370c4f542798e161e6cdf4073f07624c Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 15 Apr 2026 04:23:55 +0200 Subject: [PATCH 19/24] Address PR review feedback --- src/functions/private/ConvertTo-LuaTable.ps1 | 24 +++++++++++++++++--- tests/Lua.Tests.ps1 | 15 ++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/functions/private/ConvertTo-LuaTable.ps1 b/src/functions/private/ConvertTo-LuaTable.ps1 index 75eebca..984d7af 100644 --- a/src/functions/private/ConvertTo-LuaTable.ps1 +++ b/src/functions/private/ConvertTo-LuaTable.ps1 @@ -116,8 +116,16 @@ if ($CurrentDepth -ge $MaxDepth) { Write-Warning "Depth limit ($MaxDepth) exceeded. Serializing remaining object as string." $str = $InputObject.ToString() ` - -replace '\\', '\\\\' ` - -replace '"', '\"' + -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 "`"$str`"" } @@ -195,7 +203,17 @@ } # Fallback: convert to string - $escaped = ($InputObject.ToString()) -replace '\\', '\\\\' -replace '"', '\"' + $escaped = $InputObject.ToString() ` + -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`"" } diff --git a/tests/Lua.Tests.ps1 b/tests/Lua.Tests.ps1 index 9f2de79..46746d6 100644 --- a/tests/Lua.Tests.ps1 +++ b/tests/Lua.Tests.ps1 @@ -922,6 +922,15 @@ Describe 'ConvertTo-Lua' { $warnings = @($result | Where-Object { $_ -is [System.Management.Automation.WarningRecord] }) $warnings.Count | Should -BeGreaterThan 0 } + + It 'Escapes control characters in depth-limit fallback string' { + $inner = [System.Text.StringBuilder]::new("line1`nline2`ttab") + $obj = [ordered]@{ x = $inner } + $result = ConvertTo-Lua -InputObject $obj -Depth 1 -Compress 3>&1 + $output = @($result | Where-Object { $_ -is [string] }) + $output[-1] | Should -BeLike '*\n*' + $output[-1] | Should -BeLike '*\t*' + } } Context 'AsArray' { @@ -1001,6 +1010,12 @@ Describe 'ConvertTo-Lua' { $result | Should -Not -Be '{}' $result | Should -Match '^".*"$' } + + It 'Escapes control characters in generic fallback string' { + $sb = [System.Text.StringBuilder]::new("line1`nline2`ttab") + $result = ConvertTo-Lua -InputObject $sb -Compress + $result | Should -Be '"line1\nline2\ttab"' + } } } From d750a9af43d5d86ccf87cd2ec4697140885caba1 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 15 Apr 2026 11:52:20 +0200 Subject: [PATCH 20/24] Address PR review feedback - Assignment parser now accepts semicolons between statements (A = 1; B = 2) - Assignment-detection lookahead uses Skip-LuaWhitespace to handle comments between identifier and '=' - Added tests for both scenarios --- .../private/ConvertFrom-LuaTable.ps1 | 19 +++++++++++++------ tests/Lua.Tests.ps1 | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/functions/private/ConvertFrom-LuaTable.ps1 b/src/functions/private/ConvertFrom-LuaTable.ps1 index ad8fd0e..b3b3b63 100644 --- a/src/functions/private/ConvertFrom-LuaTable.ps1 +++ b/src/functions/private/ConvertFrom-LuaTable.ps1 @@ -63,12 +63,13 @@ $tryIdent = $script:luaString.Substring($script:luaPos, $tryPos - $script:luaPos) # Check it's not a keyword that starts a value (true/false/nil) if ($tryIdent -notin 'true', 'false', 'nil') { - # Skip whitespace after identifier to check for '=' - $peekPos = $tryPos - while ($peekPos -lt $script:luaString.Length -and - $script:luaString[$peekPos] -match '\s') { - $peekPos++ - } + # Skip whitespace and comments after identifier to check for '=' + # Use Skip-LuaWhitespace with save/restore to handle comments + $peekSavedPos = $script:luaPos + $script:luaPos = $tryPos + Skip-LuaWhitespace + $peekPos = $script:luaPos + $script:luaPos = $peekSavedPos # Check for '=' but not '==' if ($peekPos -lt $script:luaString.Length -and $script:luaString[$peekPos] -eq '=' -and @@ -112,7 +113,13 @@ $value = Read-LuaValue $assignments[$varName] = $value + # Consume optional semicolons between assignment statements Skip-LuaWhitespace + while ($script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -eq ';') { + $script:luaPos++ + Skip-LuaWhitespace + } } if ($script:luaAsPSCustomObject) { diff --git a/tests/Lua.Tests.ps1 b/tests/Lua.Tests.ps1 index 46746d6..0e81417 100644 --- a/tests/Lua.Tests.ps1 +++ b/tests/Lua.Tests.ps1 @@ -452,6 +452,25 @@ B = { val = 2 } $result.My_Addon_DB.enabled | Should -BeTrue } + It 'Parses semicolon-separated assignment statements' { + $lua = 'A = 1; B = 2; C = "three"' + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.A | Should -Be 1 + $result.B | Should -Be 2 + $result.C | Should -Be 'three' + } + + It 'Parses assignments with comment between identifier and equals' { + $lua = @' +A --[[ comment ]] += { val = 1 } +B = { val = 2 } +'@ + $result = ConvertFrom-Lua -InputObject $lua -AsHashtable + $result.A.val | Should -Be 1 + $result.B.val | Should -Be 2 + } + It 'Does not treat true/false/nil as assignments' { $result = ConvertFrom-Lua -InputObject 'true' $result | Should -BeTrue From 5e16363f173a025aadf550e3d73bf0de98350d3a Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 15 Apr 2026 12:44:02 +0200 Subject: [PATCH 21/24] Address PR review feedback: throw on unexpected end of input in Read-LuaValue --- src/functions/private/Read-LuaValue.ps1 | 2 +- tests/Lua.Tests.ps1 | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/functions/private/Read-LuaValue.ps1 b/src/functions/private/Read-LuaValue.ps1 index ec3b677..99e399a 100644 --- a/src/functions/private/Read-LuaValue.ps1 +++ b/src/functions/private/Read-LuaValue.ps1 @@ -18,7 +18,7 @@ Skip-LuaWhitespace if ($script:luaPos -ge $script:luaString.Length) { - return $null + throw 'Unexpected end of input' } $char = $script:luaString[$script:luaPos] diff --git a/tests/Lua.Tests.ps1 b/tests/Lua.Tests.ps1 index 0e81417..0f34177 100644 --- a/tests/Lua.Tests.ps1 +++ b/tests/Lua.Tests.ps1 @@ -499,6 +499,14 @@ B = { val = 2 } It 'Throws on unterminated table (missing closing brace)' { { ConvertFrom-Lua -InputObject '{ a = 1' } | Should -Throw '*Unterminated*' } + + It 'Throws on whitespace-only input' { + { ConvertFrom-Lua -InputObject ' ' } | Should -Throw '*Unexpected end of input*' + } + + It 'Throws on assignment with missing value' { + { ConvertFrom-Lua -InputObject 'A = ' } | Should -Throw '*Unexpected end of input*' + } } Context 'Pipeline input' { From 575c4e10995763721bedf5c3136fbedca14104e6 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 15 Apr 2026 14:53:58 +0200 Subject: [PATCH 22/24] Address PR review feedback: update help text for long-bracket support, optimize comment skipping --- src/functions/private/Skip-LuaWhitespace.ps1 | 15 +++---------- src/functions/public/Lua/ConvertFrom-Lua.ps1 | 22 ++++++++++---------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/functions/private/Skip-LuaWhitespace.ps1 b/src/functions/private/Skip-LuaWhitespace.ps1 index 5f42866..4edd72d 100644 --- a/src/functions/private/Skip-LuaWhitespace.ps1 +++ b/src/functions/private/Skip-LuaWhitespace.ps1 @@ -38,20 +38,11 @@ # Valid long bracket comment opening $script:luaPos = $eqStart + $eqCount + 1 $closePattern = ']' + ('=' * $eqCount) + ']' - $closeLen = $closePattern.Length - $foundClosingDelimiter = $false - 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 - $foundClosingDelimiter = $true - break - } - $script:luaPos++ - } - if (-not $foundClosingDelimiter) { + $closingIndex = $script:luaString.IndexOf($closePattern, $script:luaPos) + if ($closingIndex -lt 0) { throw 'Unterminated long-bracket comment.' } + $script:luaPos = $closingIndex + $closePattern.Length } else { # Not a long bracket - treat as single-line comment while ($script:luaPos -lt $script:luaString.Length -and diff --git a/src/functions/public/Lua/ConvertFrom-Lua.ps1 b/src/functions/public/Lua/ConvertFrom-Lua.ps1 index 54580db..dbabba9 100644 --- a/src/functions/public/Lua/ConvertFrom-Lua.ps1 +++ b/src/functions/public/Lua/ConvertFrom-Lua.ps1 @@ -9,17 +9,17 @@ 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 + - 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 ([[ ]], [=[ ]=], [==[ ]==], etc.) -> [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 (--[[ ]], --[=[ ]=], --[==[ ]==], etc.) -> Ignored .EXAMPLE ```powershell From bb9b1a4fc5b365a4bb5eb144f3f21b9dd21527ea Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 15 Apr 2026 19:54:13 +0200 Subject: [PATCH 23/24] Address PR review feedback: use strict null assertions for nil tests --- tests/Lua.Tests.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Lua.Tests.ps1 b/tests/Lua.Tests.ps1 index 0f34177..2ca23d7 100644 --- a/tests/Lua.Tests.ps1 +++ b/tests/Lua.Tests.ps1 @@ -55,7 +55,7 @@ Describe 'ConvertFrom-Lua' { It 'Converts Lua nil to PowerShell $null' { $result = ConvertFrom-Lua -InputObject 'nil' - $result | Should -BeNullOrEmpty + $result | Should -Be $null } } @@ -417,7 +417,7 @@ DB2 = { y = 2 } It 'Parses assignment with nil value' { $lua = 'empty = nil' $result = ConvertFrom-Lua -InputObject $lua -AsHashtable - $result.empty | Should -BeNullOrEmpty + $result.empty | Should -Be $null } It 'Parses assignment returning PSCustomObject by default' { @@ -479,7 +479,7 @@ B = { val = 2 } $result | Should -BeFalse $result = ConvertFrom-Lua -InputObject 'nil' - $result | Should -BeNullOrEmpty + $result | Should -Be $null } } From d380c907683940edbba3f25a65ca049c4c11eb0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:51:56 +0200 Subject: [PATCH 24/24] Bump PSModule/Process-PSModule/.github/workflows/workflow.yml (#4) Bumps [PSModule/Process-PSModule/.github/workflows/workflow.yml](https://github.com/psmodule/process-psmodule) from 5.4.3 to 5.4.7. - [Release notes](https://github.com/psmodule/process-psmodule/releases) - [Commits](https://github.com/psmodule/process-psmodule/compare/60bdf8a5a4c92c53fcf2a8d23f7d5f5c93e6864e...11117919e65242d3388727819a751f74ad24ea9e) --- updated-dependencies: - dependency-name: PSModule/Process-PSModule/.github/workflows/workflow.yml dependency-version: 5.4.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>