Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ $doc.Data['version'] = 2
Export-Toml -InputObject $doc -Path './config.toml'
```

### Normalize TOML text

```powershell
Get-Content 'Cargo.toml' -Raw | Format-Toml
Format-Toml -Path 'Cargo.toml' -Indent 4
```

`Format-Toml` parses then re-serializes TOML text, producing consistent key
quoting and canonical scalar forms β€” equivalent to
`ConvertFrom-Toml | ConvertTo-Toml` as a single call. TOML itself has no
indentation semantics; `-Indent` (default `2`) only controls how many spaces
are used to visually nest table headers and their keys by depth. Set
`-Indent 0` for the flat, unindented form.

### Merge two TOML documents

```powershell
Expand Down Expand Up @@ -124,6 +138,7 @@ Merge-Toml -BaseObject $defaults -OverrideObject $overrides
| `ConvertTo-Toml` | Serialize object β†’ TOML text |
| `Import-Toml` | Read TOML file β†’ `TomlDocument` |
| `Export-Toml` | Write object or `TomlDocument` to file |
| `Format-Toml` | Normalize TOML text to canonical form |
| `Merge-Toml` | Merge two TOML documents into one |

## Implementation notes
Expand Down
15 changes: 15 additions & 0 deletions examples/General.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ Write-Host $toml
# $doc.Data['version'] = 3
# Export-Toml -InputObject $doc -Path './config.toml'

# ── Normalize TOML text ────────────────────────────────────────────────────
$normalized = Format-Toml -InputObject @'
title="My App"
[server]
host="localhost"
port=8080
'@
Write-Host $normalized
# title = "My App"
#
# [server]
# host = "localhost"
# port = 8080


# ── Pipeline usage ─────────────────────────────────────────────────────────
# '[server]
# host = "localhost"' | ConvertFrom-Toml | ConvertTo-Toml
Expand Down
75 changes: 75 additions & 0 deletions src/functions/private/Add-TomlIndentation.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
function Add-TomlIndentation {
<#
.SYNOPSIS
Indents nested TOML table sections by nesting depth.

.DESCRIPTION
Re-indents already-serialized TOML text so that table headers and their
key/value lines are prefixed with spaces proportional to the table's nesting
depth. Depth is derived from the dotted path in each `[path]` / `[[path]]`
header β€” a root table has depth 1, `[a.b]` has depth 2, and so on. Header
lines are indented one level shallower than the keys they contain, so the
keys visually nest under their header. Root-level keys (before any header)
are never indented. TOML is whitespace-insensitive around keys and headers,
so this is purely a display convention and does not change the parsed value.

.EXAMPLE
Add-TomlIndentation -Toml "[a]`nx = 1`n[a.b]`ny = 2" -Indent 2
# Returns:
# [a]
# x = 1
# [a.b]
# y = 2

Indents nested table `[a.b]` and its key two spaces per nesting level.

.INPUTS
None. Parameters only.

.OUTPUTS
[string]
#>
[OutputType([string])]
[CmdletBinding()]
param(
# The canonical, non-indented TOML text produced by ConvertTo-Toml.
[Parameter(Mandatory)]
[AllowEmptyString()]
[string] $Toml,

# Number of spaces to indent per nesting level. 0 disables indentation.
[Parameter(Mandatory)]
[ValidateRange(0, 100)]
[int] $Indent
)

if ($Indent -eq 0 -or [string]::IsNullOrEmpty($Toml)) {
return $Toml
}

$lines = $Toml -split "`n"
$result = [System.Text.StringBuilder]::new()
$depth = 0

foreach ($rawLine in $lines) {
$line = $rawLine.TrimEnd("`r")

if ($line -match '^\[\[(.+)\]\]$' -or $line -match '^\[(.+)\]$') {
$path = $Matches[1]
$depth = (Split-TomlDottedKey -KeyPath $path).Count
$headerIndent = ' ' * ([Math]::Max(0, $depth - 1) * $Indent)
$null = $result.AppendLine("$headerIndent$line")
continue
}

if ([string]::IsNullOrWhiteSpace($line)) {
$null = $result.AppendLine()
continue
}

$lineIndent = ' ' * ($depth * $Indent)
$null = $result.AppendLine("$lineIndent$line")
}

return $result.ToString().TrimEnd()
}
108 changes: 108 additions & 0 deletions src/functions/public/Format-Toml.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
function Format-Toml {
<#
.SYNOPSIS
Normalizes TOML text to a canonical form.

.DESCRIPTION
Parses TOML text and re-serializes it, producing consistent key quoting,
canonical scalar formatting, and stable table ordering β€” semantically
equivalent to `ConvertFrom-Toml | ConvertTo-Toml` expressed as a single
ergonomic pipeline step. The result is idempotent: formatting already
canonical text returns the same text unchanged.

TOML itself is a flat, whitespace-insensitive format β€” nested tables are
represented as `[a.b]` headers rather than indented blocks, so there is no
spec-defined meaning for indentation. The `-Indent` parameter is offered as
a display convention only: it prefixes each table header and its key/value
lines with spaces proportional to the table's nesting depth (as some TOML
formatters, such as Taplo, do). This never changes the parsed value of the
document. Set `-Indent 0` to disable it and keep the flat, unindented form
that `ConvertTo-Toml` produces.

.EXAMPLE
Format-Toml -InputObject 'name="value" [ a ] x=1'

Reformats an inconsistently spaced TOML string into canonical form.

.EXAMPLE
Get-Content 'Cargo.toml' -Raw | Format-Toml

Reads a file's content and normalizes it via the pipeline.

.EXAMPLE
Format-Toml -Path 'Cargo.toml' -Indent 4

Reads a TOML file and returns its canonical form with nested tables
indented four spaces per level.

.EXAMPLE
Format-Toml -LiteralPath 'C:\configs\[env].toml'

Reads a file whose name contains wildcard-like characters, bypassing
wildcard expansion.

.INPUTS
[string] β€” pipeline input supported for the InputObject parameter.

.OUTPUTS
[string]

.NOTES
Throws when the input is not valid TOML 1.0.0, when a file path does not
resolve to an existing file, or when a file cannot be read. This function
does not validate β€” it formats. Use a try/catch (or `Test-Toml`, if
available) to check validity without raising a terminating error.

.LINK
https://psmodule.io/Toml/Functions/Format-Toml
#>
[OutputType([string])]
[CmdletBinding(DefaultParameterSetName = 'InputObject')]
param(
# TOML text to normalize.
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'InputObject')]
[ValidateNotNullOrEmpty()]
[string] $InputObject,

# Path to a TOML file to read and normalize. Accepts relative paths and wildcards.
[Parameter(Mandatory, ParameterSetName = 'Path')]
[ValidateNotNullOrEmpty()]
[string] $Path,

# Literal path to a TOML file to read and normalize. No wildcard expansion is performed.
[Parameter(Mandatory, ParameterSetName = 'LiteralPath')]
[ValidateNotNullOrEmpty()]
[string] $LiteralPath,

# Spaces used to indent nested table headers and keys per nesting level.
# This is a display convention only β€” TOML has no indentation semantics.
# Set to 0 to keep the flat, unindented canonical form.
[Parameter()]
[ValidateRange(0, 100)]
[int] $Indent = 2
)

process {
$tomlText = switch ($PSCmdlet.ParameterSetName) {
'Path' {
$resolvedPath = Resolve-Path -Path $Path -ErrorAction Stop
Write-Verbose "Reading TOML file: $($resolvedPath.ProviderPath)"
[System.IO.File]::ReadAllText($resolvedPath.ProviderPath)
}
'LiteralPath' {
$resolvedPath = Resolve-Path -LiteralPath $LiteralPath -ErrorAction Stop
Write-Verbose "Reading TOML file: $($resolvedPath.ProviderPath)"
[System.IO.File]::ReadAllText($resolvedPath.ProviderPath)
}
default {
$InputObject
}
}

Write-Verbose "Normalizing TOML text ($($tomlText.Length) character(s)), Indent=$Indent."
$doc = ConvertFrom-Toml -InputObject $tomlText
$canonical = ConvertTo-Toml -InputObject $doc

return Add-TomlIndentation -Toml $canonical -Indent $Indent
}
}
111 changes: 110 additions & 1 deletion tests/Toml.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Describe 'Toml' {
$commands | Should -Contain 'ConvertTo-Toml'
$commands | Should -Contain 'Import-Toml'
$commands | Should -Contain 'Export-Toml'
$commands | Should -Contain 'Merge-Toml'
$commands | Should -Contain 'Format-Toml'
$commands | Should -Contain 'Test-Toml'
}
}
Expand Down Expand Up @@ -804,6 +804,115 @@ Describe 'Toml' {
}
}

Describe 'Format-Toml' {

Context 'Return type' {
It 'returns a string' {
$result = Format-Toml -InputObject 'key = "value"'
$result | Should -BeOfType [string]
}
}

Context 'Normalization' {
It 'normalizes inconsistent key quoting' {
$result = Format-Toml -InputObject @'
"quoted" = 1
bare = 2
'@
$result | Should -Match 'quoted = 1'
$result | Should -Match 'bare = 2'
}

It 'is semantically equivalent to ConvertFrom-Toml | ConvertTo-Toml' {
$source = @'
title = "My App"
[server]
port = 8080
'@
$expected = ConvertTo-Toml -InputObject (ConvertFrom-Toml -InputObject $source)
(Format-Toml -InputObject $source -Indent 0) | Should -Be $expected
}

It 'indents nested tables by nesting depth' {
$result = Format-Toml -InputObject "[a]`nx = 1`n[a.b]`ny = 2" -Indent 2
$result | Should -Match "(?m)^ x = 1\r?$"
$result | Should -Match "(?m)^ \[a\.b\]\r?$"
$result | Should -Match "(?m)^ y = 2\r?$"
}

It 'produces flat, unindented output when Indent is 0' {
$result = Format-Toml -InputObject "[a]`nx = 1`n[a.b]`ny = 2" -Indent 0
$result | Should -Match "(?m)^x = 1\r?$"
$result | Should -Match "(?m)^y = 2\r?$"
}
}

Context 'Idempotency' {
It 'formatting already-canonical text returns the same text' {
$source = @'
title = "My App"
[server]
host = "localhost"
port = 8080
[server.nested]
key = "value"
'@
$once = Format-Toml -InputObject $source
$twice = Format-Toml -InputObject $once
$twice | Should -Be $once
}
}

Context 'Round-trip validity' {
It 'output remains parseable by ConvertFrom-Toml' {
$source = @'
[database]
host = "db.example.com"
ports = [8001, 8002]
'@
$formatted = Format-Toml -InputObject $source
$parsed = ConvertFrom-Toml -InputObject $formatted
$parsed.Data['database']['host'] | Should -Be 'db.example.com'
$parsed.Data['database']['ports'] | Should -HaveCount 2
}
}

Context 'Pipeline' {
It 'accepts InputObject from the pipeline' {
$result = 'key = "value"' | Format-Toml
$result | Should -Match 'key = "value"'
}
}

Context 'Path parameter' {
It 'reads a file and returns normalized TOML text' {
$path = Join-Path $TestDrive 'input.toml'
Set-Content -Path $path -Value '"quoted"=1' -NoNewline
$result = Format-Toml -Path $path
$result | Should -Match 'quoted = 1'
}
}

Context 'LiteralPath parameter' {
It 'reads a file and returns normalized TOML text' {
$path = Join-Path $TestDrive 'literal.toml'
Set-Content -Path $path -Value '"quoted"=1' -NoNewline
$result = Format-Toml -LiteralPath $path
$result | Should -Match 'quoted = 1'
}
}

Context 'Error handling' {
It 'throws on invalid TOML input' {
{ Format-Toml -InputObject 'invalid = = "bad"' } | Should -Throw
}

It 'throws when the file does not exist' {
{ Format-Toml -Path 'nonexistent-file.toml' } | Should -Throw
}
}
}

Describe 'Merge-Toml' {
BeforeAll {
$baseDoc = @'
Expand Down