bin/PSHelpXml.ps1

#Requires -Assembly 'System.Xml'

#region public
# . $PSScriptRoot\PublicFunction.ps1
#endregion public

#region Add-HelpItemNode
function Script:Add-Paragraph {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [Alias("i", "Text", "Content")]
        [string[]]
        $InputObject,

        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Xml.XmlElement]
        $Root
    )

    process {
        foreach ($_ in $InputObject) {
            $Root | Add-ChildNode "maml:para" -InnerText $_ -OutNull
        }
    }
}

function Script:Add-Description {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [Alias("i", "Text", "Content")]
        [string[]]
        $InputObject = @(),

        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Xml.XmlElement]
        $Root
    )

    begin {
        Write-Verbose "Creat xml element - maml:description"
    }

    process {
        $Root |
            Add-ChildNode "maml:description" |
            Script:Add-Paragraph $InputObject
    }
}

function Script:Add-Details {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [string]
        $CommandName,

        [Parameter(Position = 1)]
        [string[]]
        $Description,

        [Parameter(Position = 2)]
        [string[]]
        $Copyright,

        [Parameter(Position = 3)]
        [string[]]
        $Version,

        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Xml.XmlElement]
        $Root
    )

    begin {
        Write-Verbose "Creat xml element - command:details"
    }

    process {
        $NameCache = $CommandName.Trim().Split(" -", [System.StringSplitOptions]1)
        $DetailsNode = $Root | Add-ChildNode "command:details"

        $DetailsNode | Add-ChildNode "command:name" $($NameCache -join "-") -OutNull
        $DetailsNode | Script:Add-Description $Description
        $DetailsNode |
            Add-ChildNode "maml:copyright" |
            Script:Add-Paragraph $Copyright
        $DetailsNode |
            Add-ChildNode "command:version" |
            Script:Add-Paragraph $Version
        $DetailsNode | Add-ChildNode "command:verb" $NameCache[0] -OutNull
        $DetailsNode | Add-ChildNode "command:noun" $NameCache[1] -OutNull
    }
}

function Script:Add-SyntaxItem {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [string]
        $Name,

        [Parameter(Position = 1)]
        [hashtable[]]
        $Parameters,

        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Xml.XmlElement]
        $Root
    )

    begin {
        Write-Verbose "Creat xml element - command:syntax"
    }

    process {
        $SyntaxItemNode = $Root | Add-ChildNode "command:syntaxItem"
        $SyntaxItemNode | Add-ChildNode "maml:name" $Name -OutNull

        foreach ($item in $Parameters) {
            $SyntaxItemNode | Script:Add-Parameter @item
        }
    }
}

function Script:Add-Parameter {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [string]
        $ParamName,

        [Parameter(Position = 1)]
        [Alias("Values")]
        [string[]]
        $ParamValues,

        [Parameter(Position = 2)]
        [Alias("Mandatory")]
        [bool]
        $Required,

        [Parameter(Position = 3)]
        [Alias("Wild", "Wildcard", "WildcardChar")]
        [bool]
        $Globbing,

        [Parameter(Position = 4)]
        [Alias("Pipeline")]
        [bool]
        $PipelineInput,

        [Parameter(Position = 5)]
        [ValidatePattern("^\d+|named$")]
        [string]
        $Position = "named",

        [Parameter(Position = 6)]
        [Alias("Default")]
        [string]
        $DefaultValue = "None",

        [Parameter(Position = 6)]
        [string[]]
        $Description,

        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Xml.XmlElement]
        $Root
    )

    begin {
        Write-Verbose "Creat xml element - command:parameter"
    }

    process {
        $ParamNode = $Root | Add-ChildNode "command:parameter"
        $ParamNode.SetAttribute("required", $Required.ToString().ToLower())
        $ParamNode.SetAttribute("position", $Position.ToLower())
        $ParamNode.SetAttribute("globbing", $Globbing.ToString().ToLower())
        $ParamNode.SetAttribute("pipelineInput", $PipelineInput.ToString().ToLower())
        $ParamNode.SetAttribute("variableLength", "true")

        $ParamNode | Add-ChildNode "maml:name" $ParamName -OutNull
        $ParamNode | Script:Add-Description $Description

        if ($ParamValues.Count -eq 1) {
            $NextNode = $ParamNode
        }
        elseif ($ParamValues.Count -gt 1) {
            $NextNode = $ParamNode | Add-ChildNode "command:parameterValueGroup"
        }

        if ($ParamValues.Count -gt 0) {
            foreach ($value in $ParamValues) {
                $ParamValueNode = $NextNode | Add-ChildNode "command:parameterValue" $value
                $ParamValueNode.SetAttribute("required", "true")
                $ParamValueNode.SetAttribute("variableLength", "false")
            }
        }
    }
}

function Script:Add-InputType {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [Alias("Name")]
        [string]
        $TypeName,

        [Parameter(Position = 1)]
        [Alias("Link")]
        [string]
        $Uri,

        [Parameter(Position = 2)]
        [string[]]
        $Description,

        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Xml.XmlElement]
        $Root,

        [Parameter()]
        [switch]$IsReturnValue
    )

    begin {
        $NodeName = if ($IsReturnValue) { "returnValue" } else { "inputType" }
        Write-Verbose "Creat xml element - command:$NodeName"
    }

    process {
        $InputTypeNode = $Root | Add-ChildNode "command:$NodeName"

        $TypeNode = $InputTypeNode | Add-ChildNode "dev:type"
        $TypeNode | Add-ChildNode "maml:name" $TypeName -OutNull
        $TypeNode | Add-ChildNode "maml:uri" $Uri -OutNull

        $InputTypeNode | Script:Add-Description $Description
    }
}

function Script:Add-Example {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [string]
        $Title,

        [Parameter(Position = 1)]
        [Alias("Intro")]
        [string[]]
        $Introduction,

        [Parameter(Position = 2)]
        [string[]]
        $Code,

        [Parameter(Position = 3)]
        [string[]]
        $Remarks,

        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Xml.XmlElement]
        $Root
    )

    begin {
        Write-Verbose "Creat xml element - command:example"
    }

    process {
        $ExampleNode = $Root | Add-ChildNode "command:example"

        $ExampleNode | Add-ChildNode "maml:title" $Title -OutNull
        $ExampleNode |
            Add-ChildNode "maml:introduction" |
            Script:Add-Paragraph $Introduction
        $ExampleNode |
            Add-ChildNode "dev:code" -InnerText ($Code -join " ") -OutNull
        $ExampleNode |
            Add-ChildNode "dev:remarks" |
            Script:Add-Paragraph $Remarks
    }
}

function Script:Add-RelatedLink {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [Alias("Text")]
        [string]
        $LinkText,

        [Parameter(Position = 1)]
        [string]
        $Uri,

        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Xml.XmlElement]
        $Root,

        [switch]$PassThru
    )

    begin {
        Write-Verbose "Creat xml element - maml:navigationLink"
    }

    process {
        $LinkNode = $Root | Add-ChildNode "maml:navigationLink"
        $LinkNode | Add-ChildNode "maml:linkText" $LinkText -OutNull
        $LinkNode | Add-ChildNode "maml:uri" $Uri -OutNull

        if ($PassThru) {
            Write-Output $LinkNode
        }
    }
}

function Script:Add-CommandNode {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [hashtable]
        $Details,

        [Parameter(Position = 1)]
        [string[]]
        $Description,

        [Parameter(Position = 2)]
        [hashtable[]]
        $Syntax,

        [Parameter(Position = 3)]
        [hashtable[]]
        $Parameters,

        [Parameter(Position = 4)]
        [hashtable[]]
        $InputTypes,

        [Parameter(Position = 5)]
        [hashtable[]]
        $ReturnValues,

        [Parameter(Position = 6)]
        [hashtable[]]
        $Examples,

        [Parameter(Position = 7)]
        [hashtable[]]
        $RelatedLinks,

        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Xml.XmlElement]
        $Root
    )

    begin {
        Write-Verbose "Creat xml element - command:command"
    }

    process {
        $CommandNode = $Root | Add-ChildNode "command:command"
        $CommandNode.SetAttribute("xmlns:maml", "http://schemas.microsoft.com/maml/2004/10")
        $CommandNode.SetAttribute("xmlns:command", "http://schemas.microsoft.com/maml/dev/command/2004/10")
        $CommandNode.SetAttribute("xmlns:dev", "http://schemas.microsoft.com/maml/dev/2004/10")

        $CommandNode | Script:Add-Details @Details

        if ($null -eq $Description -and $Details.Keys -contains "Description") {
            $Description = $Details.Description
        }
        $CommandNode | Script:Add-Description $Description

        $SyntaxNode = $CommandNode | Add-ChildNode "command:syntax"
        foreach ($syntaxItem in $Syntax) {
            $SyntaxNode | Script:Add-SyntaxItem @syntaxItem
        }

        $ParamsNode = $CommandNode | Add-ChildNode "command:parameters"
        foreach ($param in $Parameters) {
            $ParamsNode | Script:Add-Parameter @param
        }

        $InputTypesNode = $CommandNode | Add-ChildNode "command:inputTypes"
        foreach ($input in $InputTypes) {
            $InputTypesNode | Script:Add-InputType @input
        }

        $ReturnValuesNode = $CommandNode | Add-ChildNode "command:returnValues"
        foreach ($return in $ReturnValues) {
            $ReturnValuesNode | Script:Add-InputType @return -IsReturnValue
        }

        $CommandNode | Add-ChildNode "command:terminatingErrors" -OutNull
        $CommandNode | Add-ChildNode "command:nonTerminatingErrors" -OutNull

        $ExamplesNode = $CommandNode | Add-ChildNode "command:examples"
        foreach ($example in $Examples) {
            $ExamplesNode | Script:Add-Example @example
        }

        $RelatedLinksNode = $CommandNode | Add-ChildNode "command:relatedLinks"
        foreach ($link in $RelatedLinks) {
            $RelatedLinksNode | Script:Add-RelatedLink @link
        }
    }
}

function Add-HelpItemNode {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [Alias("Cmds")]
        [hashtable[]]
        $Commands,

        [Parameter(Position = 1)]
        [string]
        $OutXml
    )

    Write-Verbose "Start converting helpItem"
    $Script:doc = [xml]::new()
    $null = $Script:doc.AppendChild($Script:doc.CreateXmlDeclaration("1.0", "utf-8", $null))
    $helpItems = $Script:doc.AppendChild($Script:doc.CreateElement("helpItems"))
    $helpItems.SetAttribute("xmlns", "http://msh")
    $helpItems.SetAttribute("schema", "maml")

    foreach ($_ in $Commands) {
        $helpItems | Script:Add-CommandNode @_
    }

    if (-not [string]::IsNullOrEmpty($OutXml)) {
        $Script:doc.Save($OutXml)
        Write-Verbose "Save in $OutXml"
    }
}
#endregion Add-HelpItemNode

#region ConvertTo-PSHelpXml
function Script:ConvertTo-Syntax {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]
        $InputObject
    )

    process {
        $InputObject = $InputObject -replace "\s*\n\s*(?=\[*-)", " "
        [string[]]$Syntaxs = $InputObject.Split("`n", [StringSplitOptions]1) |
            ForEach-Object { $_ -replace "\s+(?!\[*-)", "" }
        foreach ($_ in $Syntaxs) {
            $Parameters = $_ -split "\s+(?=\[{0,2}-)"
            $Name = $Parameters[0]
            $Parameters = [string[]]($Parameters | Select-Object -Skip 1)
            $Params =
            for ($i = 0; $i -lt $Parameters.Count; $i++) {
                $Values = $($Parameters[$i] -replace "\[{0,2}-(\w+)\]?([<{]?(\S+)[>}])?\]?", '$1-$3').Split(
                    "-|", [System.StringSplitOptions]1)

                [hashtable]$Param = @{
                    ParamName   = $Values[0]
                    ParamValues = $Values | Select-Object -Skip 1
                }

                $Param.Required = $Parameters[$i] -match "^\[?-\w+\]?$|[>}]$"
                if ($Parameters[$i] -match "\[-\w+\]") {
                    $Param.Position = $i
                }

                Write-Output $Param
            }

            Write-Output @{
                Name       = $Name
                Parameters = $Params
            }
        }
    }
}

function Script:ConvertTo-Parameters {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [AllowEmptyString()]
        [string[]]
        $InputObject
    )

    process {
        if ($null -eq $InputObject) {
            return
        }

        $InputObject = $InputObject | Where-Object { $_ }
        $InputObject =
        foreach ($item in $InputObject) {
            $item = $item.TrimStart()
            switch ($item[0]) {
                "-" {
                    $Cache = $item.Split("-<> ", [System.StringSplitOptions]1)
                    "Parameters"
                    " ParamName : $($Cache[0])"
                    " ParamValues : $($Cache[1])"
                }
                "#" { " Description : $item" }
                Default { " $item" }
            }
        }

        $(Resolve-PSIndentSyntax $InputObject $null -Recurse).Parameters
    }
}

function Script:ConvertTo-Examples {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [AllowEmptyString()]
        [string[]]
        $InputObject
    )

    process {
        if ($null -eq $InputObject) {
            return
        }

        $InputObject =
        foreach ($item in $InputObject) {
            switch -Regex ($item) {
                "^\S" {
                    "Examples"
                    " Title : $item"
                }
                "^\s+(?<Intro>PS\s[^>]+>)\s*(?<Code>.*)" {
                    " Introduction : $($Matches.Intro)"
                    " Code : $($Matches.Code -replace '^+\s*', '')"
                }
                "^\s+\+\s" {
                    " Code : $($item.TrimStart())"
                }
                Default {
                    " Remarks : $($item.TrimStart())"
                }
            }
        }

        $(Resolve-PSIndentSyntax $InputObject $null -Recurse).Examples
    }
}

function Script:ConvertTo-CommandNode {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, ValueFromPipeline)]
        [Alias("Contents", "Texts")]
        [AllowEmptyString()]
        [string[]]
        $InputObject
    )

    process {
        $Hash = Resolve-PSIndentSyntax $InputObject -CommentPattern $null

        [Ordered]@{
            Details      = @{
                CommandName = $Hash.Synopsis[0]
                Description = $Hash.Synopsis | Select-Object -Skip 1
            }
            Description  = $Hash.Description
            Syntax       = Script:ConvertTo-Syntax $($Hash.Syntax -join "`n")
            Parameters   = Script:ConvertTo-Parameters $Hash.Parameters
            InputTypes   = @{
                TypeName    = $Hash.Inputs[0]
                Description = $Hash.Inputs | Select-Object -Skip 1
            }
            ReturnValues = @{
                TypeName    = $Hash.Outputs[0]
                Description = $Hash.Outputs | Select-Object -Skip 1
            }
            Examples     = Script:ConvertTo-Examples $Hash.Examples
            RelatedLinks = foreach ($_ in $Hash.RelatedLinks) {
                $link = $_.Split(": ", 2, [System.StringSplitOptions]1)
                @{ LinkText = $link[0]; Uri = $link[1] }
            }
        }
    }
}

function ConvertTo-PSHelpXml {
    [CmdletBinding(DefaultParameterSetName = "LoadFile")]
    param (
        [Parameter(Position = 0, ValueFromPipeline, ParameterSetName = "Load")]
        [string[]]
        $InputObject,

        [Parameter(Position = 0, ValueFromPipeline, ParameterSetName = "LoadFile")]
        [string]
        $Path,

        [Parameter(Position = 1, ValueFromPipeline, ParameterSetName = "LoadFile")]
        [Microsoft.PowerShell.Commands.FileSystemCmdletProviderEncoding]
        $Encoding = "UTF8",

        [Parameter(ParameterSetName = "Load")]
        [Parameter(ParameterSetName = "LoadFile")]
        [Alias("Out")]
        [string]
        $OutXml = $($Path -replace "\.\w+$", ".xml")
    )

    if ($($PSCmdlet.ParameterSetName -eq "LoadFile") -and $(Test-Path $Path)) {
        $InputObject =
        if ($Path.EndsWith(".ps1")) {
            [string[]](& $Path)
        }
        else {
            $(Get-Content $Path -Raw -Encoding $Encoding) -split "^(?=\.Synopsis)", 0, "Multiline" |
                Where-Object { $_ }
        }
    }

    $Commands =
    foreach ($item in $InputObject) {
        Script:ConvertTo-CommandNode $item.Split("`n")
    }
    Add-HelpItemNode -Commands $Commands -OutXml $OutXml
}
#endregion ConvertTo-PSHelpXml