joshooaj.platyPS.psm1
#region PlatyPS ## DEVELOPERS NOTES & CONVENTIONS ## ## 1. Non-exported functions (subroutines) should avoid using ## PowerShell standard Verb-Noun naming convention. ## They should use camalCase or PascalCase instead. ## 2. SMALL subroutines, used only from ONE function ## should be placed inside the parent function body. ## They should use camalCase for the name. ## 3. LARGE subroutines and subroutines used from MORE THEN ONE function ## should be placed after the IMPLEMENTATION text block in the middle ## of this module. ## They should use PascalCase for the name. ## 4. Add comment "# yeild" on subroutine calls that write values to pipeline. ## It would help keep code maintainable and simplify ramp up for others. ## Import-LocalizedData -BindingVariable LocalizedData -FileName joshooaj.platyPS.Resources.psd1 ## Script constants $script:EXTERNAL_HELP_FILE_YAML_HEADER = 'external help file' $script:ONLINE_VERSION_YAML_HEADER = 'online version' $script:SCHEMA_VERSION_YAML_HEADER = 'schema' $script:APPLICABLE_YAML_HEADER = 'applicable' $script:UTF8_NO_BOM = New-Object System.Text.UTF8Encoding -ArgumentList $False $script:SET_NAME_PLACEHOLDER = 'UNNAMED_PARAMETER_SET' # TODO: this is just a place-holder, we can do better $script:DEFAULT_MAML_XML_OUTPUT_NAME = 'rename-me-help.xml' $script:MODULE_PAGE_MODULE_NAME = "Module Name" $script:MODULE_PAGE_GUID = "Module Guid" $script:MODULE_PAGE_LOCALE = "Locale" $script:MODULE_PAGE_FW_LINK = "Download Help Link" $script:MODULE_PAGE_HELP_VERSION = "Help Version" $script:MODULE_PAGE_ADDITIONAL_LOCALE = "Additional Locale" $script:MAML_ONLINE_LINK_DEFAULT_MONIKER = 'Online Version:' function New-MarkdownHelp { [CmdletBinding()] [OutputType([System.IO.FileInfo[]])] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName="FromModule")] [string[]]$Module, [Parameter(Mandatory=$true, ParameterSetName="FromCommand")] [string[]]$Command, [Parameter(Mandatory=$true, ParameterSetName="FromMaml")] [string[]]$MamlFile, [Parameter(ParameterSetName="FromModule")] [Parameter(ParameterSetName="FromCommand")] [System.Management.Automation.Runspaces.PSSession]$Session, [Parameter(ParameterSetName="FromMaml")] [switch]$ConvertNotesToList, [Parameter(ParameterSetName="FromMaml")] [switch]$ConvertDoubleDashLists, [switch]$Force, [switch]$AlphabeticParamsOrder, [hashtable]$Metadata, [Parameter(ParameterSetName="FromCommand")] [string]$OnlineVersionUrl = '', [Parameter(Mandatory=$true)] [string]$OutputFolder, [switch]$NoMetadata, [switch]$UseFullTypeName, [System.Text.Encoding]$Encoding = $script:UTF8_NO_BOM, [Parameter(ParameterSetName="FromModule")] [Parameter(ParameterSetName="FromMaml")] [switch]$WithModulePage, [Parameter(ParameterSetName="FromModule")] [Parameter(ParameterSetName="FromMaml")] [string]$ModulePagePath, [Parameter(ParameterSetName="FromModule")] [Parameter(ParameterSetName="FromMaml")] [string] $Locale = "en-US", [Parameter(ParameterSetName="FromModule")] [Parameter(ParameterSetName="FromMaml")] [string] $HelpVersion = $LocalizedData.HelpVersion, [Parameter(ParameterSetName="FromModule")] [Parameter(ParameterSetName="FromMaml")] [string] $FwLink = $LocalizedData.FwLink, [Parameter(ParameterSetName="FromMaml")] [string] $ModuleName = "MamlModule", [Parameter(ParameterSetName="FromMaml")] [string] $ModuleGuid = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", [switch] $ExcludeDontShow ) begin { validateWorkingProvider New-Item -Type Directory $OutputFolder -ErrorAction SilentlyContinue > $null } process { function updateMamlObject { param( [Parameter(Mandatory=$true)] [Markdown.MAML.Model.MAML.MamlCommand]$MamlCommandObject ) # # Here we define our misc template for new markdown to bootstrape easier # # Example if ($MamlCommandObject.Examples.Count -eq 0) { $MamlExampleObject = New-Object -TypeName Markdown.MAML.Model.MAML.MamlExample $MamlExampleObject.Title = $LocalizedData.ExampleTitle $MamlExampleObject.Code = @( New-Object -TypeName Markdown.MAML.Model.MAML.MamlCodeBlock ($LocalizedData.ExampleCode, 'powershell') ) $MamlExampleObject.Remarks = $LocalizedData.ExampleRemark $MamlCommandObject.Examples.Add($MamlExampleObject) } if ($AlphabeticParamsOrder) { SortParamsAlphabetically $MamlCommandObject } } function processMamlObjectToFile { param( [Parameter(ValueFromPipeline=$true)] [ValidateNotNullOrEmpty()] [Markdown.MAML.Model.MAML.MamlCommand]$mamlObject ) process { # populate template updateMamlObject $mamlObject if (-not $OnlineVersionUrl) { # if it's not passed, we should get it from the existing help $onlineLink = $mamlObject.Links | Select-Object -First 1 if ($onlineLink) { $online = $onlineLink.LinkUri if ($onlineLink.LinkName -eq $script:MAML_ONLINE_LINK_DEFAULT_MONIKER -or $onlineLink.LinkName -eq $onlineLink.LinkUri) { # if links follow standart MS convention or doesn't have name, # remove it to avoid duplications $mamlObject.Links.Remove($onlineLink) > $null } } } else { $online = $OnlineVersionUrl } $commandName = $mamlObject.Name # create markdown if ($NoMetadata) { $newMetadata = $null } else { # get help file name if ($MamlFile) { $helpFileName = Split-Path -Leaf $MamlFile } else { # Get-Command requires that script input be a path if ($mamlObject.Name.EndsWith(".ps1")) { $getCommandName = Resolve-Path $Command } # For cmdlets, nothing needs to be done else { $getCommandName = $commandName } $a = @{ Name = $getCommandName } if ($module) { # for module case, scope it just to this module $a['Module'] = $module } $helpFileName = GetHelpFileName (Get-Command @a) } Write-Verbose "Maml things module is: $($mamlObject.ModuleName)" $newMetadata = ($Metadata + @{ $script:EXTERNAL_HELP_FILE_YAML_HEADER = $helpFileName $script:ONLINE_VERSION_YAML_HEADER = $online $script:MODULE_PAGE_MODULE_NAME = $mamlObject.ModuleName }) } $md = ConvertMamlModelToMarkdown -mamlCommand $mamlObject -metadata $newMetadata -NoMetadata:$NoMetadata MySetContent -path (Join-Path $OutputFolder "$commandName.md") -value $md -Encoding $Encoding -Force:$Force } } if ($NoMetadata -and $Metadata) { throw $LocalizedData.NoMetadataAndMetadata } if ($PSCmdlet.ParameterSetName -eq 'FromCommand') { $command | ForEach-Object { if (-not (Get-Command $_ -ErrorAction SilentlyContinue)) { throw $LocalizedData.CommandNotFound -f $_ } GetMamlObject -Session $Session -Cmdlet $_ -UseFullTypeName:$UseFullTypeName -ExcludeDontShow:$ExcludeDontShow.IsPresent | processMamlObjectToFile } } else { if ($module) { $iterator = $module } else { $iterator = $MamlFile } $iterator | ForEach-Object { if ($PSCmdlet.ParameterSetName -eq 'FromModule') { if (-not (GetCommands -AsNames -module $_)) { throw $LocalizedData.ModuleNotFound -f $_ } GetMamlObject -Session $Session -Module $_ -UseFullTypeName:$UseFullTypeName -ExcludeDontShow:$ExcludeDontShow.IsPresent | processMamlObjectToFile $ModuleName = $_ $ModuleGuid = (Get-Module $ModuleName).Guid $CmdletNames = GetCommands -AsNames -Module $ModuleName } else # 'FromMaml' { if (-not (Test-Path $_)) { throw $LocalizedData.FileNotFound -f $_ } GetMamlObject -MamlFile $_ -ConvertNotesToList:$ConvertNotesToList -ConvertDoubleDashLists:$ConvertDoubleDashLists -ExcludeDontShow:$ExcludeDontShow.IsPresent | processMamlObjectToFile $CmdletNames += GetMamlObject -MamlFile $_ -ExcludeDontShow:$ExcludeDontShow.IsPresent | ForEach-Object {$_.Name} } if($WithModulePage) { if(-not $ModuleGuid) { $ModuleGuid = "00000000-0000-0000-0000-000000000000" } if($ModuleGuid.Count -gt 1) { Write-Warning -Message $LocalizedData.MoreThanOneGuid } # yeild NewModuleLandingPage -Path $OutputFolder ` -ModulePagePath $ModulePagePath ` -ModuleName $ModuleName ` -ModuleGuid $ModuleGuid ` -CmdletNames $CmdletNames ` -Locale $Locale ` -Version $HelpVersion ` -FwLink $FwLink ` -Encoding $Encoding ` -Force:$Force } } } } } function Get-MarkdownMetadata { [CmdletBinding(DefaultParameterSetName="FromPath")] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=1, ParameterSetName="FromPath")] [SupportsWildcards()] [string[]]$Path, [Parameter(Mandatory=$true, ParameterSetName="FromMarkdownString")] [string]$Markdown ) process { if ($PSCmdlet.ParameterSetName -eq 'FromMarkdownString') { return [Markdown.MAML.Parser.MarkdownParser]::GetYamlMetadata($Markdown) } else # FromFile) { GetMarkdownFilesFromPath $Path -IncludeModulePage | ForEach-Object { $md = Get-Content -Raw $_.FullName [Markdown.MAML.Parser.MarkdownParser]::GetYamlMetadata($md) # yeild } } } } function Update-MarkdownHelp { [CmdletBinding()] [OutputType([System.IO.FileInfo[]])] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [SupportsWildcards()] [string[]]$Path, [System.Text.Encoding]$Encoding = $script:UTF8_NO_BOM, [string]$LogPath, [switch]$LogAppend, [switch]$AlphabeticParamsOrder, [switch]$UseFullTypeName, [switch]$UpdateInputOutput, [Switch]$Force, [System.Management.Automation.Runspaces.PSSession]$Session, [switch]$ExcludeDontShow ) begin { validateWorkingProvider $infoCallback = GetInfoCallback $LogPath -Append:$LogAppend $MarkdownFiles = @() } process { $MarkdownFiles += GetMarkdownFilesFromPath $Path } end { function log { param( [string]$message, [switch]$warning ) $message = "[Update-MarkdownHelp] $([datetime]::now) $message" if ($warning) { Write-Warning $message } $infoCallback.Invoke($message) } if (-not $MarkdownFiles) { log -warning ($LocalizedData.NoMarkdownFiles -f $Path) return } $MarkdownFiles | ForEach-Object { $file = $_ $filePath = $file.FullName $oldModels = GetMamlModelImpl $filePath -ForAnotherMarkdown -Encoding $Encoding if ($oldModels.Count -gt 1) { log -warning ($LocalizedData.FileContainsMoreThanOneCommand -f $filePath) log -warning $LocalizedData.OneCommandPerFile return } $oldModel = $oldModels[0] $name = $oldModel.Name [Array]$loadedModulesBefore = $(Get-Module | Select-Object -Property Name) $command = Get-Command $name -ErrorAction SilentlyContinue if (-not $command) { if ($Force) { if (Test-Path $filePath) { Remove-Item -Path $filePath -Confirm:$false log -warning ($LocalizedData.CommandNotFoundFileRemoved -f $name, $filePath) return } } else { log -warning ($LocalizedData.CommandNotFoundSkippingFile -f $name, $filePath) return } } elseif (($null -ne $command.ModuleName) -and ($loadedModulesBefore.Name -notcontains $command.ModuleName)) { log -warning ($LocalizedData.ModuleImporteAutomaticaly -f $($command.ModuleName)) } # update the help file entry in the metadata $metadata = Get-MarkdownMetadata $filePath $metadata["external help file"] = GetHelpFileName $command $reflectionModel = GetMamlObject -Session $Session -Cmdlet $name -UseFullTypeName:$UseFullTypeName -ExcludeDontShow:$ExcludeDontShow.IsPresent $metadata[$script:MODULE_PAGE_MODULE_NAME] = $reflectionModel.ModuleName $merger = New-Object Markdown.MAML.Transformer.MamlModelMerger -ArgumentList $infoCallback $newModel = $merger.Merge($reflectionModel, $oldModel, $UpdateInputOutput) if ($AlphabeticParamsOrder) { SortParamsAlphabetically $newModel } $md = ConvertMamlModelToMarkdown -mamlCommand $newModel -metadata $metadata -PreserveFormatting MySetContent -path $file.FullName -value $md -Encoding $Encoding -Force # yield } } } function Merge-MarkdownHelp { [CmdletBinding()] [OutputType([System.IO.FileInfo[]])] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [SupportsWildcards()] [string[]]$Path, [Parameter(Mandatory=$true)] [string]$OutputPath, [System.Text.Encoding]$Encoding = $script:UTF8_NO_BOM, [Switch]$ExplicitApplicableIfAll, [Switch]$Force, [string]$MergeMarker = "!!! " ) begin { validateWorkingProvider $MarkdownFiles = @() } process { $MarkdownFiles += GetMarkdownFilesFromPath $Path } end { function log { param( [string]$message, [switch]$warning ) $message = "[Update-MarkdownHelp] $([datetime]::now) $message" if ($warning) { Write-Warning $message } else { Write-Verbose $message } } if (-not $MarkdownFiles) { log -warning ($LocalizedData.NoMarkdownFiles -f $Path) return } function getTags { param($files) ($files | Split-Path | Split-Path -Leaf | Group-Object).Name } # use parent folder names as tags $allTags = getTags $MarkdownFiles log "Using following tags for the merge: $tags" $fileGroups = $MarkdownFiles | Group-Object -Property Name log "Found $($fileGroups.Count) file groups" $fileGroups | ForEach-Object { $files = $_.Group $groupName = $_.Name $dict = New-Object 'System.Collections.Generic.Dictionary[string, Markdown.MAML.Model.MAML.MamlCommand]' $files | ForEach-Object { $model = GetMamlModelImpl $_.FullName -ForAnotherMarkdown -Encoding $Encoding # unwrap List of 1 element $model = $model[0] $tag = getTags $_ log "Adding tag $tag and $model" $dict[$tag] = $model } $tags = $dict.Keys if (($allTags | measure-object).Count -gt ($tags | measure-object).Count -or $ExplicitApplicableIfAll) { $newMetadata = @{ $script:APPLICABLE_YAML_HEADER = $tags -join ', ' } } else { $newMetadata = @{} } $merger = New-Object Markdown.MAML.Transformer.MamlMultiModelMerger -ArgumentList $null, (-not $ExplicitApplicableIfAll), $MergeMarker $newModel = $merger.Merge($dict) $md = ConvertMamlModelToMarkdown -mamlCommand $newModel -metadata $newMetadata -PreserveFormatting $outputFilePath = Join-Path $OutputPath $groupName MySetContent -path $outputFilePath -value $md -Encoding $Encoding -Force:$Force # yeild } } } function Update-MarkdownHelpModule { [CmdletBinding()] [OutputType([System.IO.FileInfo[]])] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [SupportsWildcards()] [string[]]$Path, [System.Text.Encoding]$Encoding = $script:UTF8_NO_BOM, [switch]$RefreshModulePage, [string]$ModulePagePath, [string]$LogPath, [switch]$LogAppend, [switch]$AlphabeticParamsOrder, [switch]$UseFullTypeName, [switch]$UpdateInputOutput, [switch]$Force, [System.Management.Automation.Runspaces.PSSession]$Session, [switch]$ExcludeDontShow ) begin { validateWorkingProvider $infoCallback = GetInfoCallback $LogPath -Append:$LogAppend $MarkdownFiles = @() } process { } end { function log { param( [string]$message, [switch]$warning ) $message = "[Update-MarkdownHelpModule] $([datetime]::now) $message" if ($warning) { Write-Warning $message } $infoCallback.Invoke($message) } foreach ($modulePath in $Path) { $module = $null $h = Get-MarkdownMetadata -Path $modulePath # this is pretty hacky and would lead to errors # the idea is to find module name from landing page when it's available if ($h.$script:MODULE_PAGE_MODULE_NAME) { $module = $h.$script:MODULE_PAGE_MODULE_NAME | Select-Object -First 1 log ($LocalizedData.ModuleNameFromPath -f $modulePath, $module) } if (-not $module) { Write-Error -Message ($LocalizedData.ModuleNameNotFoundFromPath -f $modulePath) continue } # always append on this call log ("[Update-MarkdownHelpModule]" + (Get-Date).ToString()) log ($LocalizedData.UpdateDocsForModule -f $module, $modulePath) $affectedFiles = Update-MarkdownHelp -Session $Session -Path $modulePath -LogPath $LogPath -LogAppend -Encoding $Encoding -AlphabeticParamsOrder:$AlphabeticParamsOrder -UseFullTypeName:$UseFullTypeName -UpdateInputOutput:$UpdateInputOutput -Force:$Force -ExcludeDontShow:$ExcludeDontShow $affectedFiles # yeild $allCommands = GetCommands -AsNames -Module $Module if (-not $allCommands) { throw $LocalizedData.ModuleOrCommandNotFound -f $Module } $updatedCommands = $affectedFiles.BaseName $allCommands | ForEach-Object { if ( -not ($updatedCommands -contains $_) ) { log ($LocalizedData.CreatingNewMarkdownForCommand -f $_) $newFiles = New-MarkdownHelp -Command $_ -OutputFolder $modulePath -AlphabeticParamsOrder:$AlphabeticParamsOrder -Force:$Force -ExcludeDontShow:$ExcludeDontShow $newFiles # yeild } } if($RefreshModulePage) { $MamlModel = New-Object System.Collections.Generic.List[Markdown.MAML.Model.MAML.MamlCommand] $files = @() $MamlModel = GetMamlModelImpl $affectedFiles -ForAnotherMarkdown -Encoding $Encoding NewModuleLandingPage -RefreshModulePage -ModulePagePath $ModulePagePath -Path $modulePath -ModuleName $module -Module $MamlModel -Encoding $Encoding -Force } } } } function New-MarkdownAboutHelp { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] $OutputFolder, [string] $AboutName ) begin { if ($AboutName.StartsWith('about_')) { $AboutName = $AboutName.Substring('about_'.Length)} validateWorkingProvider $templatePath = Join-Path $PSScriptRoot "templates\aboutTemplate.md" } process { if(Test-Path $OutputFolder) { $AboutContent = Get-Content $templatePath $AboutContent = $AboutContent.Replace("{{FileNameForHelpSystem}}",("about_" + $AboutName)) $AboutContent = $AboutContent.Replace("{{TOPIC NAME}}",$AboutName) $NewAboutTopic = New-Item -Path $OutputFolder -Name "about_$($AboutName).md" Set-Content -Value $AboutContent -Path $NewAboutTopic -Encoding UTF8 } else { throw $LocalizedData.OutputFolderNotFound } } } function New-YamlHelp { [CmdletBinding()] [OutputType([System.IO.FileInfo[]])] param( [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [string[]]$Path, [Parameter(Mandatory=$true, Position = 1)] [string]$OutputFolder, [System.Text.Encoding]$Encoding = [System.Text.Encoding]::UTF8, [switch]$Force ) begin { validateWorkingProvider $MarkdownFiles = @() if(-not (Test-Path $OutputFolder)) { New-Item -Type Directory $OutputFolder -ErrorAction SilentlyContinue > $null } if(-not (Test-Path -PathType Container $OutputFolder)) { throw $LocalizedData.PathIsNotFolder -f $OutputFolder } } process { $MarkdownFiles += GetMarkdownFilesFromPath $Path } end { $MarkdownFiles | ForEach-Object { Write-Verbose -Message ($LocalizedData.InputMarkdownFile -f '[New-YamlHelp]', $_) } foreach($markdownFile in $MarkdownFiles) { $mamlModels = GetMamlModelImpl $markdownFile.FullName -Encoding $Encoding foreach($mamlModel in $mamlModels) { $markdownMetadata = Get-MarkdownMetadata -Path $MarkdownFile.FullName ## We set the module here in the PowerShell since the Yaml block is not read by the parser $mamlModel.ModuleName = $markdownMetadata[$script:MODULE_PAGE_MODULE_NAME] $yaml = [Markdown.MAML.Renderer.YamlRenderer]::MamlModelToString($mamlModel) $outputFilePath = Join-Path $OutputFolder ($mamlModel.Name + ".yml") Write-Verbose -Message ($LocalizedData.WritingYamlToPath -f $outputFilePath) MySetContent -Path $outputFilePath -Value $yaml -Encoding $Encoding -Force:$Force } } } } function New-ExternalHelp { [CmdletBinding()] [OutputType([System.IO.FileInfo[]])] param( [Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [SupportsWildcards()] [string[]]$Path, [Parameter(Mandatory=$true)] [string]$OutputPath, [string[]]$ApplicableTag, [System.Text.Encoding]$Encoding = [System.Text.Encoding]::UTF8, [ValidateRange(80, [int]::MaxValue)] [int] $MaxAboutWidth = 80, [string]$ErrorLogFile, [switch]$Force, [switch]$ShowProgress ) begin { validateWorkingProvider $MarkdownFiles = @() $AboutFiles = @() $IsOutputContainer = $true if ( $OutputPath.EndsWith('.xml') -and (-not (Test-Path -PathType Container $OutputPath )) ) { $IsOutputContainer = $false Write-Verbose -Message ($LocalizedData.OutputPathAsFile -f '[New-ExternalHelp]', $OutputPath) } else { New-Item -Type Directory $OutputPath -ErrorAction SilentlyContinue > $null Write-Verbose -Message ($LocalizedData.OutputPathAsDirectory -f '[New-ExternalHelp]', $OutputPath) } if ( -not $ShowProgress -or $(Get-Variable -Name IsCoreClr -ValueOnly -ErrorAction SilentlyContinue) ) { Function Write-Progress() {} } } process { $files = GetMarkdownFilesFromPath $Path if ($files) { $MarkdownFiles += FilterMdFileToExcludeModulePage -Path $files } if($MarkdownFiles) { $AboutFiles += GetAboutTopicsFromPath -Path $Path -MarkDownFilesAlreadyFound $MarkdownFiles.FullName } else { $AboutFiles += GetAboutTopicsFromPath -Path $Path } } end { # Tracks all warnings and errors $warningsAndErrors = New-Object System.Collections.Generic.List[System.Object] try { # write verbose output and filter out files based on applicable tag $MarkdownFiles | ForEach-Object { Write-Verbose -Message ($LocalizedData.InputMarkdownFile -f '[New-ExternalHelp]', $_) } if ($ApplicableTag) { Write-Verbose -Message ($LocalizedData.FilteringForApplicableTag -f '[New-ExternalHelp]', $ApplicableTag) $MarkdownFiles = $MarkdownFiles | ForEach-Object { $applicableList = GetApplicableList -Path $_.FullName # this Compare-Object call is getting the intersection of two string[] if ((-not $applicableList) -or (Compare-Object $applicableList $ApplicableTag -IncludeEqual -ExcludeDifferent)) { # yeild $_ } else { Write-Verbose -Message ($LocalizedData.SkippingMarkdownFile -f '[New-ExternalHelp]', $_) } } } # group the files based on the output xml path metadata tag if ($IsOutputContainer) { $defaultPath = Join-Path $OutputPath $script:DEFAULT_MAML_XML_OUTPUT_NAME $groups = $MarkdownFiles | Group-Object { $h = Get-MarkdownMetadata -Path $_.FullName if ($h -and $h[$script:EXTERNAL_HELP_FILE_YAML_HEADER]) { Join-Path $OutputPath $h[$script:EXTERNAL_HELP_FILE_YAML_HEADER] } else { $msgLine1 = $LocalizedData.CannotFindInMetadataFile -f $script:EXTERNAL_HELP_FILE_YAML_HEADER, $_.FullName $msgLine2 = $LocalizedData.PathWillBeUsed -f $defaultPath $warningsAndErrors.Add(@{ Severity = "Warning" Message = "$msgLine1 $msgLine2" FilePath = "$($_.FullName)" }) Write-Warning -Message "[New-ExternalHelp] $msgLine1" Write-Warning -Message "[New-ExternalHelp] $msgLine2" $defaultPath } } } else { $groups = $MarkdownFiles | Group-Object { $OutputPath } } # generate the xml content $r = new-object -TypeName 'Markdown.MAML.Renderer.MamlRenderer' foreach ($group in $groups) { $maml = GetMamlModelImpl ($group.Group | ForEach-Object {$_.FullName}) -Encoding $Encoding -ApplicableTag $ApplicableTag $xml = $r.MamlModelToString($maml) $outPath = $group.Name # group name Write-Verbose -Message ($LocalizedData.WritingExternalHelpToPath -f $outPath) MySetContent -Path $outPath -Value $xml -Encoding $Encoding -Force:$Force } # handle about topics if ($AboutFiles.Count -gt 0) { foreach ($About in $AboutFiles) { $r = New-Object -TypeName 'Markdown.MAML.Renderer.TextRenderer' -ArgumentList($MaxAboutWidth) $Content = Get-Content -Raw $About.FullName $p = NewMarkdownParser $model = $p.ParseString($Content) $value = $r.AboutMarkDownToString($model) $outPath = Join-Path $OutputPath ([io.path]::GetFileNameWithoutExtension($About.FullName) + ".help.txt") if (!(Split-Path -Leaf $outPath).ToUpper().StartsWith("ABOUT_", $true, $null)) { $outPath = Join-Path (Split-Path -Parent $outPath) ("about_" + (Split-Path -Leaf $outPath)) } MySetContent -Path $outPath -Value $value -Encoding $Encoding -Force:$Force } } } catch { # Log error and rethrow $warningsAndErrors.Add(@{ Severity = "Error" Message = "$_.Exception.Message" FilePath = "" }) throw } finally { if ($ErrorLogFile) { ConvertTo-Json $warningsAndErrors | Out-File $ErrorLogFile } } } } function Get-HelpPreview { [CmdletBinding()] [OutputType('MamlCommandHelpInfo')] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1)] [SupportsWildcards()] [string[]]$Path, [switch]$ConvertNotesToList, [switch]$ConvertDoubleDashLists ) process { foreach ($MamlFilePath in $Path) { if (-not (Test-path -Type Leaf $MamlFilePath)) { Write-Error -Message ($LocalizedData.FileNotFoundSkipping -f $MamlFilePath) continue } # this is Resolve-Path that resolves mounted drives (i.e. good for tests) $MamlFilePath = (Get-ChildItem $MamlFilePath).FullName # Read the malm file $xml = [xml](Get-Content $MamlFilePath -Raw -ea SilentlyContinue) if (-not $xml) { # already error-out on the convertion, no need to repeat ourselves continue } # we need a copy of maml file to bypass powershell cache, # in case we reuse the same filename few times. $MamlCopyPath = [System.IO.Path]::GetTempFileName() try { if ($ConvertDoubleDashLists) { $p = $xml.GetElementsByTagName('maml:para') | ForEach-Object { # Convert "-- "-lists into "- "-lists # to make them markdown compatible # as described in https://github.com/PowerShell/platyPS/issues/117 $newInnerXml = $_.get_InnerXml() -replace "(`n|^)-- ", '$1- ' $_.set_InnerXml($newInnerXml) } } if ($ConvertNotesToList) { # Add inline bullet-list, as described in https://github.com/PowerShell/platyPS/issues/125 $xml.helpItems.command.alertSet.alert | ForEach-Object { # make first <para> a list item # add indentations to other <para> to make them continuation of list item $_.ChildNodes | Select-Object -First 1 | ForEach-Object { $newInnerXml = '* ' + $_.get_InnerXml() $_.set_InnerXml($newInnerXml) } $_.ChildNodes | Select-Object -Skip 1 | ForEach-Object { # this character is not a valid space. # We have to use some odd character here, becasue help engine strips out # all legetimate whitespaces. # Note: powershell doesn't render it properly, it will appear as a non-writable char. $newInnerXml = ([string][char]0xc2a0) * 2 + $_.get_InnerXml() $_.set_InnerXml($newInnerXml) } } } # in PS v5 help engine is not happy, when first non-empty link (== Online version link) is not a valid URI # User encounter this problem too oftern to ignore it, hence this workaround in platyPS: # always add a dummy link with a valid URI into xml and then remove the first link from the help object. # for more context see https://github.com/PowerShell/platyPS/issues/144 $xml.helpItems.command.relatedLinks | ForEach-Object { if ($_) { $_.InnerXml = '<maml:navigationLink xmlns:maml="http://schemas.microsoft.com/maml/2004/10"><maml:linkText>PLATYPS_DUMMY_LINK</maml:linkText><maml:uri>https://github.com/PowerShell/platyPS/issues/144</maml:uri></maml:navigationLink>' + $_.InnerXml } } $xml.Save($MamlCopyPath) foreach ($command in $xml.helpItems.command.details.name) { #PlatyPS will have trouble parsing a command with space around the name. $command = $command.Trim() $thisDefinition = @" <# .ExternalHelp $MamlCopyPath #> filter $command { [CmdletBinding()] Param ( [Parameter(Mandatory=`$true)] [switch]`$platyPSHijack ) Microsoft.PowerShell.Utility\Write-Warning 'PlatyPS hijacked your command $command.' Microsoft.PowerShell.Utility\Write-Warning 'We are sorry for that. It means, there is a bug in our Get-HelpPreview logic.' Microsoft.PowerShell.Utility\Write-Warning 'Please report this issue https://github.com/PowerShell/platyPS/issues' Microsoft.PowerShell.Utility\Write-Warning 'Restart PowerShell to fix the problem.' } # filter is rare enough to distinguish with other commands `$innerHelp = Microsoft.PowerShell.Core\Get-Help $command -Full -Category filter Microsoft.PowerShell.Core\Export-ModuleMember -Function @() "@ $m = New-Module ( [scriptblock]::Create( "$thisDefinition" )) $help = & $m { $innerHelp } # this is the second part of the workaround for https://github.com/PowerShell/platyPS/issues/144 # see comments above for context $help.relatedLinks | ForEach-Object { if ($_) { $_.navigationLink = $_.navigationLink | Select-Object -Skip 1 } } $help # yeild } } finally { Remove-Item $MamlCopyPath } } } } function New-ExternalHelpCab { [Cmdletbinding()] param( [parameter(Mandatory=$true)] [ValidateScript( { if(Test-Path $_ -PathType Container) { $True } else { Throw $LocalizedData.PathIsNotFolder -f $_ } })] [string] $CabFilesFolder, [parameter(Mandatory=$true)] [ValidateScript( { if(Test-Path $_ -PathType Leaf) { $True } else { Throw $LocalizedData.PathIsNotFile -f $_ } })] [string] $LandingPagePath, [parameter(Mandatory=$true)] [string] $OutputFolder, [parameter()] [switch] $IncrementHelpVersion ) begin { validateWorkingProvider New-Item -Type Directory $OutputFolder -ErrorAction SilentlyContinue > $null } process { #Testing for MakeCab.exe Write-Verbose -Message ($LocalizedData.TestCommandExists -f 'MakeCab.exe') $MakeCab = Get-Command MakeCab if(-not $MakeCab) { throw $LocalizedData.CommandNotFound -f 'MakeCab.exe' } #Testing for files in source directory if((Get-ChildItem -Path $CabFilesFolder).Count -le 0) { throw $LocalizedData.FilesNotFoundInFolder -f $CabFilesFolder } #Testing for valid help file types $ValidHelpFileTypes = '.xml', '.txt' $HelpFiles = Get-ChildItem -Path $CabFilesFolder -File $ValidHelpFiles = $HelpFiles | Where-Object { $_.Extension -in $ValidHelpFileTypes } $InvalidHelpFiles = $HelpFiles | Where-Object { $_.Extension -notin $ValidHelpFileTypes } if(-not $ValidHelpFiles) { throw $LocalizedData.NoValidHelpFiles } if($InvalidHelpFiles) { $InvalidHelpFiles | ForEach-Object { Write-Warning -Message ($LocalizedData.FileNotValidHelpFileType -f $_.FullName) } } ###Get Yaml Metadata here $Metadata = Get-MarkdownMetadata -Path $LandingPagePath $ModuleName = $Metadata[$script:MODULE_PAGE_MODULE_NAME] $Guid = $Metadata[$script:MODULE_PAGE_GUID] $Locale = $Metadata[$script:MODULE_PAGE_LOCALE] $FwLink = $Metadata[$script:MODULE_PAGE_FW_LINK] $OldHelpVersion = $Metadata[$script:MODULE_PAGE_HELP_VERSION] $AdditionalLocale = $Metadata[$script:MODULE_PAGE_ADDITIONAL_LOCALE] if($IncrementHelpVersion) { #IncrementHelpVersion $HelpVersion = IncrementHelpVersion -HelpVersionString $OldHelpVersion $MdContent = Get-Content -raw $LandingPagePath $MdContent = $MdContent.Replace($OldHelpVersion,$HelpVersion) Set-Content -path $LandingPagePath -value $MdContent } else { $HelpVersion = $OldHelpVersion } #Create HelpInfo File #Testing the destination directories, creating if none exists. if(-not (Test-Path $OutputFolder)) { Write-Verbose -Message ($LocalizedData.FolderNotFoundCreating -f $OutputFolder) New-Item -ItemType Directory -Path $OutputFolder | Out-Null } Write-Verbose -Message ($LocalizedData.CabFileInfo -f $ModuleName, $Guid, $Locale) #Building the cabinet file name. $cabName = ("{0}_{1}_{2}_HelpContent.cab" -f $ModuleName,$Guid,$Locale) $zipName = ("{0}_{1}_{2}_HelpContent.zip" -f $ModuleName,$Guid,$Locale) $zipPath = (Join-Path $OutputFolder $zipName) #Setting Cab Directives, make a cab is turned on, compression is turned on Write-Verbose -Message ($LocalizedData.CreatingCabFileDirectives) $DirectiveFile = "dir.dff" New-Item -ItemType File -Name $DirectiveFile -Force | Out-Null Add-Content $DirectiveFile ".Set Cabinet=on" Add-Content $DirectiveFile ".Set Compress=on" Add-Content $DirectiveFile ".Set MaxDiskSize=CDROM" #Creates an entry in the cab directive file for each file in the source directory (uses FullName to get fuly qualified file path and name) foreach($file in $ValidHelpFiles) { Add-Content $DirectiveFile ("'" + ($file).FullName +"'" ) Compress-Archive -DestinationPath $zipPath -Path $file.FullName -Update } #Making Cab Write-Verbose -Message ($LocalizedData.CreatingCabFile) MakeCab.exe /f $DirectiveFile | Out-Null #Naming CabFile Write-Verbose -Message ($LocalizedData.MovingCabFile -f $OutputFolder) Copy-Item "disk1/1.cab" (Join-Path $OutputFolder $cabName) #Remove ExtraFiles created by the cabbing process Write-Verbose -Message ($LocalizedData.RemovingExtraCabFileContents) Remove-Item "setup.inf" -ErrorAction SilentlyContinue Remove-Item "setup.rpt" -ErrorAction SilentlyContinue Remove-Item $DirectiveFile -ErrorAction SilentlyContinue Remove-Item -Path "disk1" -Recurse -ErrorAction SilentlyContinue #Create the HelpInfo Xml MakeHelpInfoXml -ModuleName $ModuleName -GUID $Guid -HelpCulture $Locale -HelpVersion $HelpVersion -URI $FwLink -OutputFolder $OutputFolder if($AdditionalLocale) { $allLocales = $AdditionalLocale -split ',' foreach($loc in $allLocales) { #Create the HelpInfo Xml for each locale $locVersion = $Metadata["$loc Version"] if([String]::IsNullOrEmpty($locVersion)) { Write-Warning -Message ($LocalizedData.VersionNotFoundForLocale -f $loc) } else { MakeHelpInfoXml -ModuleName $ModuleName -GUID $Guid -HelpCulture $loc -HelpVersion $locVersion -URI $FwLink -OutputFolder $OutputFolder } } } } } #endregion #region Implementation # IIIIIIIIII lllllll tttt tttt iiii # I::::::::I l:::::l ttt:::t ttt:::t i::::i # I::::::::I l:::::l t:::::t t:::::t iiii # II::::::II l:::::l t:::::t t:::::t # I::::I mmmmmmm mmmmmmm ppppp ppppppppp l::::l eeeeeeeeeeee mmmmmmm mmmmmmm eeeeeeeeeeee nnnn nnnnnnnn ttttttt:::::ttttttt aaaaaaaaaaaaa ttttttt:::::ttttttt iiiiiii ooooooooooo nnnn nnnnnnnn # I::::I mm:::::::m m:::::::mm p::::ppp:::::::::p l::::l ee::::::::::::ee mm:::::::m m:::::::mm ee::::::::::::ee n:::nn::::::::nn t:::::::::::::::::t a::::::::::::a t:::::::::::::::::t i:::::i oo:::::::::::oo n:::nn::::::::nn # I::::I m::::::::::mm::::::::::mp:::::::::::::::::p l::::l e::::::eeeee:::::eem::::::::::mm::::::::::m e::::::eeeee:::::een::::::::::::::nn t:::::::::::::::::t aaaaaaaaa:::::at:::::::::::::::::t i::::i o:::::::::::::::on::::::::::::::nn # I::::I m::::::::::::::::::::::mpp::::::ppppp::::::p l::::l e::::::e e:::::em::::::::::::::::::::::me::::::e e:::::enn:::::::::::::::ntttttt:::::::tttttt a::::atttttt:::::::tttttt i::::i o:::::ooooo:::::onn:::::::::::::::n # I::::I m:::::mmm::::::mmm:::::m p:::::p p:::::p l::::l e:::::::eeeee::::::em:::::mmm::::::mmm:::::me:::::::eeeee::::::e n:::::nnnn:::::n t:::::t aaaaaaa:::::a t:::::t i::::i o::::o o::::o n:::::nnnn:::::n # I::::I m::::m m::::m m::::m p:::::p p:::::p l::::l e:::::::::::::::::e m::::m m::::m m::::me:::::::::::::::::e n::::n n::::n t:::::t aa::::::::::::a t:::::t i::::i o::::o o::::o n::::n n::::n # I::::I m::::m m::::m m::::m p:::::p p:::::p l::::l e::::::eeeeeeeeeee m::::m m::::m m::::me::::::eeeeeeeeeee n::::n n::::n t:::::t a::::aaaa::::::a t:::::t i::::i o::::o o::::o n::::n n::::n # I::::I m::::m m::::m m::::m p:::::p p::::::p l::::l e:::::::e m::::m m::::m m::::me:::::::e n::::n n::::n t:::::t tttttta::::a a:::::a t:::::t tttttt i::::i o::::o o::::o n::::n n::::n # II::::::IIm::::m m::::m m::::m p:::::ppppp:::::::pl::::::le::::::::e m::::m m::::m m::::me::::::::e n::::n n::::n t::::::tttt:::::ta::::a a:::::a t::::::tttt:::::ti::::::io:::::ooooo:::::o n::::n n::::n # I::::::::Im::::m m::::m m::::m p::::::::::::::::p l::::::l e::::::::eeeeeeee m::::m m::::m m::::m e::::::::eeeeeeee n::::n n::::n tt::::::::::::::ta:::::aaaa::::::a tt::::::::::::::ti::::::io:::::::::::::::o n::::n n::::n # I::::::::Im::::m m::::m m::::m p::::::::::::::pp l::::::l ee:::::::::::::e m::::m m::::m m::::m ee:::::::::::::e n::::n n::::n tt:::::::::::tt a::::::::::aa:::a tt:::::::::::tti::::::i oo:::::::::::oo n::::n n::::n # IIIIIIIIIImmmmmm mmmmmm mmmmmm p::::::pppppppp llllllll eeeeeeeeeeeeee mmmmmm mmmmmm mmmmmm eeeeeeeeeeeeee nnnnnn nnnnnn ttttttttttt aaaaaaaaaa aaaa ttttttttttt iiiiiiii ooooooooooo nnnnnn nnnnnn # p:::::p # p:::::p # p:::::::p # p:::::::p # p:::::::p # ppppppppp # parse out the list "applicable" tags from yaml header function GetApplicableList { param( [Parameter(Mandatory=$true)] $Path ) $h = Get-MarkdownMetadata -Path $Path if ($h -and $h[$script:APPLICABLE_YAML_HEADER]) { return $h[$script:APPLICABLE_YAML_HEADER].Split(',').Trim() } } function SortParamsAlphabetically { param( [Parameter(Mandatory=$true)] $MamlCommandObject ) # sort parameters alphabetically with minor exceptions # https://github.com/PowerShell/platyPS/issues/142 $confirm = $MamlCommandObject.Parameters | Where-Object { $_.Name -eq 'Confirm' } $whatif = $MamlCommandObject.Parameters | Where-Object { $_.Name -eq 'WhatIf' } $includeTotalCount = $MamlCommandObject.Parameters | Where-Object { $_.Name -eq 'IncludeTotalCount' } $skip = $MamlCommandObject.Parameters | Where-Object { $_.Name -eq 'Skip' } $first = $MamlCommandObject.Parameters | Where-Object { $_.Name -eq 'First' } if ($confirm) { $MamlCommandObject.Parameters.Remove($confirm) > $null } if ($whatif) { $MamlCommandObject.Parameters.Remove($whatif) > $null } if ($includeTotalCount) { $MamlCommandObject.Parameters.Remove($includeTotalCount) > $null } if ($skip) { $MamlCommandObject.Parameters.Remove($skip) > $null } if ($first) { $MamlCommandObject.Parameters.Remove($first) > $null } $sortedParams = $MamlCommandObject.Parameters | Sort-Object -Property Name $MamlCommandObject.Parameters.Clear() $sortedParams | ForEach-Object { $MamlCommandObject.Parameters.Add($_) } if ($confirm) { $MamlCommandObject.Parameters.Add($confirm) } if ($whatif) { $MamlCommandObject.Parameters.Add($whatif) } if ($includeTotalCount) { $MamlCommandObject.Parameters.Add($includeTotalCount) } if ($skip) { $MamlCommandObject.Parameters.Add($skip) } if ($first) { $MamlCommandObject.Parameters.Add($first) } } # If LogPath not provided, use -Verbose output for logs function GetInfoCallback { param( [string]$LogPath, [switch]$Append ) if ($LogPath) { if (-not (Test-Path $LogPath -PathType Leaf)) { $containerFolder = Split-Path $LogPath if ($containerFolder) { # this if is for $LogPath -eq foo.log case New-Item -Type Directory $containerFolder -ErrorAction SilentlyContinue > $null } if (-not $Append) { # wipe the file, so it can be reused Set-Content -Path $LogPath -value '' -Encoding UTF8 } } $infoCallback = { param([string]$message) Add-Content -Path $LogPath -value $message -Encoding UTF8 } } else { $infoCallback = { param([string]$message) Write-Verbose $message } } return $infoCallback } function GetWarningCallback { $warningCallback = { param([string]$message) Write-Warning $message } return $warningCallback } function GetAboutTopicsFromPath { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string[]]$Path, [string[]]$MarkDownFilesAlreadyFound ) function ConfirmAboutBySecondHeaderText { param( [string]$AboutFilePath ) $MdContent = Get-Content -raw $AboutFilePath $MdParser = new-object -TypeName 'Markdown.MAML.Parser.MarkdownParser' ` -ArgumentList { param([int]$current, [int]$all) Write-Progress -Activity $LocalizedData.ParsingMarkdown -status $LocalizedData.Progress -percentcomplete ($current/$all*100)} $MdObject = $MdParser.ParseString($MdContent) if($MdObject.Children[1].text.length -gt 5) { if($MdObject.Children[1].text.substring(0,5).ToUpper() -eq "ABOUT") { return $true } } return $false } $AboutMarkDownFiles = @() if ($Path) { $Path | ForEach-Object { if (Test-Path -PathType Leaf $_) { if(ConfirmAboutBySecondHeaderText($_)) { $AboutMarkdownFiles += Get-ChildItem $_ } } elseif (Test-Path -PathType Container $_) { if($MarkDownFilesAlreadyFound) { $AboutMarkdownFiles += Get-ChildItem $_ -Filter '*.md' | Where-Object {($_.FullName -notin $MarkDownFilesAlreadyFound) -and (ConfirmAboutBySecondHeaderText($_.FullName))} } else { $AboutMarkdownFiles += Get-ChildItem $_ -Filter '*.md' | Where-Object {ConfirmAboutBySecondHeaderText($_.FullName)} } } else { Write-Error -Message ($LocalizedData.AboutFileNotFound -f $_) } } } return $AboutMarkDownFiles } function FilterMdFileToExcludeModulePage { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [System.IO.FileInfo[]]$Path ) $MarkdownFiles = @() if ($Path) { $Path | ForEach-Object { if (Test-Path $_.FullName) { $md = Get-Content -Raw -Path $_.FullName $yml = [Markdown.MAML.Parser.MarkdownParser]::GetYamlMetadata($md) $isModulePage = $null -ne $yml.'Module Guid' if (-not $isModulePage) { $MarkdownFiles += $_ } } else { Write-Error -Message ($LocalizedData.PathNotFound -f $_.FullName) } } } return $MarkdownFiles } function GetMarkdownFilesFromPath { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [SupportsWildcards()] [string[]]$Path, [switch]$IncludeModulePage ) if ($IncludeModulePage) { $filter = '*.md' } else { $filter = '*-*.md' } $aboutFilePrefixPattern = 'about_*' $MarkdownFiles = @() if ($Path) { $Path | ForEach-Object { if (Test-Path -PathType Leaf $_) { if ((Split-Path -Leaf $_) -notlike $aboutFilePrefixPattern) { $MarkdownFiles += Get-ChildItem $_ } } elseif (Test-Path -PathType Container $_) { $MarkdownFiles += Get-ChildItem $_ -Filter $filter | Where-Object {$_.BaseName -notlike $aboutFilePrefixPattern} } else { Write-Error -Message ($LocalizedData.PathNotFound -f $_) } } } return $MarkdownFiles } function GetParserMode { param( [switch]$PreserveFormatting ) if ($PreserveFormatting) { return [Markdown.MAML.Parser.ParserMode]::FormattingPreserve } else { return [Markdown.MAML.Parser.ParserMode]::Full } } function GetMamlModelImpl { param( [Parameter(Mandatory=$true)] [string[]]$markdownFiles, [Parameter(Mandatory=$true)] [System.Text.Encoding]$Encoding, [switch]$ForAnotherMarkdown, [String[]]$ApplicableTag ) if ($ForAnotherMarkdown -and $ApplicableTag) { throw $LocalizedData.ForAnotherMarkdownAndApplicableTag } # we need to pass it into .NET IEnumerable<MamlCommand> API $res = New-Object 'System.Collections.Generic.List[Markdown.MAML.Model.MAML.MamlCommand]' $markdownFiles | ForEach-Object { $mdText = MyGetContent $_ -Encoding $Encoding $schema = GetSchemaVersion $mdText $p = NewMarkdownParser $t = NewModelTransformer -schema $schema $ApplicableTag $parseMode = GetParserMode -PreserveFormatting:$ForAnotherMarkdown $model = $p.ParseString($mdText, $parseMode, $_) Write-Progress -Activity $LocalizedData.ParsingMarkdown -Completed $maml = $t.NodeModelToMamlModel($model) # flatten $maml | ForEach-Object { if (-not $ForAnotherMarkdown) { # we are preparing model to be transformed in MAML, need to embeed online version url SetOnlineVersionUrlLink -MamlCommandObject $_ -OnlineVersionUrl (GetOnlineVersion $mdText) } $res.Add($_) } } return @(,$res) } function NewMarkdownParser { $warningCallback = GetWarningCallback $progressCallback = { param([int]$current, [int]$all) Write-Progress -Activity $LocalizedData.ParsingMarkdown -status $LocalizedData.Progress -percentcomplete ($current/$all*100) } return new-object -TypeName 'Markdown.MAML.Parser.MarkdownParser' -ArgumentList ($progressCallback, $warningCallback) } function NewModelTransformer { param( [ValidateSet('1.0.0', '2.0.0')] [string]$schema, [string[]]$ApplicableTag ) if ($schema -eq '1.0.0') { throw $LocalizedData.PlatyPS100SchemaDeprecated } elseif ($schema -eq '2.0.0') { $infoCallback = { param([string]$message) Write-Verbose $message } $warningCallback = GetWarningCallback return new-object -TypeName 'Markdown.MAML.Transformer.ModelTransformerVersion2' -ArgumentList ($infoCallback, $warningCallback, $ApplicableTag) } } function GetSchemaVersion { param( [string]$markdown ) $metadata = Get-MarkdownMetadata -markdown $markdown if ($metadata) { $schema = $metadata[$script:SCHEMA_VERSION_YAML_HEADER] } if (-not $schema) { # either there is no metadata, or schema version is not specified. # assume 2.0.0 $schema = '2.0.0' } return $schema } function GetOnlineVersion { param( [string]$markdown ) $metadata = Get-MarkdownMetadata -markdown $markdown $onlineVersionUrl = $null if ($metadata) { $onlineVersionUrl = $metadata[$script:ONLINE_VERSION_YAML_HEADER] } return $onlineVersionUrl } function SetOnlineVersionUrlLink { param( [Parameter(Mandatory=$true)] [Markdown.MAML.Model.MAML.MamlCommand]$MamlCommandObject, [string]$OnlineVersionUrl = $null ) # Online Version URL $currentFirstLink = $MamlCommandObject.Links | Select-Object -First 1 if ($OnlineVersionUrl -and ((-not $currentFirstLink) -or ($currentFirstLink.LinkUri -ne $OnlineVersionUrl))) { $mamlLink = New-Object -TypeName Markdown.MAML.Model.MAML.MamlLink $mamlLink.LinkName = $script:MAML_ONLINE_LINK_DEFAULT_MONIKER $mamlLink.LinkUri = $OnlineVersionUrl # Insert link at the beginning $MamlCommandObject.Links.Insert(0, $mamlLink) } } function MakeHelpInfoXml { Param( [Parameter(mandatory=$true)] [string] $ModuleName, [Parameter(mandatory=$true)] [string] $GUID, [Parameter(mandatory=$true)] [string] $HelpCulture, [Parameter(mandatory=$true)] [string] $HelpVersion, [Parameter(mandatory=$true)] [string] $URI, [Parameter(mandatory=$true)] [string] $OutputFolder ) $HelpInfoFileNme = $ModuleName + "_" + $GUID + "_HelpInfo.xml" $OutputFullPath = Join-Path $OutputFolder $HelpInfoFileNme if(Test-Path $OutputFullPath -PathType Leaf) { [xml] $HelpInfoContent = Get-Content $OutputFullPath } #Create the base XML object for the Helpinfo.xml file. $xml = new-object xml $ns = "http://schemas.microsoft.com/powershell/help/2010/05" $declaration = $xml.CreateXmlDeclaration("1.0","utf-8",$null) $rootNode = $xml.CreateElement("HelpInfo",$ns) $xml.InsertBefore($declaration,$xml.DocumentElement) $xml.AppendChild($rootNode) $HelpContentUriNode = $xml.CreateElement("HelpContentURI",$ns) $HelpContentUriNode.InnerText = $URI $xml["HelpInfo"].AppendChild($HelpContentUriNode) $HelpSupportedCulturesNode = $xml.CreateElement("SupportedUICultures",$ns) $xml["HelpInfo"].AppendChild($HelpSupportedCulturesNode) #If no previous help file if(-not $HelpInfoContent) { $HelpUICultureNode = $xml.CreateElement("UICulture",$ns) $xml["HelpInfo"]["SupportedUICultures"].AppendChild($HelpUICultureNode) $HelpUICultureNameNode = $xml.CreateElement("UICultureName",$ns) $HelpUICultureNameNode.InnerText = $HelpCulture $xml["HelpInfo"]["SupportedUICultures"]["UICulture"].AppendChild($HelpUICultureNameNode) $HelpUICultureVersionNode = $xml.CreateElement("UICultureVersion",$ns) $HelpUICultureVersionNode.InnerText = $HelpVersion $xml["HelpInfo"]["SupportedUICultures"]["UICulture"].AppendChild($HelpUICultureVersionNode) [xml] $HelpInfoContent = $xml } else { #Get old culture info $ExistingCultures = @{} foreach($Culture in $HelpInfoContent.HelpInfo.SupportedUICultures.UICulture) { $ExistingCultures.Add($Culture.UICultureName, $Culture.UICultureVersion) } #If culture exists update version, if not, add culture and version if(-not ($HelpCulture -in $ExistingCultures.Keys)) { $ExistingCultures.Add($HelpCulture,$HelpVersion) } else { $ExistingCultures[$HelpCulture] = $HelpVersion } $cultureNames = @() $cultureNames += $ExistingCultures.GetEnumerator() #write out cultures to XML for($i=0;$i -lt $ExistingCultures.Count; $i++) { $HelpUICultureNode = $xml.CreateElement("UICulture",$ns) $HelpUICultureNameNode = $xml.CreateElement("UICultureName",$ns) $HelpUICultureNameNode.InnerText = $cultureNames[$i].Name $HelpUICultureNode.AppendChild($HelpUICultureNameNode) $HelpUICultureVersionNode = $xml.CreateElement("UICultureVersion",$ns) $HelpUICultureVersionNode.InnerText = $cultureNames[$i].Value $HelpUICultureNode.AppendChild($HelpUICultureVersionNode) $xml["HelpInfo"]["SupportedUICultures"].AppendChild($HelpUICultureNode) } [xml] $HelpInfoContent = $xml } #Commit Help if(!(Test-Path $OutputFullPath)) { New-Item -Path $OutputFolder -ItemType File -Name $HelpInfoFileNme } $HelpInfoContent.Save((Get-ChildItem $OutputFullPath).FullName) } function GetHelpFileName { param( [System.Management.Automation.CommandInfo]$CommandInfo ) if ($CommandInfo) { if ($CommandInfo.HelpFile) { if ([System.IO.Path]::IsPathRooted($CommandInfo.HelpFile)) { return (Split-Path -Leaf $CommandInfo.HelpFile) } else { return $CommandInfo.HelpFile } } # only run module evaluations if the input command isn't a script if ($CommandInfo.CommandType -ne "ExternalScript") { # overwise, lets guess it $module = @($CommandInfo.Module) + ($CommandInfo.Module.NestedModules) | Where-Object {$_.ModuleType -ne 'Manifest'} | Where-Object {$_.ExportedCommands.Keys -contains $CommandInfo.Name} $nestedModules = @( ($CommandInfo.Module.NestedModules) | Where-Object { $_.ModuleType -ne 'Manifest' } | Where-Object { $_.ExportedCommands.Keys -contains $CommandInfo.Name } | Select-Object -ExpandProperty Path ) if (-not $module) { Write-Warning -Message ($LocalizedData.ModuleNotFoundFromCommand -f '[GetHelpFileName]', $CommandInfo.Name) return } if ($module.Count -gt 1) { Write-Warning -Message ($LocalizedData.MultipleModulesFoundFromCommand -f '[GetHelpFileName]', $CommandInfo.Name) $module = $module | Select-Object -First 1 } if (Test-Path $module.Path -Type Leaf) { # for regular modules, we can deduct the filename from the module path file $moduleItem = Get-Item -Path $module.Path $isModuleItemNestedModule = $null -ne ($nestedModules | Where-Object { $_ -eq $module.Path }) -and $CommandInfo.ModuleName -ne $module.Name if ($moduleItem.Extension -eq '.psm1' -and -not $isModuleItemNestedModule) { $fileName = $moduleItem.BaseName } else { $fileName = $moduleItem.Name } } else { # if it's something like Dynamic module, # we guess the desired help file name based on the module name $fileName = $module.Name } } return "$fileName-help.xml" } } function MySetContent { [OutputType([System.IO.FileInfo])] param( [Parameter(Mandatory=$true)] [string]$Path, [Parameter(Mandatory=$true)] [string]$value, [Parameter(Mandatory=$true)] [System.Text.Encoding]$Encoding, [switch]$Force ) if (Test-Path $Path) { if (Test-Path $Path -PathType Container) { Write-Error -Message ($LocalizedData.CannotWriteFileDirectoryExists -f $Path) return } if ((MyGetContent -Path $Path -Encoding $Encoding) -eq $value) { Write-Verbose "Not writing to $Path, because content is not changing." return (Get-ChildItem $Path) } if (-not $Force) { Write-Error -Message ($LocalizedData.CannotWriteFileWithoutForce -f $Path) return } } else { $dir = Split-Path $Path if ($dir) { New-Item -Type Directory $dir -ErrorAction SilentlyContinue > $null } } Write-Verbose -Message ($LocalizedData.WritingWithEncoding -f $Path, $Encoding.EncodingName) # just to create a file Set-Content -Path $Path -Value '' $resolvedPath = (Get-ChildItem $Path).FullName [System.IO.File]::WriteAllText($resolvedPath, $value, $Encoding) return (Get-ChildItem $Path) } function MyGetContent { [OutputType([System.String])] param( [Parameter(Mandatory=$true)] [string]$Path, [Parameter(Mandatory=$true)] [System.Text.Encoding]$Encoding ) if (-not(Test-Path $Path)) { throw $LocalizedData.FileNotFound return } else { if (Test-Path $Path -PathType Container) { throw $LocalizedData.PathIsNotFile return } } Write-Verbose -Message ($LocalizedData.ReadingWithEncoding -f $Path, $Encoding.EncodingName) $resolvedPath = (Get-ChildItem $Path).FullName return [System.IO.File]::ReadAllText($resolvedPath, $Encoding) } function NewModuleLandingPage { Param( [Parameter(mandatory=$true)] [string] $Path, [Parameter(mandatory=$true)] [string] $ModuleName, [Parameter(mandatory=$true,ParameterSetName="NewLandingPage")] [string] $ModuleGuid, [Parameter(mandatory=$true,ParameterSetName="NewLandingPage")] [string[]] $CmdletNames, [Parameter(mandatory=$true,ParameterSetName="NewLandingPage")] [string] $Locale, [Parameter(mandatory=$true,ParameterSetName="NewLandingPage")] [string] $Version, [Parameter(mandatory=$true,ParameterSetName="NewLandingPage")] [string] $FwLink, [Parameter(ParameterSetName="UpdateLandingPage")] [switch] $RefreshModulePage, [string]$ModulePagePath, [Parameter(mandatory=$true,ParameterSetName="UpdateLandingPage")] [System.Collections.Generic.List[Markdown.MAML.Model.MAML.MamlCommand]] $Module, [Parameter(mandatory=$true)] [System.Text.Encoding]$Encoding = $script:UTF8_NO_BOM, [switch]$Force ) begin { if ($ModulePagePath) { $LandingPagePath = $ModulePagePath } else { $LandingPageName = $ModuleName + ".md" $LandingPagePath = Join-Path $Path $LandingPageName } } process { $Description = $LocalizedData.Description if($RefreshModulePage) { if(Test-Path $LandingPagePath) { $OldLandingPageContent = Get-Content -Raw $LandingPagePath $OldMetaData = Get-MarkdownMetadata -Markdown $OldLandingPageContent $ModuleGuid = $OldMetaData["Module Guid"] $FwLink = $OldMetaData["Download Help Link"] $Version = $OldMetaData["Help Version"] $Locale = $OldMetaData["Locale"] $p = NewMarkdownParser $model = $p.ParseString($OldLandingPageContent) $index = $model.Children.IndexOf(($model.Children | Where-Object {$_.Text -eq "Description"})) $i = 1 $stillParagraph = $true $Description = "" while($stillParagraph -eq $true) { $Description += $model.Children[$index + $i].spans.text $i++ if($model.Children[$i].NodeType -eq "Heading") { $stillParagraph = $false } } } else { $ModuleGuid = $LocalizedData.ModuleGuid $FwLink = $LocalizedData.FwLink $Version = $LocalizedData.Version $Locale = $LocalizedData.Locale $Description = $LocalizedData.Description } } $Content = "---`r`nModule Name: $ModuleName`r`nModule Guid: $ModuleGuid`r`nDownload Help Link: $FwLink`r`n" $Content += "Help Version: $Version`r`nLocale: $Locale`r`n" $Content += "---`r`n`r`n" $Content += "# $ModuleName Module`r`n## Description`r`n" $Content += "$Description`r`n`r`n## $ModuleName Cmdlets`r`n" if($RefreshModulePage) { $Module | ForEach-Object { $command = $_ if(-not $command.Synopsis) { $Content += "### [" + $command.Name + "](" + $command.Name + ".md)`r`n" + $LocalizedData.Description + "`r`n`r`n" } else { $Content += "### [" + $command.Name + "](" + $command.Name + ".md)`r`n" + $command.Synopsis + "`r`n`r`n" } } } else { $CmdletNames | ForEach-Object { $Content += "### [" + $_ + "](" + $_ + ".md)`r`n" + $LocalizedData.Description + "`r`n`r`n" } } MySetContent -Path $LandingPagePath -value $Content -Encoding $Encoding -Force:$Force # yeild } } function ConvertMamlModelToMarkdown { param( [ValidateNotNullOrEmpty()] [Parameter(Mandatory=$true)] [Markdown.MAML.Model.MAML.MamlCommand]$mamlCommand, [hashtable]$metadata, [switch]$NoMetadata, [switch]$PreserveFormatting ) begin { $parseMode = GetParserMode -PreserveFormatting:$PreserveFormatting $r = New-Object Markdown.MAML.Renderer.MarkdownV2Renderer -ArgumentList $parseMode $count = 0 } process { if (($count++) -eq 0 -and (-not $NoMetadata)) { return $r.MamlModelToString($mamlCommand, $metadata) } else { return $r.MamlModelToString($mamlCommand, $true) # skip version header } } } function GetCommands { param( [Parameter(Mandatory=$true)] [string]$Module, # return names, instead of objects [switch]$AsNames, # use Session for remoting support [System.Management.Automation.Runspaces.PSSession]$Session ) process { # Get-Module doesn't know about Microsoft.PowerShell.Core, so we don't use (Get-Module).ExportedCommands # We use: & (dummy module) {...} syntax to workaround # the case `GetMamlObject -Module joshooaj.platyPS` # because in this case, we are in the module context and Get-Command returns all commands, # not only exported ones. $commands = & (New-Module {}) ([scriptblock]::Create("Get-Command -Module '$Module'")) | Where-Object {$_.CommandType -ne 'Alias'} # we don't want aliases in the markdown output for a module if ($AsNames) { $commands.Name } else { if ($Session) { $commands.Name | ForEach-Object { # yeild MyGetCommand -Cmdlet $_ -Session $Session } } else { $commands } } } } <# Get a compact string representation from TypeInfo or TypeInfo-like object The typeObjectHash api is provided for the remoting support. We use two different parameter sets ensure the tupe of -TypeObject #> function GetTypeString { param( [Parameter(ValueFromPipeline=$true, ParameterSetName='typeObject')] [System.Reflection.TypeInfo] $TypeObject, [Parameter(ValueFromPipeline=$true, ParameterSetName='typeObjectHash')] [PsObject] $TypeObjectHash ) if ($TypeObject) { $TypeObjectHash = $TypeObject } # special case for nullable value types if ($TypeObjectHash.Name -eq 'Nullable`1') { return $TypeObjectHash.GenericTypeArguments.Name } if ($TypeObjectHash.IsGenericType) { # keep information about generic parameters return $TypeObjectHash.ToString() } return $TypeObjectHash.Name } <# You cannot just write 0..($n-1) because if $n == 0 you are screwed. Hence this helper. #> function GetRange { Param( [CmdletBinding()] [parameter(mandatory=$true)] [int]$n ) if ($n -lt 0) { throw $LocalizedData.RangeIsLessThanZero -f $n } if ($n -eq 0) { return } 0..($n - 1) } <# This function proxies Get-Command call. In case of the Remote module, we need to jump thru some hoops to get the actual Command object with proper fields. Remoting doesn't properly serialize command objects, so we need to be creative while extracting all the required metadata from the remote session See https://github.com/PowerShell/platyPS/issues/338 for historical context. #> function MyGetCommand { Param( [CmdletBinding()] [parameter(mandatory=$true, parametersetname="Cmdlet")] [string] $Cmdlet, [System.Management.Automation.Runspaces.PSSession]$Session ) # if there is no remoting, just proxy to Get-Command if (-not $Session) { return Get-Command $Cmdlet } # Here is the structure that we use in ConvertPsObjectsToMamlModel # we fill it up from the remote with some workarounds # # $Command.CommandType # $Command.Name # $Command.ModuleName # $Command.DefaultParameterSet # $Command.CmdletBinding # $ParameterSet in $Command.ParameterSets # $ParameterSet.Name # $ParameterSet.IsDefault # $Parameter in $ParameterSet.Parameters # $Parameter.Name # $Parameter.IsMandatory # $Parameter.Aliases # $Parameter.HelpMessage # $Parameter.Type # $Parameter.ParameterType # $Parameter.ParameterType.Name # $Parameter.ParameterType.GenericTypeArguments.Name # $Parameter.ParameterType.IsGenericType # $Parameter.ParameterType.ToString() - we get that for free from expand # expand first layer of properties function expand([string]$property) { Invoke-Command -Session $Session -ScriptBlock { Get-Command $using:Cmdlet | Select-Object -ExpandProperty $using:property } } # expand second layer of properties on the selected item function expand2([string]$property1, [int]$num, [string]$property2) { Invoke-Command -Session $Session -ScriptBlock { Get-Command $using:Cmdlet | Select-Object -ExpandProperty $using:property1 | Select-Object -Index $using:num -Wait | Select-Object -ExpandProperty $using:property2 } } # expand second and 3rd layer of properties on the selected item function expand3( [string]$property1, [int]$num, [string]$property2, [string]$property3 ) { Invoke-Command -Session $Session -ScriptBlock { Get-Command $using:Cmdlet | Select-Object -ExpandProperty $using:property1 | Select-Object -Index $using:num -Wait | Select-Object -ExpandProperty $using:property2 | Select-Object -ExpandProperty $using:property3 } } function local([string]$property) { Get-Command $Cmdlet | select-object -ExpandProperty $property } # helper function to fill up the parameters metadata function getParams([int]$num) { # this call we need to fill-up ParameterSets.Parameters.ParameterType with metadata $parameterType = expand3 'ParameterSets' $num 'Parameters' 'ParameterType' # this call we need to fill-up ParameterSets.Parameters with metadata $parameters = expand2 'ParameterSets' $num 'Parameters' if ($parameters.Length -ne $parameterType.Length) { Write-Error -Message ($LocalizedData.MetadataDoesNotMatchLength -f $Cmdlet) } foreach ($i in (GetRange $parameters.Length)) { $typeObjectHash = New-Object -TypeName pscustomobject -Property @{ Name = $parameterType[$i].Name IsGenericType = $parameterType[$i].IsGenericType # almost .ParameterType.GenericTypeArguments.Name # TODO: doesn't it worth another round-trip to make it more accurate # and query for the Name? GenericTypeArguments = @{ Name = $parameterType[$i].GenericTypeArguments } } Add-Member -Type NoteProperty -InputObject $parameters[$i] -Name 'ParameterTypeName' -Value (GetTypeString -TypeObjectHash $typeObjectHash) } return $parameters } # we cannot use the nested properties from this $remote command. # ps remoting doesn't serialize all of them properly. # but we can use the top-level onces $remote = Invoke-Command -Session $Session { Get-Command $using:Cmdlet } $psets = expand 'ParameterSets' $psetsArray = @() foreach ($i in (GetRange ($psets | measure-object).Count)) { $parameters = getParams $i $psetsArray += @(New-Object -TypeName pscustomobject -Property @{ Name = $psets[$i].Name IsDefault = $psets[$i].IsDefault Parameters = $parameters }) } $commandHash = @{ Name = $Cmdlet CommandType = $remote.CommandType DefaultParameterSet = $remote.DefaultParameterSet CmdletBinding = $remote.CmdletBinding # for office we cannot get the module name from the remote, grab the local one instead ModuleName = local 'ModuleName' ParameterSets = $psetsArray } return New-Object -TypeName pscustomobject -Property $commandHash } <# This function prepares help and command object (possibly do mock) and passes it to ConvertPsObjectsToMamlModel, then return results #> function GetMamlObject { Param( [CmdletBinding()] [parameter(mandatory=$true, parametersetname="Cmdlet")] [string] $Cmdlet, [parameter(mandatory=$true, parametersetname="Module")] [string] $Module, [parameter(mandatory=$true, parametersetname="Maml")] [string] $MamlFile, [parameter(parametersetname="Maml")] [switch] $ConvertNotesToList, [parameter(parametersetname="Maml")] [switch] $ConvertDoubleDashLists, [switch] $UseFullTypeName, [parameter(parametersetname="Cmdlet")] [parameter(parametersetname="Module")] [System.Management.Automation.Runspaces.PSSession]$Session, [switch]$ExcludeDontShow ) function CommandHasAutogeneratedSynopsis { param([object]$help) return (Get-Command $help.Name -Syntax) -eq ($help.Synopsis) } if($Cmdlet) { Write-Verbose -Message ($LocalizedData.Processing -f $Cmdlet) $Help = Get-Help $Cmdlet $Command = MyGetCommand -Session $Session -Cmdlet $Cmdlet return ConvertPsObjectsToMamlModel -Command $Command -Help $Help -UsePlaceholderForSynopsis:(CommandHasAutogeneratedSynopsis $Help) -UseFullTypeName:$UseFullTypeName -ExcludeDontShow:$ExcludeDontShow } elseif ($Module) { Write-Verbose -Message ($LocalizedData.Processing -f $Module) # GetCommands is slow over remoting, piping here is important for good UX GetCommands $Module -Session $Session | ForEach-Object { $Command = $_ Write-Verbose -Message ("`t" + ($LocalizedData.Processing -f $Command.Name)) $Help = Get-Help $Command.Name # yield ConvertPsObjectsToMamlModel -Command $Command -Help $Help -UsePlaceholderForSynopsis:(CommandHasAutogeneratedSynopsis $Help) -UseFullTypeName:$UseFullTypeName -ExcludeDontShow:$ExcludeDontShow } } else # Maml { $HelpCollection = Get-HelpPreview -Path $MamlFile -ConvertNotesToList:$ConvertNotesToList -ConvertDoubleDashLists:$ConvertDoubleDashLists #Provides Name, CommandType, and Empty Module name from MAML generated module in the $command object. #Otherwise loads the results from Get-Command <Cmdlet> into the $command object $HelpCollection | ForEach-Object { $Help = $_ $Command = [PsObject] @{ Name = $Help.Name CommandType = $Help.Category HelpFile = (Split-Path $MamlFile -Leaf) } # yield ConvertPsObjectsToMamlModel -Command $Command -Help $Help -UseHelpForParametersMetadata -UseFullTypeName:$UseFullTypeName -ExcludeDontShow:$ExcludeDontShow } } } function AddLineBreaksForParagraphs { [CmdletBinding()] param( [Parameter(Mandatory=$false, ValueFromPipeline=$true)] [string]$text ) begin { $paragraphs = @() } process { $text = $text.Trim() $paragraphs += $text } end { $paragraphs -join "`r`n`r`n" } } function DescriptionToPara { [CmdletBinding()] param( [Parameter(Mandatory=$false, ValueFromPipeline=$true)] $description ) process { # on some old maml modules description uses Tag to store *-bullet-points # one example of it is Exchange $description.Tag + "" + $description.Text } } function IncrementHelpVersion { param( [string] $HelpVersionString ) process { if($HelpVersionString -eq $LocalizedData.HelpVersion) { return "1.0.0.0" } $lastDigitPosition = $HelpVersionString.LastIndexOf(".") + 1 $frontDigits = $HelpVersionString.Substring(0,$lastDigitPosition) $frontDigits += ([int] $HelpVersionString.Substring($lastDigitPosition)) + 1 return $frontDigits } } <# This function converts help and command object (possibly mocked) into a Maml Model #> function ConvertPsObjectsToMamlModel { [CmdletBinding()] [OutputType([Markdown.MAML.Model.MAML.MamlCommand])] param( [Parameter(Mandatory=$true)] [object]$Command, [Parameter(Mandatory=$true)] [object]$Help, [switch]$UseHelpForParametersMetadata, [switch]$UsePlaceholderForSynopsis, [switch]$UseFullTypeName, [switch]$ExcludeDontShow ) function isCommonParameterName { param([string]$parameterName, [switch]$Workflow) if (@( 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'ProgressAction' ) -contains $parameterName) { return $true } if ($Workflow) { return @( 'PSParameterCollection', 'PSComputerName', 'PSCredential', 'PSConnectionRetryCount', 'PSConnectionRetryIntervalSec', 'PSRunningTimeoutSec', 'PSElapsedTimeoutSec', 'PSPersist', 'PSAuthentication', 'PSAuthenticationLevel', 'PSApplicationName', 'PSPort', 'PSUseSSL', 'PSConfigurationName', 'PSConnectionURI', 'PSAllowRedirection', 'PSSessionOption', 'PSCertificateThumbprint', 'PSPrivateMetadata', 'AsJob', 'JobName' ) -contains $parameterName } return $false } function getPipelineValue($Parameter) { if ($Parameter.ValueFromPipeline) { if ($Parameter.ValueFromPipelineByPropertyName) { return 'True (ByPropertyName, ByValue)' } else { return 'True (ByValue)' } } else { if ($Parameter.ValueFromPipelineByPropertyName) { return 'True (ByPropertyName)' } else { return 'False' } } } function normalizeFirstLatter { param( [Parameter(ValueFromPipeline=$true)] [string]$value ) if ($value -and $value.Length -gt 0) { return $value.Substring(0,1).ToUpperInvariant() + $value.substring(1) } return $value } #endregion $MamlCommandObject = New-Object -TypeName Markdown.MAML.Model.MAML.MamlCommand #region Command Object Values Processing $IsWorkflow = $Command.CommandType -eq 'Workflow' #Get Name $MamlCommandObject.Name = $Command.Name $MamlCommandObject.ModuleName = $Command.ModuleName #region Data not provided by the command object #Get Description #Not provided by the command object. $MamlCommandObject.Description = New-Object -TypeName Markdown.MAML.Model.Markdown.SectionBody ($LocalizedData.Description) #endregion #Get Syntax #region Get the Syntax Parameter Set objects function FillUpParameterFromHelp { param( [Parameter(Mandatory=$true)] [Markdown.MAML.Model.MAML.MamlParameter]$ParameterObject ) $HelpEntry = $Help.parameters.parameter | Where-Object {$_.Name -eq $ParameterObject.Name} $ParameterObject.DefaultValue = $HelpEntry.defaultValue | normalizeFirstLatter $ParameterObject.VariableLength = $HelpEntry.variableLength -eq 'True' $ParameterObject.Position = $HelpEntry.position | normalizeFirstLatter if ($HelpEntry.description) { if ($HelpEntry.description.text) { $ParameterObject.Description = $HelpEntry.description | DescriptionToPara | AddLineBreaksForParagraphs } else { # this case happens, when there is HelpMessage in 'Parameter' attribute, # but there is no maml or comment-based help. # then help engine put string outside of 'text' property # In this case there is no DescriptionToPara call needed $ParameterObject.Description = $HelpEntry.description | AddLineBreaksForParagraphs } } $syntaxParam = $Help.syntax.syntaxItem.parameter | Where-Object {$_.Name -eq $Parameter.Name} | Select-Object -First 1 if ($syntaxParam) { # otherwise we could potentialy get it from Reflection but not doing it for now foreach ($parameterValue in $syntaxParam.parameterValueGroup.parameterValue) { $ParameterObject.parameterValueGroup.Add($parameterValue) } } } function FillUpSyntaxFromCommand { foreach($ParameterSet in $Command.ParameterSets) { $SyntaxObject = New-Object -TypeName Markdown.MAML.Model.MAML.MamlSyntax $SyntaxObject.ParameterSetName = $ParameterSet.Name $SyntaxObject.IsDefault = $ParameterSet.IsDefault foreach($Parameter in $ParameterSet.Parameters) { # ignore CommonParameters if (isCommonParameterName $Parameter.Name -Workflow:$IsWorkflow) { # but don't ignore them, if they have explicit help entries if ($Help.parameters.parameter | Where-Object {$_.Name -eq $Parameter.Name}) { } else { continue } } $hasDontShow = $false $hasSupportsWildsCards = $false foreach ($Attribute in $Parameter.Attributes) { if ($ExcludeDontShow) { if ($Attribute.TypeId.ToString() -eq 'System.Management.Automation.ParameterAttribute' -and $Attribute.DontShow) { $hasDontShow = $true } } if ($Attribute.TypeId.ToString() -eq 'System.Management.Automation.SupportsWildcardsAttribute') { $hasSupportsWildsCards = $true } } if ($hasDontShow) { continue } $ParameterObject = New-Object -TypeName Markdown.MAML.Model.MAML.MamlParameter $ParameterObject.Name = $Parameter.Name $ParameterObject.Required = $Parameter.IsMandatory $ParameterObject.PipelineInput = getPipelineValue $Parameter $ParameterObject.Globbing = $hasSupportsWildsCards # the ParameterType could be just a string in case of remoting # or a TypeInfo object, in the regular case if ($Session) { # in case of remoting we already pre-calcuated the Type string $ParameterObject.Type = $Parameter.ParameterTypeName } else { $ParameterObject.Type = GetTypeString -TypeObject $Parameter.ParameterType } # ToString() works in both cases $ParameterObject.FullType = $Parameter.ParameterType.ToString() $ParameterObject.ValueRequired = -not ($Parameter.Type -eq "SwitchParameter") # thisDefinition is a heuristic foreach($Alias in $Parameter.Aliases) { $ParameterObject.Aliases += $Alias } $ParameterObject.Description = if ([String]::IsNullOrEmpty($Parameter.HelpMessage)) { # additional new-lines are needed for Update-MarkdownHelp scenario. switch ($Parameter.Name) { # we have well-known parameters and can generate a reasonable description for them # https://github.com/PowerShell/platyPS/issues/211 'Confirm' { $LocalizedData.Confirm + "`r`n`r`n" } 'WhatIf' { $LocalizedData.WhatIf + "`r`n`r`n" } 'IncludeTotalCount' { $LocalizedData.IncludeTotalCount + "`r`n`r`n" } 'Skip' { $LocalizedData.Skip + "`r`n`r`n" } 'First' { $LocalizedData.First + "`r`n`r`n" } default { ($LocalizedData.ParameterDescription -f $Parameter.Name) + "`r`n`r`n" } } } else { $Parameter.HelpMessage } FillUpParameterFromHelp $ParameterObject $SyntaxObject.Parameters.Add($ParameterObject) } $MamlCommandObject.Syntax.Add($SyntaxObject) } } function FillUpSyntaxFromHelp { function GuessTheType { param([string]$type) if (-not $type) { # weired, but that's how it works return 'SwitchParameter' } return $type } $ParamSetCount = 0 foreach($ParameterSet in $Help.syntax.syntaxItem) { $SyntaxObject = New-Object -TypeName Markdown.MAML.Model.MAML.MamlSyntax $ParamSetCount++ $SyntaxObject.ParameterSetName = $script:SET_NAME_PLACEHOLDER + "_" + $ParamSetCount foreach($Parameter in $ParameterSet.Parameter) { $ParameterObject = New-Object -TypeName Markdown.MAML.Model.MAML.MamlParameter $ParameterObject.Type = GuessTheType $Parameter.parameterValue $ParameterObject.Name = $Parameter.Name $ParameterObject.Required = $Parameter.required -eq 'true' $ParameterObject.PipelineInput = $Parameter.pipelineInput | normalizeFirstLatter $ParameterObject.ValueRequired = -not ($ParameterObject.Type -eq "SwitchParameter") # thisDefinition is a heuristic if ($parameter.Aliases -ne 'None') { $ParameterObject.Aliases = $parameter.Aliases } FillUpParameterFromHelp $ParameterObject $SyntaxObject.Parameters.Add($ParameterObject) } $MamlCommandObject.Syntax.Add($SyntaxObject) } } if ($UseHelpForParametersMetadata) { FillUpSyntaxFromHelp } else { FillUpSyntaxFromCommand } #endregion ########## #####GET THE HELP-Object Content and add it to the MAML Object##### #region Help-Object processing #Get Synopsis if ($UsePlaceholderForSynopsis) { # Help object ALWAYS contains SYNOPSIS. # If it's not available, it's auto-generated. # We don't want to include auto-generated SYNOPSIS (see https://github.com/PowerShell/platyPS/issues/110) $MamlCommandObject.Synopsis = New-Object -TypeName Markdown.MAML.Model.Markdown.SectionBody ($LocalizedData.Synopsis) } else { $MamlCommandObject.Synopsis = New-Object -TypeName Markdown.MAML.Model.Markdown.SectionBody ( # $Help.Synopsis only contains the first paragraph # https://github.com/PowerShell/platyPS/issues/328 $Help.details.description | DescriptionToPara | AddLineBreaksForParagraphs ) } #Get Description if($Help.description -ne $null) { $MamlCommandObject.Description = New-Object -TypeName Markdown.MAML.Model.Markdown.SectionBody ( $Help.description | DescriptionToPara | AddLineBreaksForParagraphs ) } #Add to Notes #From the Help AlertSet data if($help.alertSet) { $MamlCommandObject.Notes = New-Object -TypeName Markdown.MAML.Model.Markdown.SectionBody ( $help.alertSet.alert | DescriptionToPara | AddLineBreaksForParagraphs ) } # Not provided by the command object. Using the Command Type to create a note declaring it's type. # We can add this placeholder #Add to relatedLinks if($help.relatedLinks) { foreach($link in $Help.relatedLinks.navigationLink) { $mamlLink = New-Object -TypeName Markdown.MAML.Model.MAML.MamlLink $mamlLink.LinkName = $link.linkText $mamlLink.LinkUri = $link.uri $MamlCommandObject.Links.Add($mamlLink) } } #Add Examples foreach($Example in $Help.examples.example) { $MamlExampleObject = New-Object -TypeName Markdown.MAML.Model.MAML.MamlExample $MamlExampleObject.Introduction = $Example.introduction $MamlExampleObject.Title = $Example.title $MamlExampleObject.Code = @( New-Object -TypeName Markdown.MAML.Model.MAML.MamlCodeBlock ($Example.code, '') ) $RemarkText = $Example.remarks | DescriptionToPara | AddLineBreaksForParagraphs $MamlExampleObject.Remarks = $RemarkText $MamlCommandObject.Examples.Add($MamlExampleObject) } #Get Inputs #Reccomend adding a Parameter Name and Parameter Set Name to each input object. #region Inputs $Inputs = @() $Help.inputTypes.inputType | ForEach-Object { $InputDescription = $_.description $inputtypes = $_.type.name if ($_.description -eq $null -and $_.type.name -ne $null) { $inputtypes = $_.type.name.split("`n", [System.StringSplitOptions]::RemoveEmptyEntries) } $inputtypes | ForEach-Object { $InputObject = New-Object -TypeName Markdown.MAML.Model.MAML.MamlInputOutput $InputObject.TypeName = $_ $InputObject.Description = $InputDescription | DescriptionToPara | AddLineBreaksForParagraphs $Inputs += $InputObject } } foreach($Input in $Inputs) {$MamlCommandObject.Inputs.Add($Input)} #endregion #Get Outputs #No Output Type description is provided from the command object. #region Outputs $Outputs = @() $Help.returnValues.returnValue | ForEach-Object { $OuputDescription = $_.description $Outputtypes = $_.type.name if ($_.description -eq $null -and $_.type.name -ne $null) { $Outputtypes = $_.type.name.split("`n", [System.StringSplitOptions]::RemoveEmptyEntries) } $Outputtypes | ForEach-Object { $OutputObject = New-Object -TypeName Markdown.MAML.Model.MAML.MamlInputOutput $OutputObject.TypeName = $_ $OutputObject.Description = $OuputDescription | DescriptionToPara | AddLineBreaksForParagraphs $Outputs += $OutputObject } } foreach($Output in $Outputs) {$MamlCommandObject.Outputs.Add($Output)} #endregion ########## #####Adding Parameters Section from Syntax block##### #region Parameter Unique Selection from Parameter Sets #This will only work when the Parameters member has a public set as well as a get. function Get-ParameterByName { param( [string]$Name ) $defaultSyntax = $MamlCommandObject.Syntax | Where-Object { $Command.DefaultParameterSet -eq $_.ParameterSetName } # default syntax should have a priority $syntaxes = @($defaultSyntax) + $MamlCommandObject.Syntax foreach ($s in $syntaxes) { $param = $s.Parameters | Where-Object { $_.Name -eq $Name } if ($param) { return $param } } } function Get-ParameterNamesOrder() { # we want to keep original order for existing help # if something changed: # - remove it from it's position # - add to the end $helpNames = $Help.parameters.parameter.Name if (-not $helpNames) { $helpNames = @() } # sort-object unique does case-insensiteve unification $realNames = $MamlCommandObject.Syntax.Parameters.Name | Sort-object -Unique if (-not $realNames) { $realNames = @() } $realNamesList = New-Object 'System.Collections.Generic.List[string]' $realNamesList.AddRange( ( [string[]] $realNames) ) foreach ($name in $helpNames) { if ($realNamesList.Remove($name)) { # yeild $name } # Otherwise it didn't exist } foreach ($name in $realNamesList) { # yeild $name } } foreach($ParameterName in (Get-ParameterNamesOrder)) { $Parameter = Get-ParameterByName $ParameterName if ($Parameter) { if ($UseFullTypeName) { $Parameter = $Parameter.Clone() $Parameter.Type = $Parameter.FullType } $MamlCommandObject.Parameters.Add($Parameter) } else { Write-Warning -Message ($LocalizedData.ParameterNotFound -f '[Markdown generation]', $ParameterName, $Command.Name) } } # Handle CommonParameters, default for MamlCommand is SupportCommonParameters = $true if ($Command.CmdletBinding -eq $false) { # Remove CommonParameters by exception $MamlCommandObject.SupportCommonParameters = $false } # Handle CommonWorkflowParameters $MamlCommandObject.IsWorkflow = $IsWorkflow #endregion ########## return $MamlCommandObject } function validateWorkingProvider { if((Get-Location).Drive.Provider.Name -ne 'FileSystem') { Write-Verbose -Message $LocalizedData.SettingFileSystemProvider $AvailableFileSystemDrives = Get-PSDrive | Where-Object {$_.Provider.Name -eq "FileSystem"} | Select-Object Root if($AvailableFileSystemDrives.Count -gt 0) { Set-Location $AvailableFileSystemDrives[0].Root } else { throw $LocalizedData.FailedSettingFileSystemProvider } } } #endregion #region Parameter Auto Completers # bbbbbbbb # TTTTTTTTTTTTTTTTTTTTTTT b::::::b CCCCCCCCCCCCC lllllll tttt iiii # T:::::::::::::::::::::T b::::::b CCC::::::::::::C l:::::l ttt:::t i::::i # T:::::::::::::::::::::T b::::::b CC:::::::::::::::C l:::::l t:::::t iiii # T:::::TT:::::::TT:::::T b:::::b C:::::CCCCCCCC::::C l:::::l t:::::t # TTTTTT T:::::T TTTTTTaaaaaaaaaaaaa b:::::bbbbbbbbb C:::::C CCCCCC ooooooooooo mmmmmmm mmmmmmm ppppp ppppppppp l::::l eeeeeeeeeeee ttttttt:::::ttttttt iiiiiii ooooooooooo nnnn nnnnnnnn # T:::::T a::::::::::::a b::::::::::::::bb C:::::C oo:::::::::::oo mm:::::::m m:::::::mm p::::ppp:::::::::p l::::l ee::::::::::::ee t:::::::::::::::::t i:::::i oo:::::::::::oo n:::nn::::::::nn # T:::::T aaaaaaaaa:::::a b::::::::::::::::b C:::::C o:::::::::::::::om::::::::::mm::::::::::mp:::::::::::::::::p l::::l e::::::eeeee:::::eet:::::::::::::::::t i::::i o:::::::::::::::on::::::::::::::nn # T:::::T a::::a b:::::bbbbb:::::::b --------------- C:::::C o:::::ooooo:::::om::::::::::::::::::::::mpp::::::ppppp::::::p l::::l e::::::e e:::::etttttt:::::::tttttt i::::i o:::::ooooo:::::onn:::::::::::::::n # T:::::T aaaaaaa:::::a b:::::b b::::::b -:::::::::::::- C:::::C o::::o o::::om:::::mmm::::::mmm:::::m p:::::p p:::::p l::::l e:::::::eeeee::::::e t:::::t i::::i o::::o o::::o n:::::nnnn:::::n # T:::::T aa::::::::::::a b:::::b b:::::b --------------- C:::::C o::::o o::::om::::m m::::m m::::m p:::::p p:::::p l::::l e:::::::::::::::::e t:::::t i::::i o::::o o::::o n::::n n::::n # T:::::T a::::aaaa::::::a b:::::b b:::::b C:::::C o::::o o::::om::::m m::::m m::::m p:::::p p:::::p l::::l e::::::eeeeeeeeeee t:::::t i::::i o::::o o::::o n::::n n::::n # T:::::T a::::a a:::::a b:::::b b:::::b C:::::C CCCCCCo::::o o::::om::::m m::::m m::::m p:::::p p::::::p l::::l e:::::::e t:::::t tttttt i::::i o::::o o::::o n::::n n::::n # TT:::::::TT a::::a a:::::a b:::::bbbbbb::::::b C:::::CCCCCCCC::::Co:::::ooooo:::::om::::m m::::m m::::m p:::::ppppp:::::::pl::::::le::::::::e t::::::tttt:::::ti::::::io:::::ooooo:::::o n::::n n::::n # T:::::::::T a:::::aaaa::::::a b::::::::::::::::b CC:::::::::::::::Co:::::::::::::::om::::m m::::m m::::m p::::::::::::::::p l::::::l e::::::::eeeeeeee tt::::::::::::::ti::::::io:::::::::::::::o n::::n n::::n # T:::::::::T a::::::::::aa:::ab:::::::::::::::b CCC::::::::::::C oo:::::::::::oo m::::m m::::m m::::m p::::::::::::::pp l::::::l ee:::::::::::::e tt:::::::::::tti::::::i oo:::::::::::oo n::::n n::::n # TTTTTTTTTTT aaaaaaaaaa aaaabbbbbbbbbbbbbbbb CCCCCCCCCCCCC ooooooooooo mmmmmm mmmmmm mmmmmm p::::::pppppppp llllllll eeeeeeeeeeeeee ttttttttttt iiiiiiii ooooooooooo nnnnnn nnnnnn # p:::::p # p:::::p # p:::::::p # p:::::::p # p:::::::p # ppppppppp # Register-ArgumentCompleter can be provided thru TabExpansionPlusPlus or with V5 inbox module. # We don't care much which one it is, but the inbox one doesn't have -Description parameter if (Get-Command -Name Register-ArgumentCompleter -Module TabExpansionPlusPlus -ErrorAction Ignore) { Function ModuleNameCompleter { Param ( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter ) Get-Module -Name "$wordToComplete*" | ForEach-Object { New-CompletionResult -CompletionText $_.Name -ToolTip $_.Description } } Register-ArgumentCompleter -CommandName New-MarkdownHelp -ParameterName Module -ScriptBlock $Function:ModuleNameCompleter -Description 'This argument completer handles the -Module parameter of the New-MarkdownHelp Command.' } elseif (Get-Command -Name Register-ArgumentCompleter -ErrorAction Ignore) { Function ModuleNameCompleter { Param ( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter ) Get-Module -Name "$wordToComplete*" | ForEach-Object { $_.Name } } Register-ArgumentCompleter -CommandName New-MarkdownHelp -ParameterName Module -ScriptBlock $Function:ModuleNameCompleter } #endregion Parameter Auto Completers |