Markdown.psm1

[CmdletBinding()]
param()
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)
$script:PSModuleInfo = Test-ModuleManifest -Path "$PSScriptRoot\$baseName.psd1"
$script:PSModuleInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ }
$scriptName = $script:PSModuleInfo.Name
Write-Debug "[$scriptName] - Importing module"
#region [functions] - [public]
Write-Debug "[$scriptName] - [functions] - [public] - Processing folder"
#region [functions] - [public] - [Set-MarkdownCodeBlock]
Write-Debug "[$scriptName] - [functions] - [public] - [Set-MarkdownCodeBlock] - Importing"
function Set-MarkdownCodeBlock {
    <#
        .SYNOPSIS
        Generates a fenced code block for Markdown using the specified language.

        .DESCRIPTION
        This function takes a programming language and a script block, captures the script block’s contents,
        normalizes the indentation, removes the outer braces (if present) and then formats it as a fenced code
        block suitable for Markdown.

        .EXAMPLE
        Set-MarkdownCodeBlock -Language 'powershell' -Content {
            Get-Process
        }

        Output:
        ```powershell
        Get-Process
        ```

        Generates a fenced code block with the specified PowerShell script.

        .EXAMPLE
        CodeBlock 'powershell' {
            Get-Process
        }

        Output:
        ```powershell
        Get-Process
        ```

        Generates a fenced code block with the specified PowerShell script.

        .OUTPUTS
        string

        .NOTES
        Returns the formatted fenced code block as a string.

        .LINK
        https://psmodule.io/Markdown/Functions/Set-MarkdownCodeBlock/
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Sets text in memory'
    )]
    [Alias('Block')]
    [Alias('CodeBlock')]
    [Alias('Fence')]
    [Alias('CodeFence')]
    [OutputType([string])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string]$Language,

        [Parameter(Mandatory, Position = 1)]
        [scriptblock]$Content
    )

    # Capture the raw text of the script block
    $raw = $Content.Ast.Extent.Text
    $lines = $raw -split "`r?`n"

    # Remove leading and trailing blank lines
    while ($lines.Count -gt 0 -and $lines[0].Trim() -eq '') {
        $lines = $lines[1..($lines.Count - 1)]
    }
    while ($lines.Count -gt 0 -and $lines[-1].Trim() -eq '') {
        $lines = $lines[0..($lines.Count - 2)]
    }

    # If the first and last lines are only '{' and '}', remove them.
    if ($lines.Count -ge 2 -and $lines[0].Trim() -eq '{' -and $lines[-1].Trim() -eq '}') {
        $lines = $lines[1..($lines.Count - 2)]
    }

    # Determine common leading whitespace (indentation) on non-empty lines
    $nonEmpty = $lines | Where-Object { $_.Trim().Length -gt 0 }
    if ($nonEmpty) {
        $commonIndent = ($nonEmpty | ForEach-Object {
                $_.Length - $_.TrimStart().Length
            } | Measure-Object -Minimum).Minimum

        # Remove the common indent from each line
        $lines = $lines | ForEach-Object {
            if ($_.Length -ge $commonIndent) { $_.Substring($commonIndent) } else { $_ }
        }
    }

    $return = @()
    $return += '```{0}' -f $Language
    $return += $lines
    $return += '```'
    $return += ''

    $return -join [Environment]::NewLine
}
Write-Debug "[$scriptName] - [functions] - [public] - [Set-MarkdownCodeBlock] - Done"
#endregion [functions] - [public] - [Set-MarkdownCodeBlock]
#region [functions] - [public] - [Set-MarkdownDetails]
Write-Debug "[$scriptName] - [functions] - [public] - [Set-MarkdownDetails] - Importing"
function Set-MarkdownDetails {
    <#
        .SYNOPSIS
        Generates a collapsible Markdown details block.

        .DESCRIPTION
        This function creates a collapsible Markdown `<details>` block with a summary title
        and formatted content. It captures the output of the provided script block and
        wraps it in a Markdown details structure.

        .EXAMPLE
        Set-MarkdownDetails -Title 'More Information' -Content {
            'This is detailed content.'
        }

        Output:
        ```powershell
        <details><summary>More Information</summary>
        <p>

        This is detailed content.

        </p>
        </details>
        ```

        Generates a Markdown details block with the title "More Information" and the specified content.

        .EXAMPLE
        Details 'More Information' {
            'This is detailed content.'
        }

        Output:
        ```powershell
        <details><summary>More Information</summary>
        <p>

        This is detailed content.

        </p>
        </details>
        ```

        Generates a Markdown details block with the title "More Information" and the specified content.

        .OUTPUTS
        string

        .NOTES
        Returns the formatted Markdown details block as a string.

        .LINK
        https://psmodule.io/Markdown/Functions/Set-MarkdownDetails/
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Sets text in memory'
    )]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseSingularNouns', '',
        Justification = 'Markdown details are a collection of information. Language specific'
    )]
    [Alias('Details')]
    [OutputType([string])]
    [CmdletBinding()]
    param (
        # The title of the Markdown details block.
        [Parameter(Mandatory, Position = 0)]
        [string] $Title,

        # The content inside the Markdown details block.
        [Parameter(Mandatory, Position = 1)]
        [ScriptBlock] $Content
    )

    $captured = . $Content | Out-String
    $captured = $captured.TrimEnd()

    $return = @()
    $return += "<details><summary>$Title</summary>"
    $return += '<p>'
    $return += ''
    $return += $captured
    $return += ''
    $return += '</p>'
    $return += '</details>'
    $return += ''

    $return -join [System.Environment]::NewLine
}
Write-Debug "[$scriptName] - [functions] - [public] - [Set-MarkdownDetails] - Done"
#endregion [functions] - [public] - [Set-MarkdownDetails]
#region [functions] - [public] - [Set-MarkdownSection]
Write-Debug "[$scriptName] - [functions] - [public] - [Set-MarkdownSection] - Importing"
function Set-MarkdownSection {
    <#
        .SYNOPSIS
        Generates a formatted Markdown section with a specified header level, title, and content.

        .DESCRIPTION
        This function creates a Markdown section with a specified header level, title, and formatted content.
        The header level determines the number of `#` symbols used for the Markdown heading.
        The content is provided as a script block and executed within the function.
        The function returns the formatted Markdown as a string.

        .EXAMPLE
        Set-MarkdownSection -Level 2 -Title "Example Section" -Content {
            "This is an example of Markdown content."
        }

        Output:
        ```powershell
        ## Example Section

        This is an example of Markdown content.
        ```

        Generates a Markdown section with an H2 heading and the given content.

        .EXAMPLE
        Section 2 "Example Section" {
            "This is an example of Markdown content."
        }

        Output:
        ```powershell
        ## Example Section

        This is an example of Markdown content.
        ```

        Generates a Markdown section with an H2 heading and the given content.

        .OUTPUTS
        string

        .NOTES
        The formatted Markdown section as a string.

        .LINK
        https://psmodule.io/Markdown/Functions/Set-MarkdownSection
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Sets text in memory'
    )]
    [Alias('Header')]
    [Alias('Heading')]
    [Alias('Section')]
    [OutputType([string])]
    [CmdletBinding()]
    param(
        # Specifies the Markdown header level (1-6).
        [Parameter(Mandatory, Position = 0)]
        [ValidateRange(1, 6)]
        [int] $Level,

        # The title of the Markdown section.
        [Parameter(Mandatory, Position = 1)]
        [string] $Title,

        # The content to be included in the Markdown section.
        [Parameter(Mandatory, Position = 2)]
        [scriptblock] $Content
    )

    $captured = . $Content | Out-String
    $captured = $captured.TrimEnd()

    # Create the Markdown header by repeating the '#' character
    $hashes = '#' * $Level

    $return = @()
    $return += "$hashes $Title"
    $return += ''
    $return += $captured
    $return += ''

    $return -join [System.Environment]::NewLine
}
Write-Debug "[$scriptName] - [functions] - [public] - [Set-MarkdownSection] - Done"
#endregion [functions] - [public] - [Set-MarkdownSection]
#region [functions] - [public] - [Set-MarkdownTable]
Write-Debug "[$scriptName] - [functions] - [public] - [Set-MarkdownTable] - Importing"
function Set-MarkdownTable {
    <#
        .SYNOPSIS
        Converts objects from a script block into a Markdown table.

        .DESCRIPTION
        The Set-MarkdownTable function executes a provided script block and formats the resulting objects as a Markdown table.
        Each property of the objects becomes a column, and each object becomes a row in the table. If no objects are returned,
        a warning is displayed, and no output is produced.

        .EXAMPLE
        Table {
            Get-Process | Select-Object -First 3 Name, ID
        }

        Output:
        ```powershell
        | Name | ID |
        | ---- | -- |
        | notepad | 1234 |
        | explorer | 5678 |
        | chrome | 91011 |
        ```

        Generates a Markdown table from the first three processes, displaying their Name and ID properties.

        .OUTPUTS
        string

        .NOTES
        The Markdown-formatted table as a string output.

        This function returns a Markdown-formatted table string, which can be used in documentation or exported.

        .LINK
        https://psmodule.io/Markdown/Functions/Set-MarkdownTable/
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Sets text in memory'
    )]
    [Alias('Table')]
    [OutputType([string])]
    [CmdletBinding()]
    param (
        # Script block containing commands whose output will be converted into a Markdown table.
        [Parameter(Mandatory, Position = 0)]
        [ScriptBlock] $InputScriptBlock
    )

    # Execute the script block and capture the output objects.
    $results = & $InputScriptBlock

    if (-not $results) {
        Write-Warning 'No objects to display.'
        return
    }

    # Use the first object to get the property names.
    $first = $results | Select-Object -First 1
    $props = $first.psobject.Properties.Name

    # Build the Markdown header row.
    $header = '| ' + ($props -join ' | ') + ' |'
    # Build the separator row.
    $separator = '| ' + ( ($props | ForEach-Object { '-' }) -join ' | ' ) + ' |'

    # Output header rows.
    $return = @()
    $return += $header
    $return += $separator

    # For each object, output a table row.
    foreach ($item in $results) {
        $rowValues = foreach ($prop in $props) {
            $val = $item.$prop
            if ($null -eq $val) { '' } else { $val.ToString() }
        }
        $row = '| ' + ($rowValues -join ' | ') + ' |'
        $return += $row
    }
    $return += ''

    $return -join [Environment]::NewLine
}
Write-Debug "[$scriptName] - [functions] - [public] - [Set-MarkdownTable] - Done"
#endregion [functions] - [public] - [Set-MarkdownTable]
Write-Debug "[$scriptName] - [functions] - [public] - Done"
#endregion [functions] - [public]

#region Member exporter
$exports = @{
    Alias    = '*'
    Cmdlet   = ''
    Function = @(
        'Set-MarkdownCodeBlock'
        'Set-MarkdownDetails'
        'Set-MarkdownSection'
        'Set-MarkdownTable'
    )
    Variable = ''
}
Export-ModuleMember @exports
#endregion Member exporter