Functions/GenXdev.Coding.PowerShell.Modules/New-ModuleMarkdownHelp.ps1
|
<############################################################################## Part of PowerShell module : GenXdev.Coding.PowerShell.Modules Original cmdlet filename : New-ModuleMarkdownHelp.ps1 Original author : René Vaessen / GenXdev Version : 3.28.2026 ################################################################################ Copyright (c) 2026 René Vaessen / GenXdev Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ################################################################################> ############################################################################### <# .SYNOPSIS Generates rich Markdown help files for any PowerShell module. .DESCRIPTION Generates a comprehensive Markdown help site for any PowerShell module: one .md file per cmdlet with all metadata sections, plus a README.md index with per-sub-module tables linking to each cmdlet file. Sub-module discovery is portable: .psm1 dot-source directories for script cmdlets, .NET namespaces for compiled cmdlets, with a layered fallback chain for modules without sub-module structure. .PARAMETER ModuleName The name of the PowerShell module to generate help for (e.g., 'Pester', 'PSReadLine', 'GenXdev'). .PARAMETER OutputPath Custom output directory for generated .md files. Defaults to <moduleRoot>\Docs\. The directory is created if it doesn't exist. .PARAMETER Language BCP 47 language tag for translation. Defaults to 'en-US' which skips translation. .PARAMETER Force Overwrite existing .md files without prompting. .PARAMETER SkipTranslation Skip LLM-based translation; keep help text in the source language. .PARAMETER Model The model identifier or pattern to use for AI translations. .PARAMETER ApiEndpoint The API endpoint URL for AI translations. .PARAMETER ApiKey The API key for authenticated AI translations. .PARAMETER LinkPrefix URL prefix for README index links. When specified, cmdlet links use absolute URLs (e.g., https://github.com/org/repo/Docs/CmdletName.md). When omitted, links use relative paths (./CmdletName.md). .PARAMETER TranslationInstructions Custom instructions for the AI translation model. .EXAMPLE New-ModuleMarkdownHelp -ModuleName 'Pester' -SkipTranslation Generates Docs\ folder with one .md per Pester cmdlet + README.md index. .EXAMPLE New-ModuleMarkdownHelp -ModuleName 'GenXdev' -Language 'nl-NL' -Force -Model 'deepseek-v4-pro' -ApiKey 'your-api-key' -ApiEndpoint 'https://api.deepseek.com/chat/completions' Generates Dutch-translated markdown help, overwriting existing files. .EXAMPLE New-ModuleMarkdownHelp -ModuleName 'GenXdev' -LinkPrefix ` 'https://github.com/genXdev/genXdev/Docs/' -SkipTranslation Generates help with absolute GitHub links in the README index. #> function New-ModuleMarkdownHelp { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [OutputType([System.String[]])] param ( ####################################################################### [Parameter( Position = 0, Mandatory = $true, HelpMessage = 'The name of the PowerShell module to generate ' + 'help for' )] [ValidateScript({ $m = Microsoft.PowerShell.Core\Get-Module -Name $_ ` -ErrorAction SilentlyContinue if (-not $m) { $m = Microsoft.PowerShell.Core\Get-Module -Name $_ ` -ListAvailable -ErrorAction SilentlyContinue } if (-not $m) { throw "Module '$_' not found. " + 'Provide a valid module name.' } $true })] [string] $ModuleName, ####################################################################### [Parameter( Mandatory = $false, HelpMessage = 'Custom output directory for .md files. ' + 'Defaults to <moduleRoot>\Docs\<language>.' )] [ValidateNotNullOrEmpty()] [string] $OutputPath, ####################################################################### [ValidateSet( 'ar-SA', 'bg-BG', 'bn-IN', 'ca-ES', 'cs-CZ', 'da-DK', 'de-DE', 'el-GR', 'en-US', 'es-ES', 'et-EE', 'fi-FI', 'fr-FR', 'gu-IN', 'he-IL', 'hi-IN', 'hr-HR', 'hu-HU', 'id-ID', 'it-IT', 'ja-JP', 'kn-IN', 'ko-KR', 'lt-LT', 'lv-LV', 'ml-IN', 'mr-IN', 'ms-MY', 'nb-NO', 'nl-NL', 'pa-IN', 'pl-PL', 'pt-BR', 'ro-RO', 'ru-RU', 'sk-SK', 'sl-SI', 'sr-RS', 'sv-SE', 'ta-IN', 'te-IN', 'th-TH', 'tr-TR', 'uk-UA', 'vi-VN', 'zh-Hans-CN' )] [Parameter( Mandatory = $false, HelpMessage = 'BCP 47 language tag for the generated help ' + '(e.g., en-US, nl-NL, de-DE)' )] [string] $Language = 'en-US', ####################################################################### [Parameter( Mandatory = $false, HelpMessage = 'Overwrite existing .md files without prompting' )] [switch] $Force, ####################################################################### [Parameter( Mandatory = $false, HelpMessage = 'Skip LLM translation; keep help in source ' + 'language' )] [switch] $SkipTranslation, ####################################################################### [Parameter( Mandatory = $false, HelpMessage = ('The model identifier or pattern to use for AI translations') )] [string] $Model, ########################################################################### [Parameter( Mandatory = $false, HelpMessage = 'The API endpoint URL for AI translations' )] [string] $ApiEndpoint, ########################################################################### [Parameter( Mandatory = $false, HelpMessage = 'The API key for authenticated AI translations' )] [string] $ApiKey, ################################################################### [Parameter( Mandatory = $false, HelpMessage = 'Indicates that LLM has no support for JSON schemas' )] [string] $NoSupportForJsonSchema, ########################################################################### [Parameter( Mandatory = $false, HelpMessage = 'URL prefix for README index links (e.g., ' + 'https://github.com/org/repo/Docs/)' )] [string] $LinkPrefix, ####################################################################### [Parameter( Mandatory = $false, HelpMessage = 'Custom AI translation instructions' )] [string] $TranslationInstructions ) begin { function BuildSyntaxLine { param( [string] $CmdletName, [object[]] $Parameters, [string] $SetName ) # Filter to parameters that belong to this set. $setParams = $Parameters | Microsoft.PowerShell.Core\Where-Object { if ($_.ParameterSetNames) { $_.ParameterSetNames -contains $SetName } elseif ($_.ParameterSetName) { $_.ParameterSetName -eq $SetName } else { $SetName -eq 'All' } } if (-not $setParams -or $setParams.Count -eq 0) { return "${CmdletName} [<CommonParameters>]" } # Sort: positional first (by numeric Position), then alpha. $sortedParams = $setParams | Microsoft.PowerShell.Utility\Sort-Object -Property @{ Expression = { $pos = $_.Position if ($pos -and $pos -ne 'Named') { try { return [int]$pos } catch { } } return [int]::MaxValue } }, 'Name' $line = $CmdletName foreach ($p in $sortedParams) { if ($p.DontShow) { continue } $shortType = if ($p.ParameterType) { ($p.ParameterType ` -replace '^System\.', '' ` -replace 'Management\.Automation\.', '' ` -replace 'Collections\.Generic\.', '' ` -replace '``1', '') } else { 'Object' } $isPositional = ($p.Position -and $p.Position -ne 'Named') $isMandatory = $p.Mandatory $isSwitch = ($shortType -eq 'SwitchParameter') if ($isSwitch) { $line += if ($isMandatory) { " -$($p.Name)" } else { " [-$($p.Name)]" } } elseif ($isMandatory) { $line += " -$($p.Name) <$shortType>" } elseif ($isPositional) { $line += " [[-$($p.Name)] <$shortType>]" } else { $line += " [-$($p.Name) <$shortType>]" } } $line += ' [<CommonParameters>]' return $line } function RenderCmdletMarkdown { param( [hashtable] $Meta, [string] $SubModuleName, [object[]] $SiblingCmdlets = @(), [string] $LinkPrefix = '' ) $cmdletName = if ($Meta['CmdletName']) { $Meta['CmdletName'] } else { ($Meta['Definition'] -split '\s+')[0] } $cmdletType = if ($Meta['CmdletType']) { $Meta['CmdletType'] } else { 'Function' } $aliases = if ($Meta['Aliases'] -and $Meta['Aliases'].Count -gt 0) { ($Meta['Aliases'] | Microsoft.PowerShell.Core\ForEach-Object { "``${_}``" }) -join ', ' } else { '—' } [System.Text.StringBuilder] $sb = [System.Text.StringBuilder]::new() # -- Header -------------------------------------------------------------- $null = $sb.AppendLine("# ${cmdletName}") $null = $sb.AppendLine() $null = $sb.AppendLine( "> **Module:** ${SubModuleName} | **Type:** ${cmdletType} | " + "**Aliases:** ${aliases}") $null = $sb.AppendLine() # -- Synopsis ------------------------------------------------------------ $synopsis = if (-not [string]::IsNullOrWhiteSpace($Meta['Synopsis'])) { $Meta['Synopsis'] } else { '*(No synopsis provided)*' } $null = $sb.AppendLine('## Synopsis') $null = $sb.AppendLine() $null = $sb.AppendLine("> ${synopsis}") $null = $sb.AppendLine() # -- Description --------------------------------------------------------- if (-not [string]::IsNullOrWhiteSpace($Meta['Description'])) { $null = $sb.AppendLine('## Description') $null = $sb.AppendLine() $desc = $Meta['Description'].Trim() # Detect structured/indented content (bulleted lists, nested # indentation) that would lose formatting in plain markdown. # When detected, wrap in a fenced code block to preserve # line breaks and indentation exactly as authored. $isStructured = $desc -match '(?m)^\s{2,}(\*|-)' if ($isStructured) { $null = $sb.AppendLine('```text') $null = $sb.AppendLine($desc) $null = $sb.AppendLine('```') } else { # Use <details> for long descriptions (> 3 paragraphs). $paragraphs = $desc -split '\r?\n\s*\r?\n' if ($paragraphs.Count -gt 3) { $null = $sb.AppendLine('<details>') $null = $sb.AppendLine( '<summary><b>Expand description</b></summary>') $null = $sb.AppendLine() foreach ($p in $paragraphs) { $null = $sb.AppendLine($p.Trim()) $null = $sb.AppendLine() } $null = $sb.AppendLine('</details>') } else { foreach ($p in $paragraphs) { $null = $sb.AppendLine($p.Trim()) $null = $sb.AppendLine() } } } } # -- Syntax -------------------------------------------------------------- $null = $sb.AppendLine('## Syntax') $null = $sb.AppendLine() $parameters = $Meta['Parameters'] # Build syntax from parameter metadata for all cmdlet types. if ($parameters -and $parameters.Count -gt 0) { # Collect all unique parameter set names across all parameters. $allSetNames = [System.Collections.Generic.List[string]]::new() foreach ($p in $parameters) { $sets = if ($p.ParameterSetNames) { $p.ParameterSetNames } elseif ($p.ParameterSetName) { @($p.ParameterSetName) } else { @('All') } foreach ($sn in $sets) { if ($sn -notin $allSetNames) { $null = $allSetNames.Add($sn) } } } $null = $sb.AppendLine('```powershell') if ($allSetNames.Count -le 1) { # Single set — render inline. $singleSetName = if ($allSetNames.Count -eq 1) { $allSetNames[0] } else { 'All' } $null = $sb.AppendLine( (BuildSyntaxLine $cmdletName $parameters $singleSetName)) } else { # Multiple sets — one line per set, first line is default. $isFirst = $true foreach ($setName in $allSetNames) { if (-not $isFirst) { $null = $sb.AppendLine() } $isFirst = $false $null = $sb.AppendLine( (BuildSyntaxLine $cmdletName $parameters $setName)) } } $null = $sb.AppendLine('```') } else { # No parameters — minimal syntax line. $null = $sb.AppendLine('```powershell') $null = $sb.AppendLine("${cmdletName} [<CommonParameters>]") $null = $sb.AppendLine('```') } $null = $sb.AppendLine() # -- Parameters ---------------------------------------------------------- $parameters = $Meta['Parameters'] if ($parameters -and $parameters.Count -gt 0) { $null = $sb.AppendLine('## Parameters') $null = $sb.AppendLine() # Column headers. $null = $sb.AppendLine( '| Name | Type | Required | Pipeline | Position | ' + 'Default | Description |') $null = $sb.AppendLine( '|:---|:---|:---:|:---|:---:|:---|:---|') foreach ($p in $parameters) { $pName = "``-$($p.Name)``" # Type — shorten common types. $pType = $p.ParameterType if ($pType) { $pType = $pType ` -replace '^System\.', '' ` -replace 'Management\.Automation\.', '' ` -replace 'Collections\.Generic\.', '' ` -replace '``1', '' } else { $pType = '—' } # Required. $pRequired = if ($p.Mandatory) { '✅' } else { '—' } # Pipeline. $pipelineParts = @() if ($p.ValueFromPipeline) { $pipelineParts += 'ByValue' } if ($p.ValueFromPipelineByPropertyName) { $pipelineParts += 'ByPropertyName' } $pPipeline = if ($pipelineParts.Count -gt 0) { '✅ (' + ($pipelineParts -join ', ') + ')' } else { '—' } # Remaining arguments. if ($p.ValueFromRemainingArguments) { $pPipeline += ' 🌐 Remaining' } # Position. $pPosition = if ($p.Position -and $p.Position -ne 'Named') { $p.Position } else { 'Named' } # Default value. $pDefault = if (-not [string]::IsNullOrWhiteSpace( $p.DefaultValue)) { "``$($p.DefaultValue)``" } else { '—' } # Description (prefer HelpMessage, fallback to parameter name). $pDesc = if (-not [string]::IsNullOrWhiteSpace( $p.HelpMessage)) { $p.HelpMessage } else { "The ${pName} parameter." } # Parameter set name. if ($p.ParameterSetName -and $p.ParameterSetName -ne 'All' -and $p.ParameterSetName -ne '__AllParameterSets') { $pDesc += " *(Parameter set: ${p.ParameterSetName})*" } # Wildcards. if ($p.SupportsWildcards) { $pDesc += ' 🌐 *Supports wildcards*' } # DontShow. if ($p.DontShow) { $pDesc += ' *(hidden)*' } $null = $sb.AppendLine( "| ${pName} | ${pType} | ${pRequired} | ${pPipeline} | " + "${pPosition} | ${pDefault} | ${pDesc} |") } $null = $sb.AppendLine() } # -- Examples ------------------------------------------------------------ $examples = $Meta['Examples'] if ($examples -and $examples.Count -gt 0) { $null = $sb.AppendLine('## Examples') $null = $sb.AppendLine() $exampleNum = 1 foreach ($ex in $examples) { $exText = $ex.ToString().Trim() if ([string]::IsNullOrWhiteSpace($exText)) { continue } # Try to extract a title from the first non-code line. $title = '' $hasFence = $exText -match '```' if ($hasFence) { # Everything before first ``` is the title/description. $fencePos = $exText.IndexOf('```') if ($fencePos -gt 0) { $title = $exText.Substring(0, $fencePos).Trim() } } else { # First non-blank line until first blank line. $lines = $exText -split '\r?\n' $titleLines = @() foreach ($line in $lines) { if ([string]::IsNullOrWhiteSpace($line)) { break } $titleLines += $line } $title = ($titleLines -join ' ').Trim() } if ([string]::IsNullOrWhiteSpace($title)) { $title = "Example ${exampleNum}" } $null = $sb.AppendLine("### ${title}") $null = $sb.AppendLine() # Render the example text with code fences. if ($hasFence) { $null = $sb.AppendLine($exText) } else { $blankMatch = [regex]::Match( $exText, '\r?\n\s*\r?\n') if ($blankMatch.Success) { $codePart = $exText.Substring( 0, $blankMatch.Index).TrimEnd() $descPart = $exText.Substring( $blankMatch.Index + $blankMatch.Length).Trim() $null = $sb.AppendLine('```powershell') $null = $sb.AppendLine($codePart) $null = $sb.AppendLine('```') $null = $sb.AppendLine() if ($descPart) { $null = $sb.AppendLine($descPart) } } else { $null = $sb.AppendLine('```powershell') $null = $sb.AppendLine($exText) $null = $sb.AppendLine('```') } } $null = $sb.AppendLine() $exampleNum++ } } # -- Parameter Details --------------------------------------------------- $parameters = $Meta['Parameters'] if ($parameters -and $parameters.Count -gt 0) { $null = $sb.AppendLine('## Parameter Details') $null = $sb.AppendLine() foreach ($p in $parameters) { if ($p.DontShow) { continue } # Normalize aliases: C# cmdlets may have string instead of # array. $pAliases = if ($p.Aliases) { if ($p.Aliases -is [string]) { @($p.Aliases) } else { @($p.Aliases) } } else { @() } $pAliases = @($pAliases | Microsoft.PowerShell.Core\Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) $shortType = if ($p.ParameterType) { ($p.ParameterType ` -replace '^System\.', '' ` -replace 'Management\.Automation\.', '' ` -replace 'Collections\.Generic\.', '' ` -replace '``1', '') } else { 'Object' } $isSwitch = ($shortType -eq 'SwitchParameter') if ($isSwitch) { $null = $sb.AppendLine("### ``-$($p.Name)``") } else { $null = $sb.AppendLine( "### ``-$($p.Name) <${shortType}>``") } $null = $sb.AppendLine() # Description (prefer HelpMessage, fallback to parameter # name). $pDesc = if (-not [string]::IsNullOrWhiteSpace( $p.HelpMessage)) { $p.HelpMessage } else { "The ``-$($p.Name)`` parameter." } $null = $sb.AppendLine("> ${pDesc}") $null = $sb.AppendLine() # Detail table. $null = $sb.AppendLine('| Property | Value |') $null = $sb.AppendLine('|:---|:---|') # Required? $reqText = if ($p.Mandatory) { 'Yes' } else { 'No' } $null = $sb.AppendLine( "| **Required?** | ${reqText} |") # Position? $posText = if ($p.Position ` -and $p.Position -ne 'Named') { $p.Position } else { 'Named' } $null = $sb.AppendLine( "| **Position?** | ${posText} |") # Default value. $defText = if ($null -ne $p.DefaultValue) { if ($p.DefaultValue -is ` [System.Collections.IDictionary] -and ` $p.DefaultValue.Contains('IsPresent')) { if ($p.DefaultValue['IsPresent']) { '`$true`' } else { '`$false`' } } elseif (-not [string]::IsNullOrWhiteSpace( [string]$p.DefaultValue)) { "``$($p.DefaultValue)``" } else { '*(none)*' } } else { '*(none)*' } $null = $sb.AppendLine( "| **Default value** | ${defText} |") # Pipeline input. $pipelineParts = @() if ($p.ValueFromPipeline) { $pipelineParts += 'ByValue' } if ($p.ValueFromPipelineByPropertyName) { $pipelineParts += 'ByPropertyName' } $pipelineText = if ($pipelineParts.Count -gt 0) { 'True (' + ($pipelineParts -join ', ') + ')' } else { 'False' } $null = $sb.AppendLine( "| **Accept pipeline input?** | ${pipelineText} |") # Aliases. $aliasText = if ($pAliases.Count -gt 0) { ($pAliases | Microsoft.PowerShell.Core\ForEach-Object { "``${_}``" }) -join ', ' } else { '*(none)*' } $null = $sb.AppendLine( "| **Aliases** | ${aliasText} |") # Wildcards. $wcText = if ($p.SupportsWildcards) { 'Yes' } else { 'No' } $null = $sb.AppendLine( "| **Accept wildcard characters?** | ${wcText} |") # Parameter set name (if not default). $setName = if ($p.ParameterSetNames) { $p.ParameterSetNames[0] } elseif ($p.ParameterSetName) { $p.ParameterSetName } else { 'All' } if ($setName -ne 'All' -and $setName -ne '__AllParameterSets') { $null = $sb.AppendLine( "| **Parameter set** | ${setName} |") } $null = $sb.AppendLine() # Add Remaining Arguments note if applicable. if ($p.ValueFromRemainingArguments) { $null = $sb.AppendLine( '> This parameter accepts all remaining ' + 'arguments.') $null = $sb.AppendLine() } $null = $sb.AppendLine("<hr/>") } } # -- Outputs ------------------------------------------------------------- $outputs = $Meta['OutputType'] if ($outputs -and $outputs.Count -gt 0) { $null = $sb.AppendLine('## Outputs') $null = $sb.AppendLine() foreach ($o in $outputs) { if (-not $o) { continue } $typeName = $o.ToString() ` -replace '^System\.', '' ` -replace 'Management\.Automation\.', '' ` -replace 'Collections\.Generic\.', '' $null = $sb.AppendLine("- ``${typeName}``") } $null = $sb.AppendLine() } # -- Related Links ------------------------------------------------------- $null = $sb.AppendLine('## Related Links') $null = $sb.AppendLine() if ($SiblingCmdlets.Count -gt 0) { # Build link URL prefix — same logic as README.md index. $linkPrefix = if ($LinkPrefix) { $LinkPrefix } else { './' } foreach ($s in $SiblingCmdlets) { $sName = $s['Name'] $null = $sb.AppendLine( "- [${sName}](${linkPrefix}${sName}.md)") } } $null = $sb.AppendLine() return $sb.ToString().TrimEnd() + "`r`n" } ############################################################################### # README index renderer ############################################################################### function RenderReadmeIndex { param( [string] $ModuleName, [System.Collections.IDictionary] $LinkMap, [string] $LinkPrefix ) [System.Text.StringBuilder] $sb = [System.Text.StringBuilder]::new() # -- Header -------------------------------------------------------------- $null = $sb.AppendLine("# ${ModuleName} — Cmdlet Reference") $null = $sb.AppendLine() $null = $sb.AppendLine( 'Auto-generated cmdlet reference documentation. ' + "Last updated: $(Microsoft.PowerShell.Utility\Get-Date -Format 'yyyy-MM-dd').") $null = $sb.AppendLine() # -- Per sub-module tables ----------------------------------------------- $sortedKeys = @($LinkMap.Keys | Microsoft.PowerShell.Utility\Sort-Object) $first = $true foreach ($subModule in $sortedKeys) { $cmdlets = $LinkMap[$subModule] if (-not $first) { $null = $sb.AppendLine('---') $null = $sb.AppendLine() } $first = $false $null = $sb.AppendLine("## ${subModule}") $null = $sb.AppendLine() $null = $sb.AppendLine('| Command | Aliases | Description |') $null = $sb.AppendLine('|:---|:---|:---|') foreach ($c in $cmdlets) { $name = $c['Name'] $synopsis = if ($c['Synopsis']) { $c['Synopsis'] } else { '' } $aliases = if ($c['Aliases'] -and $c['Aliases'].Count -gt 0) { ($c['Aliases'] -join ', ') } else { '' } # Build link URL. if ($LinkPrefix) { $url = "${LinkPrefix}${name}.md" } else { $url = "./${name}.md" } # Empty cells → strip line breaks to keep table intact $aliasCell = if ($aliases) { $aliases -replace '\r?\n', ' ' } else { ' ' } $synopsisCell = if ($synopsis) { $synopsis -replace '\r?\n', ' ' } else { ' ' } $null = $sb.AppendLine( "| [${name}](${url}) | ${aliasCell} | ${synopsisCell} |") } $null = $sb.AppendLine() } return $sb.ToString().TrimEnd() + "`r`n" } ################################################################################ # -- Resolve module -------------------------------------------------- # Prefer the actually-loaded module (has ExportedCommands). # Otherwise fall back to the available module with the highest # version number. $module = @(Microsoft.PowerShell.Core\Get-Module -Name $ModuleName ` -ErrorAction SilentlyContinue | Microsoft.PowerShell.Core\Where-Object { $_.ExportedCommands.Count -gt 0 })[0] if (-not $module) { $module = @(Microsoft.PowerShell.Core\Get-Module ` -Name $ModuleName -ListAvailable ` -ErrorAction SilentlyContinue | Microsoft.PowerShell.Utility\Sort-Object Version -Descending)[0] } if (-not $module) { Microsoft.PowerShell.Utility\Write-Error ` "Module '${ModuleName}' not found." return } $moduleRoot = $module.ModuleBase Microsoft.PowerShell.Utility\Write-Verbose ( "Module root: ${moduleRoot}") # -- Determine output path ------------------------------------------- if (-not $OutputPath) { $OutputPath = [System.IO.Path]::Combine($moduleRoot, 'Docs', $Language) } # Resolve and create output directory. $docsDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( $OutputPath) if (-not (Microsoft.PowerShell.Management\Test-Path $docsDir)) { $null = Microsoft.PowerShell.Management\New-Item ` -ItemType Directory -Path $docsDir -Force ` -ErrorAction Stop } Microsoft.PowerShell.Utility\Write-Verbose ( "Output directory: ${docsDir}") # -- Track generated files for pipeline output ----------------------- $generatedFiles = [System.Collections.Generic.List[string]]::new() # -- Cache module-level metadata params ------------------------------ $metaModuleParams = GenXdev\Copy-IdenticalParamValues ` -BoundParameters $PSBoundParameters ` -FunctionName "GenXdev\Get-ModuleCmdletMetaData" ; } process { # -- Collect metadata for all cmdlets -------------------------------- if (-not $PSCmdlet.ShouldProcess( $ModuleName, 'Retrieve cmdlet metadata for all cmdlets')) { return } try { $cmdletMetaData = @(GenXdev\Get-ModuleCmdletMetaData ` @metaModuleParams -ErrorAction Stop) } catch { Microsoft.PowerShell.Utility\Write-Error ( "Failed to get metadata for module '${ModuleName}': $_") return } if ($cmdletMetaData.Count -eq 0) { Microsoft.PowerShell.Utility\Write-Warning ( "No cmdlet metadata retrieved for '${ModuleName}'.") return } $cmdletMetaDataList = ` [System.Collections.Generic.List[hashtable]]::new() foreach ($meta in $cmdletMetaData) { $null = $cmdletMetaDataList.Add($meta) } Microsoft.PowerShell.Utility\Write-Verbose ( "Retrieved metadata for $($cmdletMetaDataList.Count) " + 'cmdlet(s).') # -- Phase 1: Generate per-cmdlet .md files -------------------------- $relativeLinkMap = @{} # Pass 1: collect all cmdlet metadata into the link map (no # rendering yet) so that each cmdlet can reference all its siblings. foreach ($meta in $cmdletMetaDataList) { $cmdletName = if ($meta['CmdletName']) { $meta['CmdletName'] } elseif ($meta['Definition']) { ($meta['Definition'] -split '\s+')[0] } else { '' } if ([string]::IsNullOrWhiteSpace($cmdletName)) { continue } # Store for README linking. $subModule = if ($meta.ContainsKey('SubModuleName')) { $meta['SubModuleName'] } else { $module.Name } if (-not $relativeLinkMap.ContainsKey($subModule)) { $relativeLinkMap[$subModule] = ` [System.Collections.Generic.List[hashtable]]::new() } $null = $relativeLinkMap[$subModule].Add(@{ Name = $cmdletName Synopsis = $meta['Synopsis'] Aliases = $meta['Aliases'] }) } # Pass 2: render each cmdlet, now that sibling lists are complete. foreach ($meta in $cmdletMetaDataList) { $cmdletName = if ($meta['CmdletName']) { $meta['CmdletName'] } elseif ($meta['Definition']) { ($meta['Definition'] -split '\s+')[0] } else { '' } if ([string]::IsNullOrWhiteSpace($cmdletName)) { continue } $mdFilePath = [System.IO.Path]::Combine( $docsDir, "${cmdletName}.md") $subModule = if ($meta.ContainsKey('SubModuleName')) { $meta['SubModuleName'] } else { $module.Name } # Build sibling cmdlets list (all cmdlets in same submodule # except the current one). $siblingCmdlets = if ($relativeLinkMap.ContainsKey($subModule)) { @($relativeLinkMap[$subModule] | Microsoft.PowerShell.Core\Where-Object { $_['Name'] -ne $cmdletName }) } else { @() } # Skip if file exists and -Force not specified. if (-not $Force -and [System.IO.File]::Exists($mdFilePath)) { Microsoft.PowerShell.Utility\Write-Verbose ( "Skipping existing file: ${mdFilePath} " + '(use -Force to overwrite)') $null = $generatedFiles.Add($mdFilePath) continue } if (-not $PSCmdlet.ShouldProcess( $cmdletName, "Generate ${cmdletName}.md")) { continue } $markdown = RenderCmdletMarkdown -Meta $meta ` -SubModuleName $subModule ` -SiblingCmdlets $siblingCmdlets ` -LinkPrefix $LinkPrefix [System.IO.File]::WriteAllText($mdFilePath, $markdown) $null = $generatedFiles.Add($mdFilePath) Microsoft.PowerShell.Utility\Write-Verbose ( "Wrote: ${cmdletName}.md") } # -- Phase 2: Generate README.md index ------------------------------- $readmePath = [System.IO.Path]::Combine($docsDir, 'README.md') if (-not $Force -and [System.IO.File]::Exists($readmePath)) { Microsoft.PowerShell.Utility\Write-Verbose ( 'Skipping existing README.md (use -Force to overwrite)') } elseif ($PSCmdlet.ShouldProcess( 'README.md', 'Generate module README index')) { $readmeMarkdown = RenderReadmeIndex ` -ModuleName $module.Name ` -LinkMap $relativeLinkMap ` -LinkPrefix $LinkPrefix [System.IO.File]::WriteAllText($readmePath, $readmeMarkdown) $null = $generatedFiles.Add($readmePath) Microsoft.PowerShell.Utility\Write-Verbose ( 'Wrote: README.md') } if ($generatedFiles.Count -gt 0) { Microsoft.PowerShell.Management\Get-ChildItem $generatedFiles } } end { } } |