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/.github/linters/.luacheckrc b/.github/linters/.luacheckrc new file mode 100644 index 0000000..628126f --- /dev/null +++ b/.github/linters/.luacheckrc @@ -0,0 +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 = {"111", "112"} +files["**/tests/data/WoWSavedVariables.lua"].ignore = {"111", "112"} 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 }} diff --git a/README.md b/README.md index 6319793..65909fe 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 +$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..6373801 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 +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/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/assemblies/LsonLib.dll b/src/assemblies/LsonLib.dll deleted file mode 100644 index 3661807..0000000 Binary files a/src/assemblies/LsonLib.dll and /dev/null differ diff --git a/src/classes/private/SecretWriter.ps1 b/src/classes/private/SecretWriter.ps1 deleted file mode 100644 index 1b1732a..0000000 --- a/src/classes/private/SecretWriter.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -class SecretWriter { - [string] $Alias - [string] $Name - [string] $Secret - - SecretWriter([string] $alias, [string] $name, [string] $secret) { - $this.Alias = $alias - $this.Name = $name - $this.Secret = $secret - } - - [string] GetAlias() { - return $this.Alias - } -} diff --git a/src/classes/public/Book.ps1 b/src/classes/public/Book.ps1 deleted file mode 100644 index 8917d9a..0000000 --- a/src/classes/public/Book.ps1 +++ /dev/null @@ -1,147 +0,0 @@ -class Book { - # Class properties - [string] $Title - [string] $Author - [string] $Synopsis - [string] $Publisher - [datetime] $PublishDate - [int] $PageCount - [string[]] $Tags - # Default constructor - Book() { $this.Init(@{}) } - # Convenience constructor from hashtable - Book([hashtable]$Properties) { $this.Init($Properties) } - # Common constructor for title and author - Book([string]$Title, [string]$Author) { - $this.Init(@{Title = $Title; Author = $Author }) - } - # Shared initializer method - [void] Init([hashtable]$Properties) { - foreach ($Property in $Properties.Keys) { - $this.$Property = $Properties.$Property - } - } - # Method to calculate reading time as 2 minutes per page - [timespan] GetReadingTime() { - if ($this.PageCount -le 0) { - throw 'Unable to determine reading time from page count.' - } - $Minutes = $this.PageCount * 2 - return [timespan]::new(0, $Minutes, 0) - } - # Method to calculate how long ago a book was published - [timespan] GetPublishedAge() { - if ( - $null -eq $this.PublishDate -or - $this.PublishDate -eq [datetime]::MinValue - ) { throw 'PublishDate not defined' } - - return (Get-Date) - $this.PublishDate - } - # Method to return a string representation of the book - [string] ToString() { - return "$($this.Title) by $($this.Author) ($($this.PublishDate.Year))" - } -} - -class BookList { - # Static property to hold the list of books - static [System.Collections.Generic.List[Book]] $Books - # Static method to initialize the list of books. Called in the other - # static methods to avoid needing to explicit initialize the value. - static [void] Initialize() { [BookList]::Initialize($false) } - static [bool] Initialize([bool]$force) { - if ([BookList]::Books.Count -gt 0 -and -not $force) { - return $false - } - - [BookList]::Books = [System.Collections.Generic.List[Book]]::new() - - return $true - } - # Ensure a book is valid for the list. - static [void] Validate([book]$Book) { - $Prefix = @( - 'Book validation failed: Book must be defined with the Title,' - 'Author, and PublishDate properties, but' - ) -join ' ' - if ($null -eq $Book) { throw "$Prefix was null" } - if ([string]::IsNullOrEmpty($Book.Title)) { - throw "$Prefix Title wasn't defined" - } - if ([string]::IsNullOrEmpty($Book.Author)) { - throw "$Prefix Author wasn't defined" - } - if ([datetime]::MinValue -eq $Book.PublishDate) { - throw "$Prefix PublishDate wasn't defined" - } - } - # Static methods to manage the list of books. - # Add a book if it's not already in the list. - static [void] Add([Book]$Book) { - [BookList]::Initialize() - [BookList]::Validate($Book) - if ([BookList]::Books.Contains($Book)) { - throw "Book '$Book' already in list" - } - - $FindPredicate = { - param([Book]$b) - - $b.Title -eq $Book.Title -and - $b.Author -eq $Book.Author -and - $b.PublishDate -eq $Book.PublishDate - }.GetNewClosure() - if ([BookList]::Books.Find($FindPredicate)) { - throw "Book '$Book' already in list" - } - - [BookList]::Books.Add($Book) - } - # Clear the list of books. - static [void] Clear() { - [BookList]::Initialize() - [BookList]::Books.Clear() - } - # Find a specific book using a filtering scriptblock. - static [Book] Find([scriptblock]$Predicate) { - [BookList]::Initialize() - return [BookList]::Books.Find($Predicate) - } - # Find every book matching the filtering scriptblock. - static [Book[]] FindAll([scriptblock]$Predicate) { - [BookList]::Initialize() - return [BookList]::Books.FindAll($Predicate) - } - # Remove a specific book. - static [void] Remove([Book]$Book) { - [BookList]::Initialize() - [BookList]::Books.Remove($Book) - } - # Remove a book by property value. - static [void] RemoveBy([string]$Property, [string]$Value) { - [BookList]::Initialize() - $Index = [BookList]::Books.FindIndex({ - param($b) - $b.$Property -eq $Value - }.GetNewClosure()) - if ($Index -ge 0) { - [BookList]::Books.RemoveAt($Index) - } - } -} - -enum Binding { - Hardcover - Paperback - EBook -} - -enum Genre { - Mystery - Thriller - Romance - ScienceFiction - Fantasy - Horror -} diff --git a/src/data/Config.psd1 b/src/data/Config.psd1 deleted file mode 100644 index fea4466..0000000 --- a/src/data/Config.psd1 +++ /dev/null @@ -1,3 +0,0 @@ -@{ - RandomKey = 'RandomValue' -} diff --git a/src/data/Settings.psd1 b/src/data/Settings.psd1 deleted file mode 100644 index bcfa7b4..0000000 --- a/src/data/Settings.psd1 +++ /dev/null @@ -1,3 +0,0 @@ -@{ - RandomSetting = 'RandomSettingValue' -} diff --git a/src/finally.ps1 b/src/finally.ps1 deleted file mode 100644 index d8fc207..0000000 --- a/src/finally.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -Write-Verbose '------------------------------' -Write-Verbose '--- THIS IS A LAST LOADER ---' -Write-Verbose '------------------------------' diff --git a/src/formats/CultureInfo.Format.ps1xml b/src/formats/CultureInfo.Format.ps1xml deleted file mode 100644 index a715e08..0000000 --- a/src/formats/CultureInfo.Format.ps1xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - 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..b3b3b63 --- /dev/null +++ b/src/functions/private/ConvertFrom-LuaTable.ps1 @@ -0,0 +1,146 @@ +function ConvertFrom-LuaTable { + <# + .SYNOPSIS + Parses a Lua table constructor string into a PowerShell object. + + .DESCRIPTION + Takes a Lua table constructor string and converts it to PowerShell + hashtables, arrays, and primitive types. This is the internal parsing + engine used by ConvertFrom-Lua. + #> + [OutputType([object])] + [OutputType([System.Collections.Specialized.OrderedDictionary])] + [CmdletBinding()] + param( + # The Lua table string to parse. + [Parameter(Mandatory)] + [string] $InputString, + + # Whether to output PSCustomObjects instead of hashtables. + [Parameter()] + [switch] $AsPSCustomObject, + + # Maximum allowed nesting depth. + [Parameter()] + [int] $MaxDepth = 1024 + ) + + begin {} + + process { + $script:luaString = $InputString + $script:luaPos = 0 + $script:luaAsPSCustomObject = $AsPSCustomObject.IsPresent + $script:luaMaxDepth = $MaxDepth + $script:luaCurrentDepth = 0 + + Skip-LuaWhitespace + + # Skip optional leading 'return' keyword (common in Lua data files) + if ($script:luaPos + 6 -le $script:luaString.Length -and + $script:luaString.Substring($script:luaPos, 6) -ceq 'return') { + $nextPos = $script:luaPos + 6 + if ($nextPos -ge $script:luaString.Length -or + $script:luaString[$nextPos] -notmatch '[a-zA-Z0-9_]') { + $script:luaPos = $nextPos + Skip-LuaWhitespace + } + } + + # 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 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 + ($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 + + # 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) { + return [PSCustomObject]$assignments + } + return $assignments + } + + # Reset position (no assignment detected, or it was a keyword value) + $script:luaPos = $savedPos + + $result = Read-LuaValue + + Skip-LuaWhitespace + if ($script:luaPos -lt $script:luaString.Length) { + $remainingInput = $script:luaString.Substring($script:luaPos) + throw "Unexpected trailing content after Lua value at position $($script:luaPos): $remainingInput" + } + + return $result + } + + end {} +} diff --git a/src/functions/private/ConvertTo-LuaTable.ps1 b/src/functions/private/ConvertTo-LuaTable.ps1 new file mode 100644 index 0000000..984d7af --- /dev/null +++ b/src/functions/private/ConvertTo-LuaTable.ps1 @@ -0,0 +1,221 @@ +function ConvertTo-LuaTable { + <# + .SYNOPSIS + Converts a PowerShell object to a Lua table string representation. + + .DESCRIPTION + Recursively converts a PowerShell object (hashtable, array, PSCustomObject, or primitive) + into a Lua table constructor string. This is the internal serialization engine used by ConvertTo-Lua. + + Uses fixed 4-space indentation per the Lua community convention. + Properties with $null values are omitted (Lua nil-means-absent semantics). + #> + [OutputType([string])] + [CmdletBinding()] + param( + # The object to convert to a Lua table string. + [Parameter(Mandatory)] + [AllowNull()] + [object] $InputObject, + + # The current recursion depth. + [Parameter()] + [int] $CurrentDepth = 0, + + # Maximum allowed recursion depth. + [Parameter()] + [int] $MaxDepth = 2, + + # Whether to compress the output (no newlines or indentation). + [Parameter()] + [switch] $Compress, + + # Serialize enum values as their string name instead of numeric value. + [Parameter()] + [switch] $EnumsAsStrings + ) + + begin { + $indent = if ($Compress) { '' } else { ' ' * (4 * $CurrentDepth) } + $childIndent = if ($Compress) { '' } else { ' ' * (4 * ($CurrentDepth + 1)) } + $newline = if ($Compress) { '' } else { "`n" } + $separator = if ($Compress) { ',' } else { ",`n" } + } + + process { + if ($null -eq $InputObject) { + return 'nil' + } + + if ($InputObject -is [bool]) { + if ($InputObject) { + return 'true' + } else { + return 'false' + } + } + + # Enum handling + if ($InputObject -is [enum]) { + if ($EnumsAsStrings) { + $escaped = $InputObject.ToString() -replace '\\', '\\\\' -replace '"', '\"' + return "`"$escaped`"" + } + $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 + $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 [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) + } + + if ($InputObject -is [string]) { + $escaped = $InputObject ` + -replace '\\', '\\' ` + -replace '"', '\"' ` + -replace "`0", '\0' ` + -replace "`a", '\a' ` + -replace "`b", '\b' ` + -replace "`f", '\f' ` + -replace "`n", '\n' ` + -replace "`r", '\r' ` + -replace "`t", '\t' ` + -replace "`v", '\v' + return "`"$escaped`"" + } + + # Depth check for complex types + if ($CurrentDepth -ge $MaxDepth) { + Write-Warning "Depth limit ($MaxDepth) exceeded. Serializing remaining object as string." + $str = $InputObject.ToString() ` + -replace '\\', '\\' ` + -replace '"', '\"' ` + -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`"" + } + + if ($InputObject -is [System.Collections.IList]) { + if ($InputObject.Count -eq 0) { + return '{}' + } + $items = [System.Collections.Generic.List[string]]::new() + foreach ($item in $InputObject) { + $childParams = @{ + InputObject = $item + CurrentDepth = $CurrentDepth + 1 + MaxDepth = $MaxDepth + Compress = $Compress + EnumsAsStrings = $EnumsAsStrings + } + $value = ConvertTo-LuaTable @childParams + $items.Add("$childIndent$value") + } + return "{$newline$($items -join $separator)$newline$indent}" + } + + # Handle hashtables and ordered dictionaries + if ($InputObject -is [System.Collections.IDictionary]) { + if ($InputObject.Count -eq 0) { + return '{}' + } + $entries = [System.Collections.Generic.List[string]]::new() + foreach ($key in $InputObject.Keys) { + $val = $InputObject[$key] + # Omit $null values per Lua nil-means-absent semantics + if ($null -eq $val) { + continue + } + $value = ConvertTo-LuaTable -InputObject $val ` + -CurrentDepth ($CurrentDepth + 1) ` + -MaxDepth $MaxDepth ` + -Compress:$Compress ` + -EnumsAsStrings:$EnumsAsStrings + $luaKey = Format-LuaKey -Key ([string]$key) + $space = if ($Compress) { '' } else { ' ' } + $entries.Add("$childIndent$luaKey$space=${space}$value") + } + if ($entries.Count -eq 0) { + return '{}' + } + return "{$newline$($entries -join $separator)$newline$indent}" + } + + # Handle PSCustomObject + if ($InputObject -is [System.Management.Automation.PSCustomObject]) { + $properties = $InputObject.PSObject.Properties | Where-Object { $_.MemberType -eq 'NoteProperty' } + if (-not $properties) { + return '{}' + } + $entries = [System.Collections.Generic.List[string]]::new() + foreach ($prop in $properties) { + # Omit $null values per Lua nil-means-absent semantics + if ($null -eq $prop.Value) { + continue + } + $value = ConvertTo-LuaTable -InputObject $prop.Value ` + -CurrentDepth ($CurrentDepth + 1) ` + -MaxDepth $MaxDepth ` + -Compress:$Compress ` + -EnumsAsStrings:$EnumsAsStrings + $luaKey = Format-LuaKey -Key $prop.Name + $space = if ($Compress) { '' } else { ' ' } + $entries.Add("$childIndent$luaKey$space=${space}$value") + } + if ($entries.Count -eq 0) { + return '{}' + } + return "{$newline$($entries -join $separator)$newline$indent}" + } + + # Fallback: convert to string + $escaped = $InputObject.ToString() ` + -replace '\\', '\\' ` + -replace '"', '\"' ` + -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`"" + } + + end {} +} diff --git a/src/functions/private/Format-LuaKey.ps1 b/src/functions/private/Format-LuaKey.ps1 new file mode 100644 index 0000000..49815b9 --- /dev/null +++ b/src/functions/private/Format-LuaKey.ps1 @@ -0,0 +1,47 @@ +function Format-LuaKey { + <# + .SYNOPSIS + Formats a string as a valid Lua table key. + + .DESCRIPTION + Returns the key as a bare identifier if it matches Lua identifier rules + and is not a reserved word, otherwise wraps it in bracket-quote notation: ["key"]. + #> + [OutputType([string])] + [CmdletBinding()] + param( + # The key string to format. + [Parameter(Mandatory)] + [string] $Key + ) + + begin { + # Lua 5.4 reserved words per §3.1 + $reservedWords = @( + 'and', 'break', 'do', 'else', 'elseif', 'end', + 'false', 'for', 'function', 'goto', 'if', 'in', + 'local', 'nil', 'not', 'or', 'repeat', 'return', + 'then', 'true', 'until', 'while' + ) + } + + process { + if ($Key -match '^[a-zA-Z_][a-zA-Z0-9_]*$' -and $Key -notin $reservedWords) { + return $Key + } + $escaped = $Key ` + -replace '\\', '\\' ` + -replace '"', '\"' ` + -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`"]" + } + + 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/Read-LuaHexFloat.ps1 b/src/functions/private/Read-LuaHexFloat.ps1 new file mode 100644 index 0000000..9274645 --- /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..66ed9e2 --- /dev/null +++ b/src/functions/private/Read-LuaMultiLineString.ps1 @@ -0,0 +1,57 @@ +function Read-LuaMultiLineString { + <# + .SYNOPSIS + Reads a multi-line Lua string delimited by long brackets [[ ]], [=[ ]=], [==[ ]==], etc. + #> + [OutputType([string])] + [CmdletBinding()] + param() + + begin {} + + process { + # Count the number of '=' characters in the opening long bracket + $script:luaPos++ # skip first [ + $equalsCount = 0 + while ($script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -eq '=') { + $equalsCount++ + $script:luaPos++ + } + if ($script:luaPos -ge $script:luaString.Length -or + $script:luaString[$script:luaPos] -ne '[') { + throw 'Invalid long bracket string opening.' + } + $script:luaPos++ # skip second [ + + # Build the closing pattern: ] + N '=' + ] + $closingBracket = ']' + ('=' * $equalsCount) + ']' + $closeLen = $closingBracket.Length + + $result = [System.Text.StringBuilder]::new() + + # Per Lua spec, a newline immediately after the opening bracket is ignored + if ($script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -eq "`n") { + $script:luaPos++ + } elseif ($script:luaPos + 1 -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -eq "`r" -and + $script:luaString[$script:luaPos + 1] -eq "`n") { + $script:luaPos += 2 + } + + while ($script:luaPos -lt $script:luaString.Length) { + if ($script:luaPos + $closeLen - 1 -lt $script:luaString.Length -and + $script:luaString.Substring($script:luaPos, $closeLen) -eq $closingBracket) { + $script:luaPos += $closeLen + return $result.ToString() + } + $null = $result.Append($script:luaString[$script:luaPos]) + $script:luaPos++ + } + + throw 'Unterminated multi-line string.' + } + + end {} +} diff --git a/src/functions/private/Read-LuaNumber.ps1 b/src/functions/private/Read-LuaNumber.ps1 new file mode 100644 index 0000000..9b4346e --- /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..ad568ea --- /dev/null +++ b/src/functions/private/Read-LuaString.ps1 @@ -0,0 +1,184 @@ +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 + ) + 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 + } 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++ + } + if ($script:luaPos -ge $script:luaString.Length) { + throw 'Invalid \u escape sequence: missing closing brace.' + } + $hexStr = $script:luaString.Substring( + $hexStart, + $script:luaPos - $hexStart + ) + $codePoint = [Convert]::ToInt32($hexStr, 16) + $null = $result.Append( + [char]::ConvertFromUtf32($codePoint) + ) + $script:luaPos++ # skip } + } else { + throw 'Invalid \u escape sequence.' + } + } + "`n" { + $null = $result.Append("`n") + $script:luaPos++ + if ( + $script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -eq "`r" + ) { + $script:luaPos++ + } + } + "`r" { + $null = $result.Append("`n") + $script:luaPos++ + if ( + $script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -eq "`n" + ) { + $script:luaPos++ + } + } + 'z' { + $script:luaPos++ + while ( + $script:luaPos -lt $script:luaString.Length -and + [char]::IsWhiteSpace($script:luaString[$script:luaPos]) + ) { + $script:luaPos++ + } + } + default { + # \ddd - decimal byte sequence (1-3 digits) + if ($nextChar -match '[0-9]') { + $numStr = $nextChar.ToString() + $script:luaPos++ + for ($d = 0; $d -lt 2; $d++) { + if ($script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -match '[0-9]') { + $numStr += $script:luaString[$script:luaPos] + $script:luaPos++ + } else { + break + } + } + $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) + $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..0ceadc1 --- /dev/null +++ b/src/functions/private/Read-LuaTable.ps1 @@ -0,0 +1,164 @@ +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 + # 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 '[' -and + $script:luaString[$script:luaPos + 1] -ne '=') { + $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 ']') { + throw "Expected ']' after bracket key in Lua table." + } + $script:luaPos++ # skip ] + Skip-LuaWhitespace + if ($script:luaPos -ge $script:luaString.Length -or + $script:luaString[$script:luaPos] -ne '=') { + throw "Expected '=' after bracket key in Lua table." + } + $script:luaPos++ # skip = + Skip-LuaWhitespace + $value = Read-LuaValue + $entries.Add(@{ Key = [string]$key; Value = $value }) + $hasStringKeys = $true + } elseif ($script:luaString[$script:luaPos] -match '[a-zA-Z_]') { + # Check for identifier key: key = value + $identStart = $script:luaPos + while ($script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -match '[a-zA-Z0-9_]') { + $script:luaPos++ + } + $ident = $script:luaString.Substring( + $identStart, $script:luaPos - $identStart + ) + + Skip-LuaWhitespace + + if ($script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -eq '=') { + # Key = value pair + $script:luaPos++ # skip = + Skip-LuaWhitespace + $value = Read-LuaValue + $entries.Add(@{ + Key = $ident + Value = $value + }) + $hasStringKeys = $true + } else { + # Bare identifier as keyword value + switch ($ident) { + 'true' { $arrayValues.Add($true) } + 'false' { $arrayValues.Add($false) } + 'nil' { $arrayValues.Add($null) } + default { + throw "Unexpected bare identifier '$ident'." + } + } + $hasArrayValues = $true + } + } else { + # Array value + $value = Read-LuaValue + $arrayValues.Add($value) + $hasArrayValues = $true + } + + Skip-LuaWhitespace + + # Lua requires a comma or semicolon between fields unless the next token is } + if ($script:luaPos -lt $script:luaString.Length) { + if ($script:luaString[$script:luaPos] -eq ',' -or + $script:luaString[$script:luaPos] -eq ';') { + $script:luaPos++ + } elseif ($script:luaString[$script:luaPos] -ne '}') { + throw "Expected ',', ';', or '}' in Lua table constructor." + } + } + } + + if ($script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -eq '}') { + $script:luaPos++ # skip } + } else { + $script:luaCurrentDepth-- + throw "Unterminated Lua table constructor. Expected '}' before end of input." + } + + $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..99e399a --- /dev/null +++ b/src/functions/private/Read-LuaValue.ps1 @@ -0,0 +1,86 @@ +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) { + throw 'Unexpected end of input' + } + + $char = $script:luaString[$script:luaPos] + + # Table + if ($char -eq '{') { + return Read-LuaTable + } + + # String (double-quoted) + if ($char -eq '"') { + return Read-LuaString -QuoteChar '"' + } + + # String (single-quoted) + if ($char -eq "'") { + return Read-LuaString -QuoteChar "'" + } + + # Multi-line string [[ ... ]] or [=[ ... ]=] + if ($char -eq '[' -and + $script:luaPos + 1 -lt $script:luaString.Length -and + ($script:luaString[$script:luaPos + 1] -eq '[' -or + $script:luaString[$script:luaPos + 1] -eq '=')) { + return Read-LuaMultiLineString + } + + # Number or negative number (including .5 style floats) + if ($char -match '[0-9]' -or + ($char -eq '.' -and + $script:luaPos + 1 -lt $script:luaString.Length -and + $script:luaString[$script:luaPos + 1] -match '[0-9]') -or + ($char -eq '-' -and + $script:luaPos + 1 -lt $script:luaString.Length -and + $script:luaString[$script:luaPos + 1] -match '[0-9.]')) { + return Read-LuaNumber + } + + # Keywords and bare identifiers + if ($char -match '[a-zA-Z_]') { + $identStart = $script:luaPos + while ($script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -match '[a-zA-Z0-9_]') { + $script:luaPos++ + } + $ident = $script:luaString.Substring( + $identStart, + $script:luaPos - $identStart + ) + + switch ($ident) { + 'true' { return $true } + 'false' { return $false } + 'nil' { return $null } + default { + throw "Unexpected bare identifier '$ident' at position $identStart." + } + } + } + + throw "Unexpected character '$char' at position $($script:luaPos)." + } + + end {} +} 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/private/Skip-LuaWhitespace.ps1 b/src/functions/private/Skip-LuaWhitespace.ps1 new file mode 100644 index 0000000..4edd72d --- /dev/null +++ b/src/functions/private/Skip-LuaWhitespace.ps1 @@ -0,0 +1,68 @@ +function Skip-LuaWhitespace { + <# + .SYNOPSIS + Advances the parser position past whitespace and comments. + #> + [CmdletBinding()] + param() + + begin {} + + process { + while ($script:luaPos -lt $script:luaString.Length) { + $char = $script:luaString[$script:luaPos] + + # Skip whitespace + if ($char -match '\s') { + $script:luaPos++ + continue + } + + # Skip comments + if ($script:luaPos + 1 -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -eq '-' -and + $script:luaString[$script:luaPos + 1] -eq '-') { + $script:luaPos += 2 + + # Multi-line comment --[[ ... ]] or --[=[ ... ]=] etc. + if ($script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -eq '[') { + $eqStart = $script:luaPos + 1 + $eqCount = 0 + while ($eqStart + $eqCount -lt $script:luaString.Length -and + $script:luaString[$eqStart + $eqCount] -eq '=') { + $eqCount++ + } + if ($eqStart + $eqCount -lt $script:luaString.Length -and + $script:luaString[$eqStart + $eqCount] -eq '[') { + # Valid long bracket comment opening + $script:luaPos = $eqStart + $eqCount + 1 + $closePattern = ']' + ('=' * $eqCount) + ']' + $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 + $script:luaString[$script:luaPos] -ne "`n") { + $script:luaPos++ + } + } + } else { + # Single-line comment + while ($script:luaPos -lt $script:luaString.Length -and + $script:luaString[$script:luaPos] -ne "`n") { + $script:luaPos++ + } + } + continue + } + + break + } + } + + end {} +} diff --git a/src/functions/public/Lua/ConvertFrom-Lua.ps1 b/src/functions/public/Lua/ConvertFrom-Lua.ps1 new file mode 100644 index 0000000..dbabba9 --- /dev/null +++ b/src/functions/public/Lua/ConvertFrom-Lua.ps1 @@ -0,0 +1,95 @@ +function ConvertFrom-Lua { + <# + .SYNOPSIS + Converts a Lua table constructor string to a PowerShell object. + + .DESCRIPTION + Takes a Lua table constructor string and parses it into PowerShell objects. + By default, Lua tables with string keys become PSCustomObjects and Lua + sequences become arrays. Use -AsHashtable to get ordered hashtables instead. + + Supports the following Lua to PowerShell type mappings: + - Lua table (key = value) -> [PSCustomObject] or [ordered] hashtable + - Lua sequence (array) -> [object[]] + - Lua double-quoted string -> [string] + - Lua single-quoted string -> [string] + - Lua multi-line string ([[ ]], [=[ ]=], [==[ ]==], 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 + '{ name = "Alice", age = 30 }' | ConvertFrom-Lua + + name age + ---- --- + Alice 30 + ``` + + .EXAMPLE + ```powershell + ConvertFrom-Lua -InputObject '{ 1, 2, 3 }' + + 1 + 2 + 3 + ``` + + .EXAMPLE + ```powershell + '{ name = "Alice" }' | ConvertFrom-Lua -AsHashtable + + Name Value + ---- ----- + name Alice + ``` + + .NOTES + [Lua 5.4 Reference Manual - Table Constructors](https://www.lua.org/manual/5.4/manual.html#3.4.9) + + .LINK + https://psmodule.io/Lua/Functions/ConvertFrom-Lua/ + + .LINK + https://www.lua.org/manual/5.4/manual.html#3.4.9 + #> + [OutputType([object])] + [OutputType([System.Array])] + [CmdletBinding()] + param( + # The Lua table constructor string to convert to a PowerShell object. + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [ValidateNotNullOrEmpty()] + [string] $InputObject, + + # Output ordered hashtables instead of PSCustomObjects for Lua tables with string keys. + [Parameter()] + [switch] $AsHashtable, + + # Max nesting depth allowed in input. Throws a terminating error when exceeded. + [Parameter()] + [ValidateRange(0, 1024)] + [int] $Depth = 1024, + + # Output arrays as a single object instead of enumerating elements through the pipeline. + [Parameter()] + [switch] $NoEnumerate + ) + + begin {} + + process { + $result = ConvertFrom-LuaTable -InputString $InputObject -AsPSCustomObject:(-not $AsHashtable) -MaxDepth $Depth + if ($NoEnumerate -and $result -is [System.Array]) { + Write-Output -InputObject $result -NoEnumerate + } else { + $result + } + } + + 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..17c4308 --- /dev/null +++ b/src/functions/public/Lua/ConvertTo-Lua.ps1 @@ -0,0 +1,97 @@ +function ConvertTo-Lua { + <# + .SYNOPSIS + Converts a PowerShell object to a Lua table constructor string. + + .DESCRIPTION + Takes a PowerShell object (hashtable, PSCustomObject, array, or primitive value) and + converts it to a Lua table constructor string representation. Nested structures are + recursively converted with 4-space indentation. + + Supports the following type mappings: + - [hashtable] / [ordered] -> Lua table with key = value pairs + - [PSCustomObject] -> Lua table with key = value pairs + - [array] -> Lua table (sequence) + - [string] -> Lua double-quoted string with escape sequences + - [int] / [long] -> Lua integer + - [float] / [double] -> Lua float + - [bool] -> Lua boolean (true/false) + - $null -> top-level input serializes as nil; null-valued properties/keys are omitted + + .EXAMPLE + ```powershell + @{ name = "Alice"; age = 30 } | ConvertTo-Lua + + { + age = 30, + name = "Alice" + } + ``` + + .EXAMPLE + ```powershell + ConvertTo-Lua -InputObject @(1, 2, 3) -Compress + + {1,2,3} + ``` + + .EXAMPLE + ```powershell + "hello" | ConvertTo-Lua -AsArray + + { + "hello" + } + ``` + + .NOTES + [Lua 5.4 Reference Manual - Table Constructors](https://www.lua.org/manual/5.4/manual.html#3.4.9) + + .LINK + https://psmodule.io/Lua/Functions/ConvertTo-Lua/ + + .LINK + https://www.lua.org/manual/5.4/manual.html#3.4.9 + #> + [OutputType([string])] + [CmdletBinding()] + param( + # The object to convert to a Lua table constructor string. + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [AllowNull()] + [object] $InputObject, + + # Max recursion depth for nested object serialization. Emits a warning when exceeded. + [Parameter()] + [ValidateRange(0, 100)] + [int] $Depth = 2, + + # Omit whitespace and indentation. + [Parameter()] + [switch] $Compress, + + # Serialize PowerShell enum values as their string name instead of numeric value. + [Parameter()] + [switch] $EnumsAsStrings, + + # Always wrap output in a Lua sequence table, even for a single value. + [Parameter()] + [switch] $AsArray + ) + + begin {} + + process { + $objectToConvert = $InputObject + if ($AsArray -and $InputObject -isnot [System.Collections.IList]) { + $objectToConvert = @(, $InputObject) + } + ConvertTo-LuaTable -InputObject $objectToConvert ` + -CurrentDepth 0 ` + -MaxDepth $Depth ` + -Compress:$Compress ` + -EnumsAsStrings:$EnumsAsStrings + } + + end {} +} 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 deleted file mode 100644 index cc1fde9..0000000 --- a/src/header.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains long links.')] -[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 deleted file mode 100644 index ff720bd..0000000 --- a/src/manifest.psd1 +++ /dev/null @@ -1,5 +0,0 @@ -# This file always wins! -# Use this file to override any of the framework defaults and generated values. -@{ - ModuleVersion = '0.0.0' -} 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..2ca23d7 --- /dev/null +++ b/tests/Lua.Tests.ps1 @@ -0,0 +1,1361 @@ +[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 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 + $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 -Be $null + } + } + + 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: \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"' + } + + It 'Handles escaped backslashes' { + $result = ConvertFrom-Lua -InputObject '"path\\to\\file"' + $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' { + # Lua uses \' inside single-quoted strings + $result = ConvertFrom-Lua -InputObject "'it\'s'" + $result | 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)' { + 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' { + $result = ConvertFrom-Lua -InputObject '{}' + if ($result -is [System.Collections.IDictionary]) { + $result.Count | Should -Be 0 + } else { + @($result.PSObject.Properties).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 + } + + 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) - 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' + } + + 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 + } + } + + 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' { + 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' + } + + 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 '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 -Be $null + } + + 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 '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 + + $result = ConvertFrom-Lua -InputObject 'false' + $result | Should -BeFalse + + $result = ConvertFrom-Lua -InputObject 'nil' + $result | Should -Be $null + } + } + + Context 'Error cases' { + 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*' + } + + 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' { + 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 + } + } + + 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' + } + } + + 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' { + 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"' + } + + 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' { + 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 '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"}' + } + + 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' { + 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 '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 + } + + 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' { + 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}' + } + } + + 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 '^".*"$' + } + + 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"' + } + } +} + +Describe 'Round-trip conversion' { + BeforeAll { + $dataPath = Join-Path -Path $PSScriptRoot -ChildPath 'data' + } + + 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 -AsHashtable + $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 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 + options = [ordered]@{ + debug = $false + verbose = $true + } + } + $lua = ConvertTo-Lua -InputObject $original + $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 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 -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 + } + + 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 '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/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..a6c7eaf --- /dev/null +++ b/tests/data/Arrays.lua @@ -0,0 +1,6 @@ +return { + 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/Assignments.json b/tests/data/Assignments.json new file mode 100644 index 0000000..c79a9dc --- /dev/null +++ b/tests/data/Assignments.json @@ -0,0 +1,16 @@ +{ + "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/DeepStructure.json b/tests/data/DeepStructure.json new file mode 100644 index 0000000..f056acc --- /dev/null +++ b/tests/data/DeepStructure.json @@ -0,0 +1,118 @@ +{ + "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..c238340 --- /dev/null +++ b/tests/data/DeepStructure.lua @@ -0,0 +1,97 @@ +return { + 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/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..028b8c1 --- /dev/null +++ b/tests/data/Strings.lua @@ -0,0 +1,7 @@ +return { + 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..8f7eb5e --- /dev/null +++ b/tests/data/TestStructure.lua @@ -0,0 +1,65 @@ +return { + 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/WoWSavedVariables.json b/tests/data/WoWSavedVariables.json new file mode 100644 index 0000000..22eb0e4 --- /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, + }, + }, + }, +}