MarkX.ps1

<#
.SYNOPSIS
    Gets MarkX
.DESCRIPTION
    Gets MarkX - Markdown as XML
    
    This allows us to query, extract, and customize markdown.
.EXAMPLE
    # 'Hello World' In Markdown / MarkX
    # Hello World' | MarkX
.EXAMPLE
    # MarkX is aliased to Markdown
    # 'Hello World' as Markdown as XML
    '# Hello World' | Markdown | Select -Expand XML
.EXAMPLE
    # We can generate tables by piping in objects
    @{n1=1;n2=2}, @{n1=2;n3=3} | MarkX
.EXAMPLE
    # Make a TimesTable in MarkX
    @(
        "#### TimesTable"
        foreach ($rowN in 1..9) {
            $row = [Ordered]@{}
            foreach ($colN in 1..9) {
                $row["$colN"] = $colN * $rowN
            }
            $row
        }
    ) | Get-MarkX
.EXAMPLE
    # We can pipe a command into MarkX
    # This will get the command help as Markdown
    Get-Command Get-MarkX | MarkX
.EXAMPLE
    # We can pipe help into MarkX
    Get-Help Get-MarkX | MarkX
.EXAMPLE
    # We can get code from markdown
    Get-Help Get-MarkX |
        MarkX |
        Select-Object -ExpandProperty Code
.INPUTS
    `[Object]`

    This accepts any object from the pipeline and any number of arguments
.OUTPUTS
    Directory inputs will become a site.

    File inputs will become a `MarkX` object.
.NOTES
    MarkX can be attached to any type.

    All we need to do is add `MarkX` to the `.pstypenames` of an object.

    This allows the object to maintain it's underlying capabilties.

    * A `[IO.FileInfo]` is still a `[IO.FileInfo]`.
    * A `[IO.DirectoryInfo]` is a still a `[IO.DirectoryInfo]`.
    * A `[ScriptBlock]` is still a `[ScriptBlock]`.
    * A `[Management.Automation.CommandInfo]` is still a `[Mangement.Automation.CommandInfo]`.
    * A `[Management.Automation.PSModuleInfo]` is still a `[Mangement.Automation.PSModuleInfo]`.
#>

[Alias('Markdown','Get-Markdown')]
param()

# Collect all input and arguments.
# This function treats all input and arguments as interchangeable.
$allInput = @($input) + $(if ($args) {
    $args
})

# We could have been provided a list of markdown files
# or markdown itself.

# So we need to make a pass over each input
$remainingInput = @()
$inputObjects = @()

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

:nextInput foreach ($in in $AllInput) {
    if (
        $in -is [Management.Automation.CommandInfo] -or
        $in -is [Management.Automation.PSModuleInfo]
    ) {
        $inputObjects += $in
        continue nextInput
    }

    if ($in -is [Management.Automation.PathInfo]) {
        $in = "$in"
    }
    # If the input starts as slashes
    $inFile = if ($in -match '^\.[\\/]') {
        # get the unresolved path and attempt to cast it to a file
        $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($in) -as [IO.FileInfo]
    } else {
        # otherwise, attempt to cast the input to a file
        $in -as [IO.FileInfo]
    }

    # If the file was empty (or null)
    if (-not $inFile.Length) {
        # check to see if it is a directory.
        if ([IO.Directory]::Exists("$inFile")) {
            # If it was, we will shapeshift the directory into MarkX
            $inputObjects += [IO.DirectoryInfo]"$inFile"            
            continue nextInput  
        } else {

            # we will treat it as plain markdown
            $remainingInput += $in
            continue nextInput # and should continue.
        }        
    }
    
    
    # If the file does not exist
    if (-not [IO.File]::Exists($inFile.FullName)) {
        # we will treat it as markdown
        $remainingInput += $in
        continue # and should continue.
    }

    if ($inFile.Extension -eq '.ps1') {                
        $in = $ExecutionContext.SessionState.InvokeCommand.GetCommand($inFile.FullName, 'ExternalScript')        
        $inputObjects += $in        
        continue nextInput
    }
    
    # If the file was not markdown or mdx.
    if ($inFile.Extension -notin $markXProtoType.MarkdownExtension) {
        # we will treat the input as markdown
        $remainingInput += $in
        continue # and should continue.
    }    

    $inputObjects += $inFile    
}

# If we have a number of input files,
# we will want to show progress
$progress = [Ordered]@{id=[random]::new().Next()}
$progress.Activity = "Converting Markdown"

# Walk over each of our input files
for ($inputNumber = 0; $inputNumber -lt $inputObjects.Count; $inputNumber++) {

    $inputObject = $inputObjects[$inputNumber]
    # determine percent complete
    $progress.PercentComplete = $inputNumber * 100 / $inputObjects.Length
    $progress.Status = $inputObject.Name
    # and write a progress message.
    Write-Progress @progress

    if (
        $inputObject -is [IO.DirectoryInfo] -or
        $inputObject -is [IO.FileInfo] -or 
        $inputObject -is [Management.Automation.CommandInfo] -or
        $inputObject -is [Management.Automation.PSModuleInfo] -or
        $inputObject -is [ScriptBlock] 
    ) {
        if ($inputObject -is [IO.FileInfo] -and 
            $inputObject.Extension -eq '.ps1') {
            $inputObject = $ExecutionContext.SessionState.InvokeCommand.GetCommand($inputObject.FullName,'ExternalScript')            
        }

        if ($inputObject -is [Management.Automation.ExternalScriptInfo] -and -not $inputObject.ScriptBlock) {
            continue
        }
        $inputObject.pstypenames.insert(0,'MarkX')
        $inputObject.Input = $inputObject
        $inputObject |            
            Add-Member NoteProperty Invocation $MyInvocation -Force -PassThru
    }    
    else {
        # Then, create a new MarkX object
        $markx = New-Object PSObject -Property @{
            PSTypeName = 'MarkX'
            Invocation = $MyInvocation
        }
        # set its input to the input file.
        $markx.Input = $inputObjects[$inputNumber]
        # and output the object
        $markx
    }    
}

# If we had any input
if ($inputNumber) {
    # We need to complete our progress bars.
    $progress.Remove('PercentComplete')
    $progress.Completed = $true
    Write-Progress @progress
}

# If there was no remaining input, return
if (-not $remainingInput.Length) { return }
# Otherwise, create a property bag for the Markdown.
$markx = New-Object PSObject -Property @{
    PSTypeName = 'MarkX'
    Invocation = $MyInvocation
}
# set its input to all remaining input
$markx.Input = $remainingInput
# and then output the MarkX / Markdown
return $markx