build/scripts/Build-Wiki.ps1
# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. <# .SYNOPSIS Builds the markdown documentation for the module. .DESCRIPTION Builds the markdown documentation for the module using the PlatyPS PowerShell module. .PARAMETER Path Specifies the output path for the function markdown files. .PARAMETER ModulePath Specifies the path of the module to generate the help for. .PARAMETER ModuleName Specifies the name of the already loaded module to generate the help for. .PARAMETER Description Specifies the description for the module. .PARAMETER RemoveDeprecated Removes any files that were previously generated but were not generated during this update. Those files likely represent functions that were either renamed, removed or that stopped being exported. .PARAMETER Force Indicates that this should overwrite existing files that have the same names. .INPUTS None .OUTPUTS None .EXAMPLE Build-Wiki -Path './' -ModuleName 'PowerShellForGitHub' -RemoveDeprecated #> [CmdletBinding(DefaultParameterSetName='ModuleName')] param ( [string] $Path = 'docs', [Parameter( Mandatory, ParameterSetName='ModulePath')] [string] $ModulePath, [Parameter( ParameterSetName='ModuleName')] [string] $ModuleName = 'PowerShellForGitHub', [string] $Description = 'PowerShellForGitHub is a PowerShell module that provides command-line interaction and automation for the [GitHub v3 API](https://developer.github.com/v3/).', [switch] $RemoveDeprecated, [switch] $Force ) function Out-Utf8File { <# .DESCRIPTION Writes a file using UTF8 (no BOM) encoding. .PARAMETER Path The path to the file to write to. .PARAMETER Content The string content for the file. .PARAMETER Force Indicates that this should overwrite an existing file that has the same name. .INPUTS String .EXAMPLE Out-Utf8File -Path ./foo.txt -Content 'bar' Creates a 'foo.txt' in the current working directory with the content 'bar' as a UTF-8 file without BOM. .NOTES This is being used because the PS5 Encoding options only include utf8 with BOM, and we want to write without BOM. This is fixed in PS6+, but we need to support PS4+. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path, [Parameter(ValueFromPipeline)] [string] $Content, [switch] $Force ) begin { if (Test-Path -Path $Path -PathType Leaf) { if ($Force.IsPresent) { Remove-Item -Path $Path -Force | Out-Null } else { throw "[$Path] already exists and -Force was not specified." } } $stream = New-Object -TypeName System.IO.StreamWriter -ArgumentList ($Path, [System.Text.Encoding]::UTF8) } process { $stream.WriteLine($Content) } end { $stream.Close(); } } function Build-SideBar { <# .DESCRIPTION Generate the sidebar content file. .PARAMETER Path The path where the file should be written to. .PARAMETER ModuleRootPageFileName The filename for the root of the module documentation. .PARAMETER ModuleName The name of the module the documentation is for. .PARAMETER ModulePages The names of the module pages that have been generated. .EXAMPLE Build-SideBar -Path ./docs -ModuleRootPageFileName 'root.md' -ModuleName 'PowerShellForGitHub' -ModulePages @('Foo', 'Bar') #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="It's an approved verb in PS Core, just not Windows PowerShell. Plus, this is an internal helper.")] [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [string] $ModuleRootPageFileName, [Parameter(Mandatory)] [string] $ModuleName, [string[]] $ModulePages ) $sideBarFilePath = Join-Path -Path $Path -ChildPath '_sidebar.md' $moduleRootPageBaseName = $ModuleRootPageFileName.Substring(0, $ModuleRootPageFileName.lastIndexOf('.')) $moduleContentStartMarker = '<!-- startDocs -->' $moduleContentEndMarker = '<!-- endDocs -->' $moduleContent = @() $moduleContent += $moduleContentStartMarker $moduleContent += '### Docs' $moduleContent += '' $moduleContent += "[$ModuleName]($moduleRootPageBaseName)" $moduleContent += '' $moduleContent += '#### Functions' $moduleContent += '' foreach ($modulePage in $modulePages) { $moduleContent += "- [$modulePage]($modulePage)" } $moduleContent += $moduleContentEndMarker $moduleContent += '' $content = '' $docsSideBarRegEx = "$moduleContentStartMarker[\r\n]+(?:[^<]+[\r\n]+)*$moduleContentEndMarker[\r\n]+" if (Test-Path -Path $sideBarFilePath -PathType Leaf) { $content = Get-Content -Path $sideBarFilePath -Raw -Encoding utf8 if ($content -match $docsSideBarRegEx) { $content = $content -replace $docsSideBarRegEx,($moduleContent -join [Environment]::NewLine) } else { $content += [Environment]::NewLine $content += ($moduleContent -join [Environment]::NewLine) } } else { $newContent = @() $newContent += "## $ModuleName" $newContent += '' $newContent += $moduleContent $content = $newContent -join [Environment]::NewLine } $content | Out-Utf8File -Path $sideBarFilePath -Force } function Build-Footer { <# .DESCRIPTION Generate the footer content file. .PARAMETER Path The path where the file should be written to. .PARAMETER ModuleRootPageFileName The filename for the root of the module documentation. .PARAMETER ModuleName The name of the module the documentation is for. .EXAMPLE Build-Footer -Path ./docs -ModuleRootPageFileName 'root.md' -ModuleName 'PowerShellForGitHub' #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="It's an approved verb in PS Core, just not Windows PowerShell. Plus, this is an internal helper.")] [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [string] $ModuleRootPageFileName, [Parameter(Mandatory)] [string] $ModuleName ) $footerFilePath = Join-Path -Path $Path -ChildPath '_footer.md' $moduleRootPageBaseName = $ModuleRootPageFileName.Substring(0, $ModuleRootPageFileName.lastIndexOf('.')) $moduleContentStartMarker = '<!-- startDocs -->' $moduleContentEndMarker = '<!-- endDocs -->' $moduleContent = @() $moduleContent += $moduleContentStartMarker $moduleContent += '' $moduleContent += "[Back to [$ModuleName]($moduleRootPageBaseName)]" $moduleContent += '' $moduleContent += $moduleContentEndMarker $moduleContent += '' $content = '' $docsFooterRegEx = "$moduleContentStartMarker[\r\n]+(?:[^<]+[\r\n]+)*$moduleContentEndMarker[\r\n]+" if (Test-Path -Path $footerFilePath -PathType Leaf) { $content = Get-Content -Path $footerFilePath -Raw -Encoding utf8 if ($content -match $docsFooterRegEx) { $content = $content -replace $docsFooterRegEx,($moduleContent -join [Environment]::NewLine) } else { $content += [Environment]::NewLine $content += ($moduleContent -join [Environment]::NewLine) } } else { $content = ($moduleContent -join [Environment]::NewLine) } $content | Out-Utf8File -Path $footerFilePath -Force } function Build-HomePage { <# .DESCRIPTION Generate the home page file for the Wiki. .PARAMETER Path The path where the file should be written to. .PARAMETER ModuleRootPageFileName The filename for the root of the module documentation. .EXAMPLE Build-HomePage -Path ./docs -ModuleRootPageFileName 'root.md' #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="It's an approved verb in PS Core, just not Windows PowerShell. Plus, this is an internal helper.")] [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [string] $ModuleRootPageFileName ) $homePageFilePath = Join-Path -Path $Path -ChildPath 'Home.md' $moduleRootPageBaseName = $ModuleRootPageFileName.Substring(0, $ModuleRootPageFileName.lastIndexOf('.')) $moduleContentStartMarker = '<!-- startDocs -->' $moduleContentEndMarker = '<!-- endDocs -->' $moduleContent = @() $moduleContent += $moduleContentStartMarker $moduleContent += '' $moduleContent += "[Full Module Documentation]($moduleRootPageBaseName)" $moduleContent += '' $moduleContent += $moduleContentEndMarker $moduleContent += '' $content = '' $docsFooterRegEx = "$moduleContentStartMarker[\r\n]+(?:[^<]+[\r\n]+)*$moduleContentEndMarker[\r\n]+" if (Test-Path -Path $homePageFilePath -PathType Leaf) { $content = Get-Content -Path $homePageFilePath -Raw -Encoding utf8 if ($content -match $docsFooterRegEx) { $content = $content -replace $docsFooterRegEx,($moduleContent -join [Environment]::NewLine) } else { $content += [Environment]::NewLine $content += ($moduleContent -join [Environment]::NewLine) } } else { $content = ($moduleContent -join [Environment]::NewLine) } $content | Out-Utf8File -Path $homePageFilePath -Force } $ErrorActionPreference = 'Stop' Set-StrictMode -Version 1.0 $Path = Resolve-Path -Path $Path if ($PSVersionTable.PSVersion.Major -lt 7) { Write-Warning 'It is recommended to run this with PowerShell 7+, as platyPS has a bug which doesn''t properly handle multi-line examples when run on older vesrions of PowerShell.' } $numSteps = 11 $currentStep = 0 $progressParams = @{ 'Activity' = 'Generating documentation for wiki' 'Id' = 1 } ####### $currentStep++ Write-Progress @progressParams -Status 'Ensuring PlatyPS installed' -PercentComplete (($currentStep / $numSteps) * 100) if ($null -eq (Get-Module -Name 'PlatyPS' -ListAvailable)) { Write-Verbose -Message 'Installing PlatyPS Module' Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force -Verbose:$false | Out-Null Install-Module PlatyPS -Scope CurrentUser -Force } ####### $currentStep++ Write-Progress @progressParams -Status 'Ensuring source module is loaded' -PercentComplete (($currentStep / $numSteps) * 100) if (-not [String]::IsNullOrEmpty($ModulePath)) { Write-Verbose -Message "Importing [$ModulePath]" $module = Import-Module -Name $ModulePath -PassThru -Force -Verbose:$false $ModuleName = $module.Name } $moduleRootPageFileName = "$ModuleName.md" # We generate the files to a _temp_ directory so that we can determine if there have been any # files that should be _removed_ from the Wiki due to rename/removal of exports. $tempFolder = Join-Path -Path $env:TEMP -ChildPath ([Guid]::NewGuid().Guid) New-Item -Path $tempFolder -ItemType Directory | Out-Null Write-Verbose -Message "Working from temp location: $tempFolder" ####### $currentStep++ Write-Progress @progressParams -Status 'Creating the new module markdown help files' -PercentComplete (($currentStep / $numSteps) * 100) # The ModulePage is generated to the current working directory, so we need to be temporarily located # at the temp folder. Push-Location -Path $tempFolder $params = @{ Module = $ModuleName OutputFolder = $tempFolder UseFullTypeName = $true AlphabeticParamsOrder = $true WithModulePage = $true ModulePagePath = $moduleRootPageFileName NoMetadata = $false # Otherwise was having issues with Update-MarkdownHelpModule FwLink = 'N/A' Encoding = ([System.Text.Encoding]::UTF8) Force = $true } New-MarkdownHelp @params | Out-Null ####### $currentStep++ Write-Progress @progressParams -Status 'Updating the generated documentation' -PercentComplete (($currentStep / $numSteps) * 100) $params = @{ Path = $tempFolder RefreshModulePage = $true ModulePagePath = $moduleRootPageFileName UseFullTypeName = $true AlphabeticParamsOrder = $true Encoding = ([System.Text.Encoding]::UTF8) } Update-MarkdownHelpModule @params | Out-Null # The ModulePage is generated to the current working directory. Now that we're done generating, # let's go back to our original location Pop-Location ####### $currentStep++ Write-Progress @progressParams -Status "Cleaning up content in $moduleRootPageFileName" -PercentComplete (($currentStep / $numSteps) * 100) $moduleRootPageFilePath = Join-Path -Path $tempFolder -ChildPath $moduleRootPageFileName $moduleRootPageContent = Get-Content -Path $moduleRootPageFilePath -Raw -Encoding utf8 $moduleRootPageContent = $moduleRootPageContent.Replace('.md)', ')') $descriptionMarker = '{{ Fill in the Description }}' $moduleRootPageContent = $moduleRootPageContent.Replace($descriptionMarker, $Description) $moduleRootPageContent | Out-Utf8File -Path $moduleRootPageFilePath -Force | Out-Null ####### $currentStep++ Write-Progress @progressParams -Status "Removing metadata from generated files" -PercentComplete (($currentStep / $numSteps) * 100) $modulePages = @() $generatedFiles = Get-ChildItem -Path $tempFolder -Filter '*.md' $metadataRegEx = '^---[\r\n]+(?:[^-].+[\r\n]+){1,10}---[\r\n]{1,4}' $generatedMarker = '<!-- Generated -->' + [Environment]::NewLine foreach ($file in $generatedFiles) { $fileContent = Get-Content -Path $file.FullName -Raw -Encoding utf8 if ($fileContent -match $metadataRegEx) { $fileContent = $fileContent -replace $metadataRegEx,$generatedMarker $fileContent | Out-Utf8File -Path $file.FullName -Force if ($file.Name -ne $moduleRootPageFileName) { $modulePages += $file.BaseName } } } ####### $currentStep++ Write-Progress @progressParams -Status "Updating sidebar" -PercentComplete (($currentStep / $numSteps) * 100) Build-SideBar -Path $Path -ModuleRootPageFileName $moduleRootPageFileName -ModuleName $ModuleName -ModulePages $modulePages ####### $currentStep++ Write-Progress @progressParams -Status "Updating footer" -PercentComplete (($currentStep / $numSteps) * 100) Build-Footer -Path $Path -ModuleRootPageFileName $moduleRootPageFileName -ModuleName $ModuleName ####### $currentStep++ Write-Progress @progressParams -Status "Updating home page" -PercentComplete (($currentStep / $numSteps) * 100) Build-HomePage -Path $Path -ModuleRootPageFileName $moduleRootPageFileName ####### $currentStep++ Write-Progress @progressParams -Status "Detecting deprecated pages" -PercentComplete (($currentStep / $numSteps) * 100) $deprecatedFiles = @() $currentFiles = Get-ChildItem -Path $Path -Filter '*.md' foreach ($file in $currentFiles) { $content = Get-Content -Path $file -Raw -Encoding utf8 if (($content -match $generatedMarker) -and ($file.BaseName -notin $modulePages) -and ($file.Name -notin ($moduleRootPageFileName, 'Home.md'))) { $deprecatedFiles += $file } } if ($deprecatedFiles.Length -gt 0) { if ($RemoveDeprecated.IsPresent) { Write-Verbose "The following files have been deprecated and will be removed:" } else { Write-Verbose "The following files have been deprecated. They can be removed automatically by specifying the -RemoveDeprecated switch." } foreach ($file in $deprecatedFiles) { Write-Verbose "* $($file.Name)" if ($RemoveDeprecated.IsPresent) { Remove-Item -Path $file.FullName -Force } } } ####### $currentStep++ Write-Progress @progressParams -Status "Moving generated content to final destination" -PercentComplete (($currentStep / $numSteps) * 100) $files = Get-ChildItem -Path $tempFolder foreach ($file in $files) { Move-Item -Path $file.FullName -Destination $Path -Force:$Force.IsPresent } Remove-Item -Path $tempFolder -Recurse -Force ####### Write-Progress @progressParams -Completed |