Types/MarkX/Sync.ps1

<#
.SYNOPSIS
    Synchronizes Mark Data
.DESCRIPTION
    Synchronizes a MarkX input into a parsed MarkX file.
.NOTES
    This will read markdown from files, convert them from markdown,
    and attempt to cast the output to XHTML so it can be queried.
#>

$currentRows = @()

$allInput = @($input)
if ($allInput -and -not $this) {
    $this = [PSCustomObject]@{PSTypeName='MarkX'}
    $this | Add-Member NoteProperty '#Input' $allInput -Force
}

$markXProtoType = [PSCustomObject]@{PSTypeName='MarkX'}

$allMarkdown = @(:nextInput foreach ($md in $this.Input) {    
    if ($md -isnot [string]) {
        if ($md -is [PSModuleInfo]) {
            # Since we have already decorated this type,
            # we need to get the underlying module path with reflection
            $modulePath = $md.GetType().GetProperty("Path").GetValue($md)                        
            if (-not $this.Header) {
                $this.Header = [Ordered]@{
                    title = $md.name
                    description = $md.description
                    author = $md.author
                    copyright = $md.copyright
                    companyName = $md.companyName
                    version = $md.version
                    guid = $md.guid
                    licenseUri = $md.licenseUri
                    releaseNotes = $md.releaseNotes
                    projectUri = $md.projectUri
                    privateData = $md.privateData
                }
            }
                        
            @(                    
                "# $($md.name) @ $($md.version)"
                [Environment]::newline
                "## $($md.description)"

                if ($md.privateData.PSData.PSIntro) {
                    [Environment]::newline
                    $md.PrivateData.PSData.PSIntro
                }
                elseif ($md.privateData.PSIntro) {
                    [Environment]::newline
                    $md.PrivateData.PSIntro
                }                
            )

            $md = ($modulePath | Split-Path) -as [IO.DirectoryInfo]
        }

        if ($md -is [IO.DirectoryInfo]) {
            $this |
                Add-Member NoteProperty '#Path' $md.Fullname -Force
            $this.Site = "$($md.FullName)"
            $this.Site.includes = $this.Include
            continue nextInput
        }
        
        # If the markdown was a file
        if ($md -is [IO.FileInfo] -and 
            # and it had a markdown extension
            $md.Extension -in $markXProtoType.MarkdownExtension) {

            $this |
                Add-Member NoteProperty '#Path' $md.Fullname -Force

            $md = Get-Content -LiteralPath $md.Fullname -Raw
            $md
            continue
        }

        if ($md -is [ScriptBlock]) {
            ${function:Lambda λ} = $md
            $md = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Lambda λ', 'Function')
        }
        
        if ($md -is [Management.Automation.CommandInfo]) {
            $cmdHelp = if (
                $md -is [Management.Automation.FunctionInfo] -or
                $md -is [Management.Automation.AliasInfo]
            ) {
                Get-Help -Name $md.Name
            } elseif ($md -is [Management.Automation.ExternalScriptInfo]) {
                $this | Add-Member NoteProperty '#Path' "$($md.Source)" -Force
                Get-Help -Name $md.Source
            } else {
                continue nextInput
            }                        

            $thisScriptBlock = 
                if ($this -is [ScriptBlock]) {
                    $this
                } elseif ($this.ScriptBlock) {
                    $this.ScriptBlock
                }

            if ($thisScriptBlock) {    
                $frontMatter = @(foreach ($attr in $thisScriptBlock.Attributes) {
                    if ($attr -is [Reflection.AssemblyMetaDataAttribute] -and
                        $attr.Key -match 'Front[\s\p{P}]{0,}Matter') {
                        $attr.Value
                    }   
                })
                if ($frontMatter) {
                    $this | Add-Member NoteProperty '#FrontMatter' $frontMatter -Force
                }
            }

            if ($cmdHelp) {
                $md = $cmdHelp
            }
        }

        if ($md.pstypenames -match 'HelpInfo') {
            $helpObject = [Ordered]@{
                title = $(
                    if ($md.Name -match '[\\/]') {
                        "$(@($md.Name -split '[\\/]')[-1] -replace '\.ps1')"
                    } else {
                        "$($md.Name)"
                    }
                )
                synopsis = $md.synopsis
                description = $md.Description.text -join [Environment]::NewLine
                notes = $md.alertset.alert.text -join [Environment]::NewLine
                inputs = $md.inputTypes.inputType.type.name -join [Environment]::NewLine
                outputs = $md.returnValues.returnValue.type.name -join [Environment]::NewLine
                examples = @(
                    foreach ($example in $md.examples.example) {
                        $exampleNumber++
                        # Combine the code and remarks
                        $exampleLines = 
                            @(
                                $example.Code
                                foreach ($remark in $example.Remarks.text) {
                                    if (-not $remark) { continue }
                                    $remark
                                }
                            ) -join ([Environment]::NewLine) -split '(?>\r\n|\n)' # and split into lines

                        # Anything until the first non-comment line is a markdown predicate to the example
                        $nonCommentLine = $false
                        $markdownLines = @()

                        # Go thru each line in the example as part of a loop
                        $codeBlock = @(foreach ($exampleLine in $exampleLines) {
                            # Any comments until the first uncommentedLine are markdown
                            if ($exampleLine -match '^\#' -and -not $nonCommentLine) {
                                $markdownLines += $exampleLine -replace '^\#' -replace '^\s+'
                            } else {
                                $nonCommentLine = $true
                                $exampleLine
                            }
                        }) -join [Environment]::NewLine
                        
                        # Join all of our markdown lines together
                        $markdownLines -join [Environment]::NewLine
                        "~~~PowerShell"
                        $codeBlock
                        "~~~"
                    }
                ) -join [Environment]::NewLine
                parameters = [Ordered]@{}
                source = 
                    if ($this -is [ScriptBlock]) {
                        $this.Ast.ToString()
                    } elseif ($this.ScriptBlock) {
                        "$($this.ScriptBlock.Ast.ToString())"
                    }
            }

            foreach ($param in $md.parameters.parameter) {
                $helpObject.parameters[$param.name] = [Ordered]@{
                    name = $param.Name
                    required = $param.required
                    description = $param.description.text -join [Environment]::NewLine                      
                    aliases = $param.aliases
                    position = $param.position
                    pipeline = $param.pipelineInput                    
                    type = $param.parameterValue
                    wildcard = $param.globbing                    
                }            
            }

            @(                                
                "# $($helpObject.title)"
                
                if ($md.Synopsis) {
                    "## $($md.Synopsis)"
                }
                
                if ($helpObject.description) {
                    "### $($helpObject.description)"
                    [Environment]::NewLine
                }

                if ($helpObject.inputs) {
                    "### Inputs"
                    [Environment]::NewLine
                    $helpObject.inputs
                    [Environment]::NewLine
                }

                if ($helpObject.outputs) {
                    "### Outputs"
                    $helpObject.outputs
                    [Environment]::NewLine
                }

                if ($helpObject.notes) {
                    if (-not ($helpObject.inputs -or $helpObject.outputs)) {
                        "### Notes"
                    }
                    $helpObject.notes
                    [Environment]::NewLine
                }                                
                
                [Environment]::NewLine  
                $helpObject.examples
                [Environment]::NewLine

                if ($helpObject.parameters.Count) {
                    "<details><summary>Parameters</summary>"
                    [Environment]::NewLine

                    foreach ($parameterName in $helpObject.Parameters.Keys) {
                        $parameter = $helpObject.Parameters[$parameterName]
                        "#### $($parameterName)"
                        [Environment]::NewLine
                        $parameter.description 
                        [Environment]::NewLine
                        $properties = 'required', 'type','position', 
                            'pipeline',
                            'wildcard',                            
                            'aliases'
                        "|$($properties -join '|')|"
                        "|$(
                            (@('-') * ($properties.Count)) -join '|'
                        )|"

                        "|$(
                            $parameter[$properties] -join '|'
                        )|"

                        [Environment]::NewLine   
                    }
                    
                    "</details>"
                }

                if ($helpObject.source -and 
                    -not ($helpObject.source -match '(?m)^(?>~~~|```)PowerShell')
                ) {
                    "<details><summary>View Source</summary>"
                    [Environment]::NewLine
                    "~~~PowerShell"
                    $helpObject.Source
                    "~~~"
                    "</details>"
                    [Environment]::NewLine
                }
            ) -join [Environment]::NewLine

            $header = $this.Header

            if (-not $header) {
                $this.Header = $helpObject
            } elseif ($header -isnot [string]) {
                foreach ($key in $helpObject.Keys) {
                    if (-not $header[$key]) {
                        $header[$key] = $helpObject[$key]
                    }
                }
                $this.Header = $header
            }
            continue nextInput
        }        
        if ($md -is [ScriptBlock]) {
            "<pre><code class='language-powershell'>$(
                [Web.HttpUtility]::HtmlEncode(
                    "$md"
                )
            )</code><pre>"

            continue nextInput
        } 
        if ($md -is [Collections.IDictionary] -or 
            ($md.GetType -and 
                (-not $md.GetType().IsPrimitive)
            )  
        ) {            
            $currentRows += $md            
            continue
        }        
    }
    
    if ($currentRows) {    
        $this.ToTable($currentRows)        
        $currentRows = @()
    }

    if ($md -match '(?>\.md|markdown)$' -and
        (
            [IO.File]::Exists("$md") -or 
            (Test-Path $md -ErrorAction Ignore)
        )        
    ) {
        $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($md)
        $this | Add-Member NoteProperty '#Path' "$resolvedPath" -Force
        $md = Get-Content -Raw $md
    }

    $frontMatter = ''
    if ($md -match '^---') {
        $null, $frontMatter, $md = $md -split '---', 3
        if ($frontMatter) {
            $this | Add-Member NoteProperty '#FrontMatter' $frontMatter -Force
        }
    }

    $md
})

if ($currentRows) {    
    $allMarkdown += $this.ToTable($currentRows)
    $currentRows = @()
}

$yamlHeaders = @()
$allMarkdown = @(foreach ($md in $allMarkdown) {    
    if ($md -match '^[\-\+]{3}') {
        $null, $frontMatter, $restOfMakdown = $md -split '[\-\+]{3}', 3
        
        $this | Add-Member NoteProperty '#FrontMatter' $frontMatter -Force

        if ($frontMatter) {
            $yamlHeaders+=$frontMatter
        }
        $restOfMakdown
    } else {
        $md
    }
})

if ($yamlHeaders) {
    $yamlHeader = $yamlHeaders -join (
        [Environment]::NewLine + '---' + [Environment]::NewLine
    )
    $this | Add-Member NoteProperty '#YamlHeader' $yamlHeader -Force
}

$markdown = $allMarkdown -join [Environment]::NewLine

$this | 
    Add-Member NoteProperty '#Markdown' $Markdown -Force

$Markdown = $this.'#Markdown'

if (-not $Markdown) { return }

$mdPipelineBuilder = 
    [Markdig.MarkdownExtensions]::UseYamlFrontMatter(
        [Markdig.MarkdownExtensions]::UseAdvancedExtensions(
            [Markdig.MarkdownPipelineBuilder]::new()
        )
    )

$mdPipeline = $mdPipelineBuilder.Build()

$this | 
    Add-Member NoteProperty '#HTML' (
        [Markdig.Markdown]::ToHtml($markdown, $mdPipeline) -replace '\sdisabled="disabled"'
    ) -Force

$this | 
    Add-Member NoteProperty '#XML' (
        "<xhtml>$($this.'#HTML')</xhtml>" -as [xml]
    ) -Force

if (-not $this.'#XML') { return }

$this.psobject.Properties.Remove('#DataSet')
return $this