Private/Write-AACGovernanceMarkdown.ps1

function Write-AACGovernanceMarkdown {
    <#
    .SYNOPSIS
        Writes Invoke-AACGovernanceAssessment's results as one Markdown page
        for a wiki - GitHub, or an Azure DevOps wiki - with the management
        group hierarchy as a Mermaid diagram and a table per area.
    .DESCRIPTION
        -Section is ordered: name -> @{ Note; Rows; Columns (the property
        names shown, in order) }. Each table shows the first -MaxRows rows
        (the HTML report and the objects have them all). -Hierarchy is the
        AAC.ManagementGroup rows, drawn as a Mermaid graph: GitHub renders
        ```mermaid blocks, an Azure DevOps wiki ::: mermaid blocks
        (-Flavor). Pipes and line breaks in values are escaped so tables
        hold.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [Parameter(Mandatory)]
        [string] $Title,

        [System.Collections.IDictionary] $Fact = [ordered]@{},

        [string] $Headline = '',

        [object[]] $Tile = @(),

        [object[]] $Hierarchy = @(),

        [System.Collections.IDictionary] $Section = [ordered]@{},

        [ValidateSet('GitHub', 'AzureDevOps')]
        [string] $Flavor = 'GitHub',

        [ValidateRange(1, 5000)]
        [int] $MaxRows = 50
    )

    $cell = {
        param($Value)
        $text = if ($null -eq $Value) { '' } elseif ($Value -is [datetime]) { $Value.ToString('yyyy-MM-dd HH:mm') } elseif ($Value -is [bool]) { if ($Value) { 'Yes' } else { 'No' } } else { [string]$Value }
        ($text -replace '\r?\n', ' ' -replace '\|', '\|').Trim()
    }
    $lines = [System.Collections.Generic.List[string]]::new()
    $lines.Add("# $Title")
    $lines.Add('')
    $lines.Add("_Written by Azure Admin Console on $((Get-Date).ToString('d MMMM yyyy HH:mm'))$(if ($Fact.Count) { ' - ' + (@($Fact.Keys | ForEach-Object { "$($_): $($Fact[$_])" }) -join ' - ') })._")
    $lines.Add('')
    if ($Headline) { $lines.Add("> $(& $cell $Headline)"); $lines.Add('') }
    if ($Tile.Count) {
        $lines.Add('| Measure | Value |'); $lines.Add('|---|---:|')
        foreach ($t in $Tile) { $lines.Add("| $(& $cell $t.Label) | $(& $cell $t.Value) |") }
        $lines.Add('')
    }

    # The hierarchy, as a Mermaid graph.
    if ($Hierarchy.Count) {
        $lines.Add('## Management group hierarchy'); $lines.Add('')
        $idOf = @{}
        $n = 0
        foreach ($h in $Hierarchy) { $n++; $idOf[[string]$h.ManagementGroup] = "mg$n" }
        $graph = [System.Collections.Generic.List[string]]::new()
        $graph.Add('graph TD')
        foreach ($h in $Hierarchy) {
            $label = "$($h.ManagementGroup)<br/>$($h.SubscriptionsBelow) sub(s)$(if ($h.PolicyAssignments) { " - $($h.PolicyAssignments) polic(ies)" })$(if ($h.RoleAssignments) { " - $($h.RoleAssignments) role(s)" })" -replace '"', "'"
            $graph.Add(" $($idOf[[string]$h.ManagementGroup])[""$label""]")
            if ($h.Parent -and $idOf.Contains([string]$h.Parent)) { $graph.Add(" $($idOf[[string]$h.Parent]) --> $($idOf[[string]$h.ManagementGroup])") }
        }
        if ($Flavor -eq 'AzureDevOps') { $lines.Add('::: mermaid'); foreach ($g in $graph) { $lines.Add($g) }; $lines.Add(':::') }
        else { $lines.Add('```mermaid'); foreach ($g in $graph) { $lines.Add($g) }; $lines.Add('```') }
        $lines.Add('')
    }

    foreach ($name in $Section.Keys) {
        $s = $Section[$name]
        $rows = @($s.Rows | Where-Object { $null -ne $_ })
        $lines.Add("## $name ($($rows.Count))"); $lines.Add('')
        if ($s.Contains('Note') -and $s.Note) { $lines.Add([string]$s.Note); $lines.Add('') }
        if (-not $rows.Count) { $lines.Add('_Nothing to show._'); $lines.Add(''); continue }
        $columns = @($s.Columns | Where-Object { $rows[0].PSObject.Properties[$_] })
        $lines.Add('| ' + ($columns -join ' | ') + ' |')
        $lines.Add('|' + (@($columns | ForEach-Object { '---' }) -join '|') + '|')
        foreach ($r in @($rows | Select-Object -First $MaxRows)) { $lines.Add('| ' + (@($columns | ForEach-Object { & $cell $r.$_ }) -join ' | ') + ' |') }
        if ($rows.Count -gt $MaxRows) { $lines.Add(''); $lines.Add("_The first $MaxRows of $($rows.Count): the HTML report and the objects have them all._") }
        $lines.Add('')
    }

    $folder = Split-Path -Path $Path -Parent
    if ($folder -and -not (Test-Path -LiteralPath $folder)) { $null = New-Item -ItemType Directory -Path $folder -Force }
    [System.IO.File]::WriteAllText($Path, ($lines -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
}