|
38 | 38 |
|
39 | 39 | return ConvertFrom-YamlMapping -Context $Context -Indent $Indent -Depth $Depth |
40 | 40 | } |
41 | | - |
42 | | -function ConvertFrom-YamlMapping { |
43 | | - <# |
44 | | - .SYNOPSIS |
45 | | - Parses a YAML block-style mapping into a PSCustomObject or OrderedDictionary. |
46 | | - #> |
47 | | - [CmdletBinding()] |
48 | | - [OutputType([System.Collections.Specialized.OrderedDictionary], [pscustomobject])] |
49 | | - param( |
50 | | - [Parameter(Mandatory)] [pscustomobject] $Context, |
51 | | - [Parameter(Mandatory)] [int] $Indent, |
52 | | - [Parameter(Mandatory)] [int] $Depth |
53 | | - ) |
54 | | - |
55 | | - $lines = $Context.Lines |
56 | | - $map = [ordered]@{} |
57 | | - |
58 | | - while ($Context.Index -lt $lines.Count) { |
59 | | - $line = $lines[$Context.Index] |
60 | | - if ($line.Indent -lt $Indent) { break } |
61 | | - if ($line.Indent -gt $Indent) { |
62 | | - throw "ConvertFrom-Yaml: unexpected indentation at line $($line.Number)." |
63 | | - } |
64 | | - if ($line.Content.StartsWith('- ') -or $line.Content -eq '-') { |
65 | | - # A sequence at the same indent as a mapping key is a sibling, not part of mapping. |
66 | | - break |
67 | | - } |
68 | | - |
69 | | - $colonIdx = Find-YamlMappingColon -Content $line.Content |
70 | | - if ($colonIdx -lt 0) { |
71 | | - throw "ConvertFrom-Yaml: expected mapping key at line $($line.Number): '$($line.Content)'." |
72 | | - } |
73 | | - |
74 | | - $key = ConvertFrom-YamlScalar -Raw $line.Content.Substring(0, $colonIdx).Trim() |
75 | | - $rest = $line.Content.Substring($colonIdx + 1).Trim() |
76 | | - |
77 | | - $Context.Index++ |
78 | | - |
79 | | - if ($rest.Length -gt 0) { |
80 | | - $map[[string]$key] = ConvertFrom-YamlScalar -Raw $rest |
81 | | - continue |
82 | | - } |
83 | | - |
84 | | - # Value on subsequent indented lines (mapping or sequence) or null. |
85 | | - if ($Context.Index -ge $lines.Count) { |
86 | | - $map[[string]$key] = $null |
87 | | - continue |
88 | | - } |
89 | | - |
90 | | - $next = $lines[$Context.Index] |
91 | | - if ($next.Indent -le $Indent) { |
92 | | - $map[[string]$key] = $null |
93 | | - continue |
94 | | - } |
95 | | - |
96 | | - # Sequences are allowed to start at the same indent as the parent key in YAML. |
97 | | - # We require the child to be indented strictly greater than the key here for clarity. |
98 | | - $childIndent = $next.Indent |
99 | | - $value = ConvertFrom-YamlNode -Context $Context -Indent $childIndent -Depth ($Depth + 1) |
100 | | - $map[[string]$key] = $value |
101 | | - } |
102 | | - |
103 | | - if ($Context.AsHashtable) { |
104 | | - return $map |
105 | | - } |
106 | | - |
107 | | - $obj = [pscustomobject]@{} |
108 | | - foreach ($k in $map.Keys) { |
109 | | - Add-Member -InputObject $obj -MemberType NoteProperty -Name $k -Value $map[$k] |
110 | | - } |
111 | | - return $obj |
112 | | -} |
113 | | - |
114 | | -function ConvertFrom-YamlSequence { |
115 | | - <# |
116 | | - .SYNOPSIS |
117 | | - Parses a YAML block-style sequence into a PowerShell array. |
118 | | - #> |
119 | | - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '', |
120 | | - Justification = 'Comma-unary operator preserves array type; PSScriptAnalyzer misdetects as Object[].')] |
121 | | - [CmdletBinding()] |
122 | | - [OutputType([object[]])] |
123 | | - param( |
124 | | - [Parameter(Mandatory)] [pscustomobject] $Context, |
125 | | - [Parameter(Mandatory)] [int] $Indent, |
126 | | - [Parameter(Mandatory)] [int] $Depth |
127 | | - ) |
128 | | - |
129 | | - $lines = $Context.Lines |
130 | | - $list = [System.Collections.Generic.List[object]]::new() |
131 | | - |
132 | | - while ($Context.Index -lt $lines.Count) { |
133 | | - $line = $lines[$Context.Index] |
134 | | - if ($line.Indent -lt $Indent) { break } |
135 | | - if ($line.Indent -gt $Indent) { |
136 | | - throw "ConvertFrom-Yaml: unexpected indentation at line $($line.Number)." |
137 | | - } |
138 | | - if (-not ($line.Content.StartsWith('- ') -or $line.Content -eq '-')) { |
139 | | - break |
140 | | - } |
141 | | - |
142 | | - $afterDash = if ($line.Content.Length -ge 2) { $line.Content.Substring(2).TrimEnd() } else { '' } |
143 | | - |
144 | | - if ($afterDash.Length -eq 0) { |
145 | | - # Value on subsequent indented lines. |
146 | | - $Context.Index++ |
147 | | - if ($Context.Index -ge $lines.Count) { |
148 | | - $list.Add($null) |
149 | | - continue |
150 | | - } |
151 | | - $next = $lines[$Context.Index] |
152 | | - if ($next.Indent -le $Indent) { |
153 | | - $list.Add($null) |
154 | | - continue |
155 | | - } |
156 | | - $list.Add((ConvertFrom-YamlNode -Context $Context -Indent $next.Indent -Depth ($Depth + 1))) |
157 | | - continue |
158 | | - } |
159 | | - |
160 | | - # Inline element: could be a scalar, or a mapping like "- key: value" with possibly more |
161 | | - # mapping keys on following lines indented at "Indent + 2" (under the dash). |
162 | | - $colonIdx = Find-YamlMappingColon -Content $afterDash |
163 | | - if ($colonIdx -ge 0) { |
164 | | - # Treat this as a single-line entry into a mapping. Build a synthetic line stream: |
165 | | - # the current "key: value" line gets re-interpreted at indent (Indent + 2), and any |
166 | | - # continuation lines at indent > (Indent + 2) belong to the same mapping. |
167 | | - $childIndent = $Indent + 2 |
168 | | - $synthetic = [pscustomobject]@{ |
169 | | - Indent = $childIndent |
170 | | - Content = $afterDash |
171 | | - Number = $line.Number |
172 | | - } |
173 | | - # Replace current line with synthetic and recurse as a mapping. |
174 | | - $Context.Lines[$Context.Index] = $synthetic |
175 | | - $value = ConvertFrom-YamlMapping -Context $Context -Indent $childIndent -Depth ($Depth + 1) |
176 | | - $list.Add($value) |
177 | | - continue |
178 | | - } |
179 | | - |
180 | | - # Plain scalar element. |
181 | | - $list.Add((ConvertFrom-YamlScalar -Raw $afterDash)) |
182 | | - $Context.Index++ |
183 | | - } |
184 | | - |
185 | | - return , $list.ToArray() |
186 | | -} |
187 | | - |
188 | | -function Find-YamlMappingColon { |
189 | | - <# |
190 | | - .SYNOPSIS |
191 | | - Returns the index of the unquoted `:` separator in a content line, or -1 if not found. |
192 | | -
|
193 | | - .DESCRIPTION |
194 | | - The colon must be followed by whitespace or end-of-line for it to be a YAML mapping |
195 | | - separator. Colons inside quoted strings are ignored. |
196 | | - #> |
197 | | - [CmdletBinding()] |
198 | | - [OutputType([int])] |
199 | | - param( |
200 | | - [Parameter(Mandatory)] |
201 | | - [AllowEmptyString()] |
202 | | - [string] $Content |
203 | | - ) |
204 | | - |
205 | | - $inSingle = $false |
206 | | - $inDouble = $false |
207 | | - for ($i = 0; $i -lt $Content.Length; $i++) { |
208 | | - $c = $Content[$i] |
209 | | - if ($c -eq '\' -and $inDouble) { $i++; continue } |
210 | | - if ($c -eq "'" -and -not $inDouble) { $inSingle = -not $inSingle; continue } |
211 | | - if ($c -eq '"' -and -not $inSingle) { $inDouble = -not $inDouble; continue } |
212 | | - if ($c -eq ':' -and -not $inSingle -and -not $inDouble) { |
213 | | - if ($i -eq $Content.Length - 1) { return $i } |
214 | | - $next = $Content[$i + 1] |
215 | | - if ($next -eq ' ' -or $next -eq "`t") { return $i } |
216 | | - } |
217 | | - } |
218 | | - return -1 |
219 | | -} |
220 | | - |
221 | | -function ConvertFrom-YamlScalar { |
222 | | - <# |
223 | | - .SYNOPSIS |
224 | | - Converts a raw YAML scalar token into the appropriate PowerShell type. |
225 | | - #> |
226 | | - [CmdletBinding()] |
227 | | - [OutputType([string], [bool], [int], [long], [double])] |
228 | | - param( |
229 | | - [Parameter(Mandatory)] |
230 | | - [AllowEmptyString()] |
231 | | - [string] $Raw |
232 | | - ) |
233 | | - |
234 | | - $value = $Raw.Trim() |
235 | | - |
236 | | - if ($value.Length -eq 0) { return $null } |
237 | | - |
238 | | - # Quoted strings. |
239 | | - if ($value.Length -ge 2 -and $value.StartsWith("'") -and $value.EndsWith("'")) { |
240 | | - $inner = $value.Substring(1, $value.Length - 2) |
241 | | - return ($inner -replace "''", "'") |
242 | | - } |
243 | | - if ($value.Length -ge 2 -and $value.StartsWith('"') -and $value.EndsWith('"')) { |
244 | | - $inner = $value.Substring(1, $value.Length - 2) |
245 | | - return (Expand-YamlDoubleQuoted -Text $inner) |
246 | | - } |
247 | | - |
248 | | - # Null literal (YAML 1.2.2 core schema): empty, ~, null only. Case-sensitive. |
249 | | - if ($value -ceq '~' -or $value -ceq 'null') { return $null } |
250 | | - |
251 | | - # Boolean literal (YAML 1.2.2 core schema): true / false only. Case-sensitive. |
252 | | - if ($value -ceq 'true') { return $true } |
253 | | - if ($value -ceq 'false') { return $false } |
254 | | - |
255 | | - # Integer. |
256 | | - $intVal = 0 |
257 | | - if ([int]::TryParse($value, [System.Globalization.NumberStyles]::Integer, [cultureinfo]::InvariantCulture, [ref] $intVal)) { |
258 | | - return $intVal |
259 | | - } |
260 | | - $longVal = [long]0 |
261 | | - if ([long]::TryParse($value, [System.Globalization.NumberStyles]::Integer, [cultureinfo]::InvariantCulture, [ref] $longVal)) { |
262 | | - return $longVal |
263 | | - } |
264 | | - |
265 | | - # Float. |
266 | | - $dblVal = 0.0 |
267 | | - if ([double]::TryParse($value, [System.Globalization.NumberStyles]::Float, [cultureinfo]::InvariantCulture, [ref] $dblVal)) { |
268 | | - return $dblVal |
269 | | - } |
270 | | - |
271 | | - # Plain string. |
272 | | - return $value |
273 | | -} |
274 | | - |
275 | | -function Expand-YamlDoubleQuoted { |
276 | | - <# |
277 | | - .SYNOPSIS |
278 | | - Expands escape sequences inside a double-quoted YAML scalar. |
279 | | - #> |
280 | | - [CmdletBinding()] |
281 | | - [OutputType([string])] |
282 | | - param( |
283 | | - [Parameter(Mandatory)] |
284 | | - [AllowEmptyString()] |
285 | | - [string] $Text |
286 | | - ) |
287 | | - |
288 | | - $sb = [System.Text.StringBuilder]::new() |
289 | | - $i = 0 |
290 | | - while ($i -lt $Text.Length) { |
291 | | - $c = $Text[$i] |
292 | | - if ($c -eq '\' -and $i + 1 -lt $Text.Length) { |
293 | | - $next = $Text[$i + 1] |
294 | | - $expanded = $true |
295 | | - switch ($next) { |
296 | | - 'n' { $null = $sb.Append("`n") } |
297 | | - 't' { $null = $sb.Append("`t") } |
298 | | - 'r' { $null = $sb.Append("`r") } |
299 | | - '"' { $null = $sb.Append('"') } |
300 | | - '\' { $null = $sb.Append('\') } |
301 | | - '0' { $null = $sb.Append([char]0) } |
302 | | - default { $expanded = $false } |
303 | | - } |
304 | | - |
305 | | - if ($expanded) { |
306 | | - $i += 2 |
307 | | - continue |
308 | | - } |
309 | | - } |
310 | | - $null = $sb.Append($c) |
311 | | - $i++ |
312 | | - } |
313 | | - return $sb.ToString() |
314 | | -} |
0 commit comments