Public/ConvertFrom-DhScript.ps1
|
function ConvertFrom-DhScript { <# .SYNOPSIS Best-effort: derive a YAML layout spec from a .ps1 that uses DashHtml cmdlets, by static parsing (no execution). .DESCRIPTION Parses a PowerShell script's abstract syntax tree, finds the New-DhDashboard / Add-Dh* / Set-DhTableLink calls, and emits the equivalent YAML layout. LITERAL arguments (strings, numbers, booleans, constant arrays / hashtables) are captured faithfully; anything dynamic — a `-Data $rows`, a value built in a loop, a computed expression — is emitted as a clearly-marked <expr: ...> placeholder, because static analysis cannot know what it evaluates to. For a lossless view of a dynamic script, RUN it and pipe the resulting report object to ConvertTo-DhYaml instead — that captures everything. This tool does NOT execute the script. .PARAMETER Path Path to the .ps1 script. .PARAMETER OutFile Optional path to write the YAML. Else returns the string. .EXAMPLE ConvertFrom-DhScript -Path .\Examples\AD-Dashboard.ps1 -OutFile ad-layout.yaml #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path, [string] $OutFile ) if (-not (Test-Path -LiteralPath $Path)) { throw "ConvertFrom-DhScript: file not found: $Path" } $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $Path), [ref]$null, [ref]$errors) if ($errors) { throw "ConvertFrom-DhScript: '$Path' has parse errors; cannot analyse." } $reverse = @{ 'add-dhhtmlblock'='html'; 'add-dhcollapsible'='collapsible'; 'add-dhfiltercard'='filtercardgrid'; 'add-dhbarchart'='barchart'; 'add-dhpiechart'='piechart'; 'add-dhbullet'='bullet'; 'add-dhlinechart'='linechart'; 'add-dheventfeed'='eventfeed'; 'add-dhstatusgrid'='statusgrid'; 'add-dhtabs'='tabs'; 'add-dhheatmap'='heatmap'; 'add-dhtopologymap'='topologymap' } $commands = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst] }, $true) $model = [ordered]@{} $tables = [System.Collections.Generic.List[object]]::new() $blocks = [System.Collections.Generic.List[object]]::new() $gfs = [System.Collections.Generic.List[object]]::new() $alerts = [System.Collections.Generic.List[object]]::new() $links = [System.Collections.Generic.List[object]]::new() foreach ($cmd in $commands) { $name = $cmd.GetCommandName() if (-not $name) { continue } $lname = $name.ToLower() if ($lname -ne 'new-dhdashboard' -and $lname -notlike 'add-dh*' -and $lname -ne 'set-dhtablelink') { continue } $params = Get-DhScriptParams $cmd switch ($lname) { 'new-dhdashboard' { foreach ($k in $params.Keys) { if ($k -ieq 'Report') { continue } $model[(ConvertTo-DhLcFirst $k)] = $params[$k] } } 'add-dhtable' { $t = [ordered]@{} foreach ($k in $params.Keys) { if ($k -ieq 'Report') { continue } if ($k -ieq 'TableId') { $t['id'] = $params[$k]; continue } if ($k -ieq 'Data') { $d = $params[$k] if ($d -is [System.Collections.IEnumerable] -and $d -isnot [string]) { $t['dataRows'] = @($d).Count } else { $t['dataSource'] = [string]$d } continue } $t[$k] = $params[$k] } [void]$tables.Add($t) } 'add-dhsummary' { $hasNav = $params.Keys | Where-Object { $_ -ieq 'NavGroup' } | ForEach-Object { $params[$_] } | Where-Object { $_ } if ($hasNav) { $blk = [ordered]@{ type = 'summary' } foreach ($k in $params.Keys) { if ($k -inotin @('Report')) { $blk[$k] = $params[$k] } } [void]$blocks.Add($blk) } else { $sum = [ordered]@{} foreach ($k in $params.Keys) { if ($k -inotin @('Report')) { $sum[(ConvertTo-DhLcFirst $k)] = $params[$k] } } $model['summary'] = $sum } } 'add-dhglobalfilter' { $gfs.Add((New-DhScriptItem $params)) | Out-Null } 'add-dhalertbanner' { $alerts.Add((New-DhScriptItem $params))| Out-Null } 'set-dhtablelink' { $links.Add((New-DhScriptItem $params)) | Out-Null } 'export-dhdashboard' { } default { $type = $reverse[$lname] if (-not $type) { break } $blk = [ordered]@{ type = $type } foreach ($k in $params.Keys) { if ($k -inotin @('Report')) { $blk[$k] = $params[$k] } } [void]$blocks.Add($blk) } } } if ($tables.Count) { $model['tables'] = $tables.ToArray() } if ($blocks.Count) { $model['blocks'] = $blocks.ToArray() } if ($gfs.Count) { $model['globalFilters'] = $gfs.ToArray() } if ($alerts.Count) { $model['alertBanners'] = $alerts.ToArray() } if ($links.Count) { $model['links'] = $links.ToArray() } $header = "# DashHtml layout — best-effort from $(Split-Path $Path -Leaf) (static parse).`n" + "# <expr: ...> markers are dynamic values a static parse can't resolve;`n" + "# run the script and pipe the report to ConvertTo-DhYaml for a lossless view.`n" $yaml = $header + (ConvertTo-DhYamlText $model) if ($OutFile) { Set-Content -Path $OutFile -Value $yaml -Encoding UTF8 Write-Verbose "ConvertFrom-DhScript: wrote $OutFile" } else { $yaml } } function ConvertTo-DhLcFirst { param([string] $s) if ([string]::IsNullOrEmpty($s)) { return $s } return $s.Substring(0,1).ToLower() + $s.Substring(1) } function New-DhScriptItem { param([hashtable] $Params) $item = [ordered]@{} foreach ($k in $Params.Keys) { if ($k -inotin @('Report')) { $item[(ConvertTo-DhLcFirst $k)] = $Params[$k] } } return $item } function Get-DhScriptParams { <# Extract a {ParamName -> value} map from a CommandAst. Literal values are resolved via SafeGetValue; non-literals become '<expr: ...>' strings. #> param([System.Management.Automation.Language.CommandAst] $Cmd) $out = @{} $els = $Cmd.CommandElements $i = 1 while ($i -lt $els.Count) { $el = $els[$i] if ($el -is [System.Management.Automation.Language.CommandParameterAst]) { $pname = $el.ParameterName if ($null -ne $el.Argument) { $out[$pname] = Resolve-DhAstValue $el.Argument $i++ } elseif (($i + 1) -lt $els.Count -and -not ($els[$i + 1] -is [System.Management.Automation.Language.CommandParameterAst])) { $out[$pname] = Resolve-DhAstValue $els[$i + 1] $i += 2 } else { $out[$pname] = $true; $i++ } # switch } else { $i++ } # positional argument — skipped (best-effort) } return $out } function Resolve-DhAstValue { param([System.Management.Automation.Language.Ast] $Ast) try { $v = $Ast.SafeGetValue() return $v } catch { return "<expr: $($Ast.Extent.Text)>" } } |