Private/DhYamlReader.ps1
|
# --------------------------------------------------------------------------- # Minimal, self-contained YAML reader for the DashHtml layout schema. # Parses the block-style subset emitted by DhYamlWriter (plus flow {} / []), # producing nested [ordered] hashtables + arrays with typed scalars. # NOT a general YAML parser: no anchors, tags, block scalars, or multi-doc. # --------------------------------------------------------------------------- function Remove-DhYamlComment { <# Strip a trailing '# ...' comment that is outside quotes. #> param([string] $line) $inS = $false; $inD = $false for ($i = 0; $i -lt $line.Length; $i++) { $c = $line[$i] if ($c -eq "'" -and -not $inD) { $inS = -not $inS } elseif ($c -eq '"' -and -not $inS) { $inD = -not $inD } elseif ($c -eq '#' -and -not $inS -and -not $inD) { if ($i -eq 0 -or $line[$i - 1] -eq ' ' -or $line[$i - 1] -eq "`t") { return $line.Substring(0, $i) } } } return $line } function Get-DhYamlLines { param([string] $text) $out = [System.Collections.Generic.List[object]]::new() foreach ($raw in ($text -split "`r?`n")) { $line = Remove-DhYamlComment $raw if ($line.Trim().Length -eq 0) { continue } $indent = $line.Length - $line.TrimStart(' ').Length [void]$out.Add([pscustomobject]@{ Indent = $indent; Text = $line.Trim() }) } return , $out } function ConvertFrom-DhYamlScalarText { <# Coerce an unquoted / quoted scalar token to a typed value. #> param([string] $s) $s = $s.Trim() if ($s.Length -eq 0) { return $null } if ($s.StartsWith('"') -and $s.EndsWith('"') -and $s.Length -ge 2) { $inner = $s.Substring(1, $s.Length - 2) return ($inner -replace '\\n', "`n" -replace '\\r', "`r" -replace '\\t', "`t" -replace '\\"', '"' -replace '\\\\', '\') } if ($s.StartsWith("'") -and $s.EndsWith("'") -and $s.Length -ge 2) { return $s.Substring(1, $s.Length - 2) -replace "''", "'" } switch -Regex ($s) { '^(null|~)$' { return $null } '^true$' { return $true } '^false$' { return $false } '^-?\d+$' { $n = 0L; if ([long]::TryParse($s, [ref]$n)) { if ($n -ge [int]::MinValue -and $n -le [int]::MaxValue) { return [int]$n } return $n } } '^-?\d+\.\d+$' { $d = 0.0; if ([double]::TryParse($s, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$d)) { return $d } } } return $s } function ConvertFrom-DhYamlFlow { <# Parse a flow scalar / mapping / sequence starting at $pos.Value. #> param([string] $s, [ref] $pos) while ($pos.Value -lt $s.Length -and $s[$pos.Value] -eq ' ') { $pos.Value++ } if ($pos.Value -ge $s.Length) { return $null } $c = $s[$pos.Value] if ($c -eq '{') { $pos.Value++ $map = [ordered]@{} while ($true) { while ($pos.Value -lt $s.Length -and ($s[$pos.Value] -eq ' ' -or $s[$pos.Value] -eq ',')) { $pos.Value++ } if ($pos.Value -ge $s.Length -or $s[$pos.Value] -eq '}') { $pos.Value++; break } $key = ConvertFrom-DhYamlFlowToken $s $pos ':' while ($pos.Value -lt $s.Length -and ($s[$pos.Value] -eq ' ' -or $s[$pos.Value] -eq ':')) { $pos.Value++ } $val = ConvertFrom-DhYamlFlow $s $pos $map["$key"] = $val } return $map } elseif ($c -eq '[') { $pos.Value++ $seq = [System.Collections.Generic.List[object]]::new() while ($true) { while ($pos.Value -lt $s.Length -and ($s[$pos.Value] -eq ' ' -or $s[$pos.Value] -eq ',')) { $pos.Value++ } if ($pos.Value -ge $s.Length -or $s[$pos.Value] -eq ']') { $pos.Value++; break } [void]$seq.Add((ConvertFrom-DhYamlFlow $s $pos)) } return , $seq.ToArray() } else { $tok = ConvertFrom-DhYamlFlowToken $s $pos ',]}' return ConvertFrom-DhYamlScalarText $tok } } function ConvertFrom-DhYamlFlowToken { <# Read a bare-or-quoted token up to one of $stop chars (outside quotes). #> param([string] $s, [ref] $pos, [string] $stop) while ($pos.Value -lt $s.Length -and $s[$pos.Value] -eq ' ') { $pos.Value++ } $start = $pos.Value if ($pos.Value -lt $s.Length -and ($s[$pos.Value] -eq '"' -or $s[$pos.Value] -eq "'")) { $q = $s[$pos.Value]; $pos.Value++ while ($pos.Value -lt $s.Length -and $s[$pos.Value] -ne $q) { if ($s[$pos.Value] -eq '\' -and $q -eq '"') { $pos.Value++ } $pos.Value++ } $pos.Value++ # closing quote return $s.Substring($start, $pos.Value - $start) } while ($pos.Value -lt $s.Length -and $stop.IndexOf($s[$pos.Value]) -lt 0) { $pos.Value++ } return $s.Substring($start, $pos.Value - $start).Trim() } function ConvertFrom-DhYamlValue { <# A value after 'key:' — flow collection or plain scalar. #> param([string] $text) $t = $text.Trim() if ($t.StartsWith('{') -or $t.StartsWith('[')) { $pos = 0 return ConvertFrom-DhYamlFlow $t ([ref]$pos) } return ConvertFrom-DhYamlScalarText $t } function Split-DhYamlKeyValue { <# Split 'key: value' at the first top-level colon-space (or trailing colon). #> param([string] $line) $inS = $false; $inD = $false for ($i = 0; $i -lt $line.Length; $i++) { $c = $line[$i] if ($c -eq "'" -and -not $inD) { $inS = -not $inS } elseif ($c -eq '"' -and -not $inS) { $inD = -not $inD } elseif ($c -eq ':' -and -not $inS -and -not $inD) { if ($i -eq $line.Length - 1) { return @{ Key = $line.Substring(0, $i).Trim(); Value = '' } } if ($line[$i + 1] -eq ' ') { return @{ Key = $line.Substring(0, $i).Trim(); Value = $line.Substring($i + 1).Trim() } } } } return $null } function ConvertFrom-DhYamlBlock { <# Parse a block (map or seq) at $indent starting at $ix.Value. #> param([System.Collections.Generic.List[object]] $lines, [ref] $ix, [int] $indent) $first = $lines[$ix.Value] if ($first.Text.StartsWith('- ') -or $first.Text -eq '-') { return ConvertFrom-DhYamlSeq $lines $ix $indent } return ConvertFrom-DhYamlMap $lines $ix $indent } function ConvertFrom-DhYamlMap { param([System.Collections.Generic.List[object]] $lines, [ref] $ix, [int] $indent) $map = [ordered]@{} while ($ix.Value -lt $lines.Count) { $ln = $lines[$ix.Value] if ($ln.Indent -lt $indent) { break } if ($ln.Text.StartsWith('- ') -or $ln.Text -eq '-') { break } $kv = Split-DhYamlKeyValue $ln.Text if ($null -eq $kv) { throw "DhYaml: cannot parse line: '$($ln.Text)'" } if ($kv.Value -ne '') { $map[$kv.Key] = ConvertFrom-DhYamlValue $kv.Value $ix.Value++ } else { $ix.Value++ if ($ix.Value -lt $lines.Count -and $lines[$ix.Value].Indent -gt $indent) { $map[$kv.Key] = ConvertFrom-DhYamlBlock $lines $ix ($lines[$ix.Value].Indent) } else { $map[$kv.Key] = $null } } } return $map } function ConvertFrom-DhYamlSeq { param([System.Collections.Generic.List[object]] $lines, [ref] $ix, [int] $indent) $seq = [System.Collections.Generic.List[object]]::new() while ($ix.Value -lt $lines.Count) { $ln = $lines[$ix.Value] if ($ln.Indent -lt $indent) { break } if (-not ($ln.Text.StartsWith('- ') -or $ln.Text -eq '-')) { break } $item = if ($ln.Text -eq '-') { '' } else { $ln.Text.Substring(2).Trim() } if ($item -eq '') { $ix.Value++ if ($ix.Value -lt $lines.Count -and $lines[$ix.Value].Indent -gt $indent) { [void]$seq.Add((ConvertFrom-DhYamlBlock $lines $ix ($lines[$ix.Value].Indent))) } else { [void]$seq.Add($null) } } elseif ($item.StartsWith('{') -or $item.StartsWith('[')) { [void]$seq.Add((ConvertFrom-DhYamlValue $item)) $ix.Value++ } else { $kv = Split-DhYamlKeyValue $item if ($null -ne $kv) { # Inline first key of a map; the map's remaining keys sit at indent+2. $childIndent = $ln.Indent + 2 $map = [ordered]@{} if ($kv.Value -ne '') { $map[$kv.Key] = ConvertFrom-DhYamlValue $kv.Value $ix.Value++ } else { $ix.Value++ if ($ix.Value -lt $lines.Count -and $lines[$ix.Value].Indent -gt $childIndent) { $map[$kv.Key] = ConvertFrom-DhYamlBlock $lines $ix ($lines[$ix.Value].Indent) } else { $map[$kv.Key] = $null } } # Continue reading the rest of this map's keys at childIndent while ($ix.Value -lt $lines.Count -and $lines[$ix.Value].Indent -eq $childIndent -and -not ($lines[$ix.Value].Text.StartsWith('- ') -or $lines[$ix.Value].Text -eq '-')) { $sub = ConvertFrom-DhYamlMap $lines $ix $childIndent foreach ($k in $sub.Keys) { $map[$k] = $sub[$k] } } [void]$seq.Add($map) } else { [void]$seq.Add((ConvertFrom-DhYamlScalarText $item)) $ix.Value++ } } } return , $seq.ToArray() } function ConvertFrom-DhYamlText { <# .SYNOPSIS Parse the DashHtml YAML subset (or JSON) into nested hashtables. #> param([string] $Text) $trimmed = $Text.TrimStart() if ($trimmed.StartsWith('{') -or $trimmed.StartsWith('[')) { # JSON fast path — a strict subset of our needs, native + bulletproof. return ($Text | ConvertFrom-Json -Depth 40 -AsHashtable) } $lines = Get-DhYamlLines $Text if ($lines.Count -eq 0) { return [ordered]@{} } $ix = 0 return ConvertFrom-DhYamlBlock $lines ([ref]$ix) ($lines[0].Indent) } |