Src/Plugins/Text/Text.ps1

function ConvertTo-AlignedString
{
<#
    .SYNOPSIS
        Justifies and indents a block of text using the specified alignment properties.
#>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Object[]] $InputObject,

        ## Tab indents
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateRange(0,10)]
        [System.Int32] $Tabs = 0,

        ## Tab size
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateRange(0,10)]
        [System.Int32] $TabSize = 4,

        ## Text alignment
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('Left','Center','Right','Justify')]
        [string] $Align = 'Left',

        ## Text width
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $Width = ($Host.UI.RawUI.BufferSize.Width -1),

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $NoNewLine
    )
    begin
    {
        if ($PSBoundParameters.ContainsKey('Debug'))
        {
            $DebugPreference = 'Continue'
        }
    }
    process
    {
        $tabSpaces = ''.PadRight(($Tabs * $TabSize), ' ')
        $usableWidth = $Width - ($Tabs * $TabSize)

        [System.Text.StringBuilder] $textBuilder = New-Object -TypeName 'System.Text.StringBuilder'
        [System.Text.StringBuilder] $lineBuilder = New-Object -TypeName 'System.Text.StringBuilder'

        foreach ($line in ($InputObject -split '\r\n?|\n'))
        {
            $currentPosition = 0
            [ref] $null = $lineBuilder.Clear()

            $convertToJustifiedStringParams = @{
                Align       = $Align
                Width       = $usableWidth
            }

            ## Break on spaces and then at maximum "usable" length
            [System.String[]] $words = $line -split '\s' -split "(.{$usableWidth})" |
                        Where-Object { -not [System.String]::IsNullOrEmpty($_) }

            for ($w = 0; $w -lt $words.Count; $w++)
            {
                $word = $words[$w]
                $isLastWord = $w -eq ($words.Count -1)

                Write-Debug "Word: '$word', Length: $($word.Length), IsLastWord: $isLastWord"
                $newPosition = $currentPosition + $word.Length

                if ($currentPosition -gt 0)
                {
                    ## We need to prefix the word with a space if we're not at the start of the line
                    $newPosition += 1
                }

                if ($newPosition -lt $usableWidth)
                {
                    ## We haven't reached the end of the line, so just append
                    if ($currentPosition -gt 0)
                    {
                        ## We're not at the start of the line, so prefix word with a space
                        [ref] $null = $lineBuilder.Append(' ')
                    }

                    [ref] $null = $lineBuilder.Append($word)

                    if ($isLastWord)
                    {
                        ## Output what we have thus far
                        $convertToJustifiedStringParams['InputObject'] = $lineBuilder.ToString()
                        $justifiedString = ConvertTo-JustifiedString @convertToJustifiedStringParams
                        [ref] $null = $textBuilder.Append($tabSpaces).Append($justifiedString)

                        if (-not $NoNewLine)
                        {
                            [ref] $null = $textBuilder.AppendLine()
                        }
                    }

                    $currentPosition = $newPosition
                }
                elseif ($newPosition -eq $usableWidth)
                {
                    ## We're bang on the end of the line, therefore can't justify
                    if ($currentPosition -gt 0)
                    {
                        [ref] $null = $lineBuilder.Append(' ')
                    }

                    [ref] $null = $lineBuilder.Append($word)
                    [ref] $null = $textBuilder.Append($tabSpaces).Append($lineBuilder.ToString())

                    if ($isLastWord -and (-not $NoNewLine))
                    {
                        [ref] $null = $textBuilder.AppendLine()
                    }
                    elseif (-not $isLastWord)
                    {
                        [ref] $null = $textBuilder.AppendLine()
                    }

                    [ref] $null = $lineBuilder.Clear()
                    $currentPosition = 0
                }
                else
                {
                    ## We're over the end of the line, so justify and start a new line
                    $convertToJustifiedStringParams['InputObject'] = $lineBuilder.ToString()
                    $justifiedString = ConvertTo-JustifiedString @convertToJustifiedStringParams
                    [ref] $null = $textBuilder.Append($tabSpaces).Append($justifiedString)

                    if ($isLastWord)
                    {
                        ## Just a single word on a new line
                        $convertToJustifiedStringParams['InputObject'] = $word
                        $justifiedString = ConvertTo-JustifiedString @convertToJustifiedStringParams
                        [ref] $null = $textBuilder.AppendLine().Append($tabSpaces).Append($justifiedString)

                        if (-not $NoNewLine)
                        {
                            [ref] $null = $textBuilder.AppendLine()
                        }
                    }
                    else
                    {
                        ## Start a new line
                        [ref] $null = $textBuilder.AppendLine()
                        [ref] $null = $lineBuilder.Clear().Append($word)
                        $currentPosition = $word.Length
                    }
                }
            }
        }
        return $textBuilder.ToString()
    }
}

function ConvertTo-IndentedString
{
<#
    .SYNOPSIS
        Indents a block of text using the number of tab stops.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNull()]
        [Object[]] $InputObject,

        ## Tab indent
        [Parameter()]
        [ValidateRange(0,10)]
        [System.Int32] $Tabs = 0,

        ## Tab size
        [Parameter()]
        [ValidateRange(0,10)]
        [System.Int32] $TabSize = 4
    )
    process
    {
        $padding = ''.PadRight(($Tabs * $TabSize), ' ')
        ## Use a StringBuilder to write the table line by line (to indent it)
        [System.Text.StringBuilder] $builder = New-Object System.Text.StringBuilder

        foreach ($line in ($InputObject -split '\r\n?|\n'))
        {
            [ref] $null = $builder.Append($padding)
            [ref] $null = $builder.AppendLine($line.TrimEnd()) ## Trim trailing space (#67)
        }
        return $builder.ToString()
    }
}

function ConvertTo-JustifiedString
{
<#
    .SYNOPSIS
        Justifies a block of text using the specified alignment.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNull()]
        [System.Object[]] $InputObject,

        ## Text alignment
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('Left','Center','Right','Justify')]
        [System.String] $Align = 'Left',

        ## Text width
        [Parameter()]
        [System.Int32] $Width = ($Host.UI.RawUI.BufferSize.Width -1)
    )
    process
    {
        foreach ($object in $InputObject)
        {
            $objectString = $object.ToString()
            if (($Align -eq 'Left') -or ($objectString.Length -ge $Width))
            {
                $justifiedString = $objectString
            }
            else
            {
                $paddingLength = $Width - $objectString.Length
                if ($Align -eq 'Center')
                {
                    $paddingLength = ($paddingLength /2) -as [System.Int32]
                }
                $padding = ''.PadRight($paddingLength, ' ')
                $justifiedString = '{0}{1}' -f $padding, $objectString
            }
            Write-Output -InputObject $justifiedString
        }
    }
}

function Get-PScriboListItemMaximumLength
{
<#
    .SYNOPSIS
        Renders a List's item numbers to determine the maximum string/render width.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter','NumberStyle')]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $List,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.Management.Automation.PSObject] $NumberStyle
    )
    process
    {
        $List.Items |
            Where-Object { $_.Type -eq 'PScribo.Item' } |
                ForEach-Object {
                    $number = ConvertFrom-NumberStyle -Value $_.Index -NumberStyle $numberStyle
                    Write-Output -InputObject $number.Length
                } |
                    Measure-Object -Maximum |
                        Select-Object -ExpandProperty Maximum
    }
}

function Get-TextTableCaption
{
<#
    .SYNOPSIS
        Generates html <p> caption from a PScribo.Table object.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNull()]
        [System.Management.Automation.PSObject] $Table
    )
    begin
    {
        ## Fix Set-StrictMode
        if (-not (Test-Path -Path Variable:\Options))
        {
            $options = New-PScriboTextOption;
        }
    }
    process
    {
        $tableStyle = Get-PScriboDocumentStyle -TableStyle $Table.Style
        $convertToAlignedStringParams = @{
            InputObject = '{0} {1} {2}' -f $tableStyle.CaptionPrefix, $Table.CaptionNumber, $Table.Caption
            Width       = $options.TextWidth
            Tabs        = $Table.Tabs
            Align       = $tableStyle.Align
        }
        return (ConvertTo-AlignedString @convertToAlignedStringParams)
    }
}

function New-PScriboTextOption
{
<#
    .SYNOPSIS
        Sets the text plugin specific formatting/output options.

    .NOTES
        All plugin options should be prefixed with the plugin name.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        ## Text/output width. 0 = none/no wrap.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Int32] $TextWidth = 120,

        ## Document header separator character.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateLength(1,1)]
        [System.String] $HeaderSeparator = '=',

        ## Document section separator character.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateLength(1,1)]
        [System.String] $SectionSeparator = '-',

        ## Document section separator character.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateLength(1,1)]
        [System.String] $LineBreakSeparator = '_',

        ## Default header/section separator width.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Int32] $SeparatorWidth = $TextWidth,

        ## Text encoding
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('ASCII','Unicode','UTF7','UTF8')]
        [System.String] $Encoding = 'ASCII'
    )
    process
    {
        return @{
            TextWidth = $TextWidth
            HeaderSeparator = $HeaderSeparator
            SectionSeparator = $SectionSeparator
            LineBreakSeparator = $LineBreakSeparator
            SeparatorWidth = $SeparatorWidth
            Encoding = $Encoding
        }
    }
}

function Out-TextBlankLine
{
<#
    .SYNOPSIS
        Output formatted text blankline.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $BlankLine
    )
    process
    {
        $blankLineBuilder = New-Object -TypeName System.Text.StringBuilder
        for ($i = 0; $i -lt $BlankLine.LineCount; $i++)
        {
            [ref] $null = $blankLineBuilder.AppendLine()
        }
        return $blankLineBuilder.ToString()
    }
}

function Out-TextDocument
{
<#
    .SYNOPSIS
        Text output plugin for PScribo.

    .DESCRIPTION
        Outputs a text file representation of a PScribo document object.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments','pluginName')]
    param
    (
        ## ThePScribo document object to convert to a text document
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $Document,

        ## Output directory path for the .txt file
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.String] $Path,

        ### Hashtable of all plugin supported options
        [Parameter()]
        [AllowNull()]
        [System.Collections.Hashtable] $Options
    )
    process
    {
        $pluginName = 'Text'
        $stopwatch = [Diagnostics.Stopwatch]::StartNew()
        Write-PScriboMessage -Message ($localized.DocumentProcessingStarted -f $Document.Name)

        ## Merge the document, text default and specified text options
        $mergePScriboPluginOptionParams = @{
            DefaultPluginOptions = New-PScriboTextOption
            DocumentOptions = $Document.Options
            PluginOptions = $Options
        }
        $Options = Merge-PScriboPluginOption @mergePScriboPluginOptionParams
        $script:currentPageNumber = 1

        [System.Text.StringBuilder] $textBuilder = New-Object -Type 'System.Text.StringBuilder'
        $firstPageHeader = Out-TextHeaderFooter -Header -FirstPage
        [ref] $null = $textBuilder.Append($firstPageHeader)

        foreach ($subSection in $Document.Sections.GetEnumerator())
        {
            $currentIndentationLevel = 1
            if ($null -ne $subSection.PSObject.Properties['Level'])
            {
                $currentIndentationLevel = $subSection.Level +1
            }
            Write-PScriboProcessSectionId -SectionId $subSection.Id -SectionType $subSection.Type -Indent $currentIndentationLevel

            switch ($subSection.Type)
            {
                'PScribo.Section'
                {
                    [ref] $null = $textBuilder.Append((Out-TextSection -Section $subSection))
                }
                'PScribo.Paragraph'
                {
                    [ref] $null = $textBuilder.Append((Out-TextParagraph -Paragraph $subSection))
                }
                'PScribo.PageBreak'
                {
                    [ref] $null = $textBuilder.Append((Out-TextPageBreak))
                }
                'PScribo.LineBreak'
                {
                    [ref] $null = $textBuilder.Append((Out-TextLineBreak))
                }
                'PScribo.Table'
                {
                    [ref] $null = $textBuilder.Append((Out-TextTable -Table $subSection))
                }
                'PScribo.TOC'
                {
                    [ref] $null = $textBuilder.Append((Out-TextTOC -TOC $subSection))
                }
                'PScribo.BlankLine'
                {
                    [ref] $null = $textBuilder.Append((Out-TextBlankLine -BlankLine $subSection))
                }
                'PScribo.Image'
                {
                    [ref] $null = $textBuilder.Append((Out-TextImage -Image $subSection))
                }
                'PScribo.ListReference'
                {
                    $textList = Out-TextList -List $Document.Lists[$subSection.Number -1]
                    [ref] $null = $textBuilder.Append($textList)
                }
                Default
                {
                    Write-PScriboMessage -Message ($localized.PluginUnsupportedSection -f $subSection.Type) -IsWarning
                }
            }
        }

        $pageFooter =Out-TextHeaderFooter -Footer
        [ref] $null = $textBuilder.Append($pageFooter)

        $stopwatch.Stop()
        Write-PScriboMessage -Message ($localized.DocumentProcessingCompleted -f $Document.Name)
        $destinationPath = Join-Path -Path $Path ('{0}.txt' -f $Document.Name)
        Write-PScriboMessage -Message ($localized.SavingFile -f $destinationPath)
        Set-Content -Value ($textBuilder.ToString()) -Path $destinationPath -Encoding $Options.Encoding
        [ref] $null = $textBuilder

        if ($stopwatch.Elapsed.TotalSeconds -gt 90)
        {
            Write-PScriboMessage -Message ($localized.TotalProcessingTimeMinutes -f $stopwatch.Elapsed.TotalMinutes)
        }
        else
        {
            Write-PScriboMessage -Message ($localized.TotalProcessingTimeSeconds -f $stopwatch.Elapsed.TotalSeconds)
        }

        ## Return the file reference to the pipeline
        Write-Output (Get-Item -Path $destinationPath)
    }
}

function Out-TextHeaderFooter
{
<#
    .SYNOPSIS
        Output formatted header/footer.
#>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'DefaultHeader')]
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'FirstPageHeader')]
        [System.Management.Automation.SwitchParameter] $Header,

        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'DefaultFooter')]
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'FirstPageFooter')]
        [System.Management.Automation.SwitchParameter] $Footer,

        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'FirstPageHeader')]
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'FirstPageFooter')]
        [System.Management.Automation.SwitchParameter] $FirstPage
    )
    process
    {
        $headerFooter = Get-PScriboHeaderFooter @PSBoundParameters
        if ($null -ne $headerFooter)
        {
            [System.Text.StringBuilder] $hfBuilder = New-Object System.Text.StringBuilder

            foreach ($subSection in $headerFooter.Sections.GetEnumerator())
            {
                ## When replacing tokens (by reference), the tokens are removed
                $cloneSubSection = $subSection | ConvertTo-Json -Depth 100 -Compress | ConvertFrom-Json
                switch ($cloneSubSection.Type)
                {
                    'PScribo.Paragraph'
                    {
                        $paragraph = Out-TextParagraph -Paragraph $cloneSubSection
                        [ref] $null = $hfBuilder.Append($paragraph)
                    }
                    'PScribo.Table'
                    {
                        if ($Footer)
                        {
                            ## Add a space before a footer table
                            [ref] $null = $hfBuilder.AppendLine()
                        }

                        $table = Out-TextTable -Table $cloneSubSection
                        [ref] $null = $hfBuilder.Append($table)

                        if ($Header)
                        {
                            ## Add a space after a header table
                            [ref] $null = $hfBuilder.AppendLine()
                        }
                    }
                    'PScribo.BlankLine'
                    {
                        $blankLine = Out-TextBlankLine -BlankLine $subSection
                        [ref] $null = $hfBuilder.Append($blankLine)
                    }
                    'PScribo.LineBreak'
                    {
                        [ref] $null = $hfBuilder.Append((Out-TextLineBreak))
                    }
                }
            }

            return $hfBuilder.ToString()
        }
    }
}

function Out-TextImage
{
<#
    .SYNOPSIS
        Output formatted image text.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNull()]
        [System.Management.Automation.PSObject] $Image
    )
    begin
    {
        ## Fix Set-StrictMode
        if (-not (Test-Path -Path Variable:\Options))
        {
            $options = New-PScriboTextOption
        }
    }
    process
    {
        $convertToAlignedStringParams = @{
            InputObject = '[Image Text="{0}"]' -f $Image.Text
            Width       = $options.TextWidth
        }

        return (ConvertTo-AlignedString @convertToAlignedStringParams)
    }
}

function Out-TextLineBreak
{
<#
    .SYNOPSIS
        Output formatted line break text.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param ( )
    begin
    {
        ## Fix Set-StrictMode
        if (-not (Test-Path -Path Variable:\Options))
        {
            $options = New-PScriboTextOption
        }
    }
    process
    {
        $convertToAlignedStringParams = @{
            InputObject = ''.PadRight($options.SeparatorWidth, $options.LineBreakSeparator)
            Width       = $options.TextWidth
        }
        return (ConvertTo-AlignedString @convertToAlignedStringParams)
    }
}

function Out-TextList
{
<#
    .SYNOPSIS
        Output formatted text list.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter','NumberStyle')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments','Options')]
    param
    (
        ## Section to output
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $List,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $Indent = 2,

        ## Number style
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $NumberStyle,

        ## Bullet style
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $BulletStyle
    )
    begin
    {
        ## Fix Set-StrictMode
        if (-not (Test-Path -Path Variable:\Options))
        {
            $options = New-PScriboTextOption
        }
    }
    process
    {
        $listBuilder = New-Object -TypeName System.Text.StringBuilder
        $leader = ''.PadRight($Indent, ' ')

        if ($List.IsNumbered)
        {
            if ($List.HasNumberStyle)
            {
                $NumberStyle = $List.NumberStyle
            }
            elseif (-not $PSBoundParameters.ContainsKey('Style'))
            {
                $NumberStyle = $Document.DefaultNumberStyle
            }
            $style = $Document.NumberStyles[$NumberStyle]

            $maxItemNumberLength = Get-PScriboListItemMaximumLength -List $List -NumberStyle $style

            $outTextListParams = @{
                NumberStyle = $NumberStyle
            }
        }
        else
        {
            if ($List.HasBulletStyle)
            {
                $BulletStyle = $List.BulletStyle
            }

            switch ($BulletStyle)
            {
                Circle
                {
                    $numberString = 'o'
                    break
                }
                Dash
                {
                    $numberString = '-'
                    break
                }
                Disc
                {
                    $numberString = '*'
                    break
                }
                Default
                {
                    ## Square style is not supported in Text so default to the browser's rendering engine
                    $numberString = '*'
                }
            }

            $outTextListParams = @{
                BulletStyle = $BulletStyle
            }
        }

        foreach ($item in $List.Items)
        {
            if ($item.Type -eq 'PScribo.Item')
            {
                if ($List.IsNumbered)
                {
                    $padding = ''
                    $itemNumber = ConvertFrom-NumberStyle -Value $item.Index -NumberStyle $style
                    $paddingLength = ($maxItemNumberLength) - $itemNumber.Length
                    if ($paddingLength -gt 0)
                    {
                        $padding = ''.PadRight($paddingLength, ' ')
                    }

                    if ($style.Align -eq 'Left')
                    {
                        $numberString = '{0}{1}' -f $itemNumber, $padding
                    }
                    elseif ($style.Align -eq 'Right')
                    {
                        $numberString = '{0}{1}' -f $padding, $itemNumber
                    }
                }

                [ref] $null = $listBuilder.AppendFormat('{0}{1} {2}', $leader, $numberString, $item.Text).AppendLine()
            }
            else
            {
                $newIndent = $Indent + 2
                if ($List.IsNumbered)
                {
                    $newIndent = $Indent + $maxItemNumberLength +1
                }
                $nestedList = Out-TextList -List $item -Indent $newIndent @outTextListParams
                [ref] $null = $listBuilder.Append($nestedList)
            }
        }
        [ref] $null = $listBuilder.AppendLine()
        return $listBuilder.ToString()
    }
}

function Out-TextPageBreak
{
<#
    .SYNOPSIS
        Output formatted line break text.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param ( )
    process
    {
        $pagebreakBuilder = New-Object -TypeName System.Text.StringBuilder

        $isFirstPage = $currentPageNumber -eq 1
        $pageFooter = Out-TextHeaderFooter -Footer -FirstPage:$isFirstPage
        [ref] $null = $pageBreakBuilder.Append($pageFooter)

        $script:currentPageNumber++
        [ref] $null = $pagebreakBuilder.Append((Out-TextLineBreak)).AppendLine()

        $pageHeader = Out-TextHeaderFooter -Header
        [ref] $null = $pageBreakBuilder.Append($pageHeader)

        return $pageBreakBuilder.ToString()
    }
}

function Out-TextParagraph
{
<#
    .SYNOPSIS
        Output formatted paragraph run.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNull()]
        [System.Management.Automation.PSObject] $Paragraph
    )
    begin
    {
        ## Fix Set-StrictMode
        if (-not (Test-Path -Path Variable:\Options))
        {
            $options = New-PScriboTextOption;
        }
    }
    process
    {
        $convertToAlignedStringParams = @{
            Width       = $options.TextWidth
            Tabs        = $Paragraph.Tabs
            Align       = 'Left'
        }

        if (-not [System.String]::IsNullOrEmpty($Paragraph.Style))
        {
            $paragraphStyle = Get-PScriboDocumentStyle -Style $Paragraph.Style
            $convertToAlignedStringParams['Align'] = $paragraphStyle.Align
        }

        [System.Text.StringBuilder] $paragraphBuilder = New-Object -TypeName 'System.Text.StringBuilder'
        foreach ($paragraphRun in $Paragraph.Sections)
        {
            $text = Resolve-PScriboToken -InputObject $paragraphRun.Text
            [ref] $null = $paragraphBuilder.Append($text)

            if (($paragraphRun.IsParagraphRunEnd -eq $false) -and
                ($paragraphRun.NoSpace -eq $false))
            {
                [ref] $null = $paragraphBuilder.Append(' ')
            }
        }

        $convertToAlignedStringParams['InputObject'] = $paragraphBuilder.ToString()
        return (ConvertTo-AlignedString @convertToAlignedStringParams)
    }
}

function Out-TextSection
{
<#
    .SYNOPSIS
        Output formatted text section.
#>

    [CmdletBinding()]
    param
    (
        ## Section to output
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $Section
    )
    begin
    {
        ## Fix Set-StrictMode
        if (-not (Test-Path -Path Variable:\Options))
        {
            $options = New-PScriboTextOption
        }
    }
    process
    {
        $padding = ''.PadRight(($Section.Tabs * 4), ' ')
        $sectionBuilder = New-Object -TypeName System.Text.StringBuilder
        if ($Document.Options['EnableSectionNumbering'])
        {
            [string] $sectionName = '{0} {1}' -f $Section.Number, $Section.Name
        }
        else
        {
            [string] $sectionName = '{0}' -f $Section.Name
        }
        [ref] $null = $sectionBuilder.AppendLine()
        [ref] $null = $sectionBuilder.Append($padding)
        [ref] $null = $sectionBuilder.AppendLine($sectionName.TrimStart())
        [ref] $null = $sectionBuilder.Append($padding)
        [ref] $null = $sectionBuilder.AppendLine(''.PadRight(($options.SeparatorWidth - $padding.Length), $options.SectionSeparator))

        foreach ($subSection in $Section.Sections.GetEnumerator())
        {
            $currentIndentationLevel = 1
            if ($null -ne $subSection.PSObject.Properties['Level'])
            {
                $currentIndentationLevel = $subSection.Level +1
            }
            Write-PScriboProcessSectionId -SectionId $subSection.Id -SectionType $subSection.Type -Indent $currentIndentationLevel

            switch ($subSection.Type)
            {
                'PScribo.Section'
                {
                    [ref] $null = $sectionBuilder.Append((Out-TextSection -Section $subSection))
                }
                'PScribo.Paragraph'
                {
                    [ref] $null = $sectionBuilder.Append((Out-TextParagraph -Paragraph $subSection))
                }
                'PScribo.PageBreak'
                {
                    [ref] $null = $sectionBuilder.Append((Out-TextPageBreak)) ## Page breaks implemented as line break with extra padding
                }
                'PScribo.LineBreak'
                {
                    [ref] $null = $sectionBuilder.Append((Out-TextLineBreak))
                }
                'PScribo.Table'
                {
                    [ref] $null = $sectionBuilder.Append((Out-TextTable -Table $subSection))
                }
                'PScribo.BlankLine'
                {
                    [ref] $null = $sectionBuilder.Append((Out-TextBlankLine -BlankLine $subSection))
                }
                'PScribo.Image'
                {
                    [ref] $null = $sectionBuilder.Append((Out-TextImage -Image $subSection))
                }
                'PScribo.ListReference'
                {
                    $textList = Out-TextList -List $Document.Lists[$subSection.Number -1]
                    [ref] $null = $sectionBuilder.Append($textList)
                }
                Default
                {
                    Write-PScriboMessage -Message ($localized.PluginUnsupportedSection -f $subSection.Type) -IsWarning
                }
            }
        }

        return $sectionBuilder.ToString()
    }
}

function Out-TextTable
{
<#
    .SYNOPSIS
        Output formatted text table.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $Table
    )
    begin
    {
        ## Fix Set-StrictMode
        if (-not (Test-Path -Path Variable:\Options))
        {
            $options = New-PScriboTextOption
        }
    }
    process
    {
        $tableStyle = Get-PScriboDocumentStyle -TableStyle $Table.Style
        $tableBuilder = New-Object -TypeName System.Text.StringBuilder
        $tableRenderWidth = $options.TextWidth - ($Table.Tabs * 4)

        ## We need to rewrite arrays for text formatting and we don't want to
        ## alter the source documnent (#126)
        $cloneTable = $Table | ConvertTo-Json -Depth 100 -Compress | ConvertFrom-Json

        ## We need to flatten arrays and replace page numbers before outputting the table
        foreach ($row in $cloneTable.Rows)
        {
            foreach ($property in $row.PSObject.Properties)
            {
                if ($property.Value -is [System.Array])
                {
                    $property.Value = [System.String]::Join(' ', $property.Value)
                }
                $property.Value = Resolve-PScriboToken -InputObject $property.Value
            }
        }

        ## We've got to render the table first to determine how wide it is
        ## before we can justify it
        if ($Table.IsKeyedList)
        {
            ## Create new objects with headings as properties
            $tableText = (ConvertTo-PSObjectKeyedListTable -Table $cloneTable |
                            Select-Object -Property * -ExcludeProperty '*__Style' |
                                Format-Table -Wrap -AutoSize |
                                    Out-String -Width $tableRenderWidth).Trim([System.Environment]::NewLine)
        }
        elseif ($Table.IsList)
        {
            $tableText = ($cloneTable.Rows |
                Select-Object -Property * -ExcludeProperty '*__Style' |
                    Format-List | Out-String -Width $tableRenderWidth).Trim([System.Environment]::NewLine)
        }
        else
        {
            ## Don't trim tabs for table headers
            ## Tables set to AutoSize as otherwise rendering is different between PoSh v4 and v5
            $tableText = ($cloneTable.Rows |
                            Select-Object -Property * -ExcludeProperty '*__Style' |
                                Format-Table -Wrap -AutoSize |
                                    Out-String -Width $tableRenderWidth).Trim([System.Environment]::NewLine)
        }

        if ($Table.HasCaption -and ($tableStyle.CaptionLocation -eq 'Above'))
        {
            $justifiedCaption = Get-TextTableCaption -Table $Table
            [ref] $null = $tableBuilder.AppendLine($justifiedCaption)
            [ref] $null = $tableBuilder.AppendLine()
        }

        ## We don't want to wrap table contents so just justify it
        $convertToJustifiedStringParams = @{
            InputObject = $tableText
            Width = $tableRenderWidth
            Align = $tableStyle.Align
        }
        $justifiedTableText = ConvertTo-JustifiedString @convertToJustifiedStringParams

        [ref] $null = $tableBuilder.Append($justifiedTableText)

        if ($Table.HasCaption -and ($tableStyle.CaptionLocation -eq 'Below'))
        {
            $justifiedCaption = Get-TextTableCaption -Table $Table
            [ref] $null = $tableBuilder.AppendLine()
            [ref] $null = $tableBuilder.AppendLine()
            [ref] $null = $tableBuilder.Append($justifiedCaption)
        }

        $convertToIndentedStringParams = @{
            InputObject = $tableBuilder.ToString()
            Tabs        = $Table.Tabs
        }

        return (ConvertTo-IndentedString @convertToIndentedStringParams)
    }
}

function Out-TextTOC
{
<#
    .SYNOPSIS
        Output formatted Table of Contents
#>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $TOC
    )
    begin
    {
        ## Fix Set-StrictMode
        if (-not (Test-Path -Path Variable:\Options))
        {
            $options = New-PScriboTextOption
        }
    }
    process
    {
        $tocBuilder = New-Object -TypeName System.Text.StringBuilder
        [ref] $null = $tocBuilder.AppendLine($TOC.Name)
        [ref] $null = $tocBuilder.AppendLine(''.PadRight($options.SeparatorWidth, $options.SectionSeparator))

        if ($Options.ContainsKey('EnableSectionNumbering'))
        {
            $maxSectionNumberLength = $Document.TOC.Number | ForEach-Object { $_.Length } | Measure-Object -Maximum | Select-Object -ExpandProperty Maximum
            foreach ($tocEntry in $Document.TOC)
            {
                $sectionNumberPaddingLength = $maxSectionNumberLength - $tocEntry.Number.Length
                $sectionNumberIndent = ''.PadRight($tocEntry.Level, ' ')
                $sectionPadding = ''.PadRight($sectionNumberPaddingLength, ' ')
                [ref] $null = $tocBuilder.AppendFormat('{0}{1} {2}{3}', $tocEntry.Number, $sectionPadding, $sectionNumberIndent, $tocEntry.Name).AppendLine()
            }
        }
        else
        {
            $maxSectionNumberLength = $Document.TOC.Level | Sort-Object | Select-Object -Last 1
            foreach ($tocEntry in $Document.TOC)
            {
                $sectionNumberIndent = ''.PadRight($tocEntry.Level, ' ')
                [ref] $null = $tocBuilder.AppendFormat('{0}{1}', $sectionNumberIndent, $tocEntry.Name).AppendLine()
            }
        }

        return $tocBuilder.ToString()
    }
}


# SIG # Begin signature block
# MIIuuwYJKoZIhvcNAQcCoIIurDCCLqgCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCjSdTPrsM3RvSy
# w9wnFqSMnkGYFwyXS6wddO/3eU5Zd6CCE6QwggWQMIIDeKADAgECAhAFmxtXno4h
# MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV
# BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z
# ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
# AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z
# G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ
# anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s
# Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL
# 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb
# BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3
# JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c
# AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx
# YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0
# viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL
# T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud
# EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf
# Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk
# aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS
# PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK
# 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB
# cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp
# 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg
# dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri
# RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7
# 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5
# nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3
# i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H
# EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G
# CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
# ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C
# 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce
# 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da
# E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T
# SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA
# FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh
# D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM
# 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z
# 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05
# huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY
# mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP
# /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T
# AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD
# VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG
# A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV
# HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
# cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN
# BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry
# sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL
# IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf
# Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh
# OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh
# dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV
# 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j
# wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH
# Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC
# XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l
# /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW
# eE4wggdYMIIFQKADAgECAhAIfHT3o/FeY5ksO94AUhTmMA0GCSqGSIb3DQEBCwUA
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
# ODQgMjAyMSBDQTEwHhcNMjMxMDE4MDAwMDAwWhcNMjYxMjE2MjM1OTU5WjBgMQsw
# CQYDVQQGEwJHQjEPMA0GA1UEBxMGTG9uZG9uMR8wHQYDVQQKExZWaXJ0dWFsIEVu
# Z2luZSBMaW1pdGVkMR8wHQYDVQQDExZWaXJ0dWFsIEVuZ2luZSBMaW1pdGVkMIIC
# IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtyhrsCMi6pgLcX5sWY7I09dO
# WKweRHfDwW5AN6ffgLCYO9dqWWxvqu95FqnNVRyt1VNzEl3TevKVhRE0GGdirei3
# VqnFFjLDwD2jHhGY8qoSYyfffj/WYq2DkvNI62C3gUwSeP3FeqKRalc2c3V2v4jh
# yEYhrgG3nfnWQ/Oq2xzuiCqHy1E4U+IKKDtrXls4JX2Z4J/uAHZIAyKfrcTRQOhZ
# R4ZS1cQkeSBU9Urx578rOmxL0si0GAoaYQC49W7OimRelbahxZw/R+f5ch+C1ycU
# CpeXLg+bFhpa0+EXnkGidlILZbJiZJn7qvMQTZgipQKZ8nhX3rtJLqTeodPWzcCk
# tXQUy0q5fxhR3e6Ls7XQesq/G2yMcCMTCd6eltze0OgabvL6Xkhir5lATHaJtnmw
# FlcKzRr1YXK1k1D84hWfKSAdUg8T1O72ztimbqFLg6WoC8M2qqsHcm2DOc9hM3i2
# CWRpegikitRvZ9i1wkA9arGh7+a7UD+nLh2hnGmO06wONLNqABOEn4JOVnNrQ1gY
# eDeH9FDx7IYuAvMsfXG9Bo+I97TR2VfwDAx+ccR+UQLON3aQyFZ3BefYnvUu0gUR
# ikEAnAS4Jnc3BHizgb0voz0iWRDjFoTTmCmrInCVDGc+5KMy0xyoUwdQvYvRGAWB
# 61OCWnXBXbAEPniTZ80CAwEAAaOCAgMwggH/MB8GA1UdIwQYMBaAFGg34Ou2O/hf
# EYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRuAv58K4EDYLmb7WNcxt5+r4NfnzA+BgNV
# HSAENzA1MDMGBmeBDAEEATApMCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2lj
# ZXJ0LmNvbS9DUFMwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMD
# MIG1BgNVHR8Ega0wgaowU6BRoE+GTWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9E
# aWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEu
# Y3JsMFOgUaBPhk1odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz
# dGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNybDCBlAYIKwYB
# BQUHAQEEgYcwgYQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNv
# bTBcBggrBgEFBQcwAoZQaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lD
# ZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcnQw
# CQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAgEAnXMg6efkBrwLIvd1Xmuh0dam
# 9FhUtDEj+P5SIqdP/U4veOv66NEQhBHLbW2Dvrdm6ec0HMj9b4e8pt4ylKFzHIPj
# fpuRffHVR9JQSx8qpryN6pP49DfCkAYeZGqjY3pGRzd/xQ0cfwcuYbUF+vwVk7tj
# q8c93VHCM0rb5M4N2hD1Ze1pvZxtaf9QnFKFzgXZjr02K6bswQc2+n5jFCp7zV1f
# KTstyb68rhSJBWKK1tMeFk6a6HXr5buTD3skluC0oyPmD7yAd97r2owjDMEveEso
# kADP/z7XQk7wqbwbpi4W6Uju2qHK/9UUsVRF5KTVEAIzVw2V1Aq/Jh3JuSV7b7C1
# 4CghNekltBb+w7YVp8/IFcj7axqnpNQ/+f7RVc3A5hyjV+MkoSwn8Sg7a7hn6SzX
# jec/TfRVvWCmG94MQHko+6206uIXrZnmQ6UQYFyOHRlyKDEozzkZhIcVlsZloUjL
# 3FZ5V/l8TIIzbc3bkEnu4iByksNvRxI6c5264OLauYlWv50ZUPwXmZ9gX8bs3BqZ
# avbGUrOW2PIjtWhtvs4zHhMBCoU1Z0OMvXcF9rUDqefmVCZK46xz3DGKVkDQnxY6
# UWQ3GL60/lEzju4YS99LJVQks2UGmP6LAzlEZ1dnGqi1aQ51OidCYEs39B75PsvO
# By2iAR8pBVi/byWBypExghptMIIaaQIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYD
# VQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBH
# NCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQgMjAyMSBDQTECEAh8dPej8V5j
# mSw73gBSFOYwDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAA
# oQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4w
# DAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQg2RDM/0kO+9Syr7nN29sM4rn4
# 5EYYqGh/8hMUPKRKiC0wDQYJKoZIhvcNAQEBBQAEggIAbJqWhN8TngaYHfktSBcd
# 7aXMcCOLvZd1qgQR8tHJjdo2xRMPeicppaix8r02P4rMjUpFKo8e6T0l89wly9+5
# nmOeI/NBwcFwQyWDN3FEs9ncjcM0Z+A589gezvZ8tPln4SWjVXOxIg+NB0uTyEWd
# 8reX7plEsCycE3mqSRDeUkvSFwnNOrBMm/lUGI9/4O/zwnUUcOV3gIyzEwMjwoQS
# Iq8/Q1hmma30F6Ow6q1RIWEbOIgXiq+uVyX3XxSDylg7XnRku++mQ+5lro9SmWGm
# jAP97DgMl01C1O0vyM2Si2KIxhqWVLjk92wP4weIw/oyqKpEFBRnHWvgxgOehAxB
# sp6pWLi9MWzuFWTxyqMp9Ggi3MKdB2q7hjFRKQwYkvA94ax5BN+ZeUZbvpA90QzB
# QuSfS+FxibJ99iBokNa2F05e6oHrN4HGEJWPgylKifoBYLx7xM+z1KD2YemTZsKu
# t2YhTpt/X/MLTecVfKr1DZ1jfEDqasYj/nkVHmG/aoy0CqgcsTZ9V4k5+amz3222
# CWfifo+cCIXxiR9RDXf/FW2n1JtkMGq0p20EjohigqCD+6v27sWIzYXmyMF3eyBa
# i2kAxAKETvBW4JfxBwKpaukQNbu93maSFXi5jYwdDrBR8na4cNOqKhtrCuTYynhn
# uq8NzerdJcNOfMOLCvSs2hqhghc6MIIXNgYKKwYBBAGCNwMDATGCFyYwghciBgkq
# hkiG9w0BBwKgghcTMIIXDwIBAzEPMA0GCWCGSAFlAwQCAQUAMHgGCyqGSIb3DQEJ
# EAEEoGkEZzBlAgEBBglghkgBhv1sBwEwMTANBglghkgBZQMEAgEFAAQgxFeRGdOd
# BVsOufpnGoRftIUdNYQ/sjy9Fb8OA0PWz6ICEQCAKwrtIVoU5Tt8/erspk12GA8y
# MDI1MDMwNDEyNDE1NFqgghMDMIIGvDCCBKSgAwIBAgIQC65mvFq6f5WHxvnpBOMz
# BDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNl
# cnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBT
# SEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTI0MDkyNjAwMDAwMFoXDTM1MTEyNTIz
# NTk1OVowQjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSAwHgYDVQQD
# ExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIP
# ADCCAgoCggIBAL5qc5/2lSGrljC6W23mWaO16P2RHxjEiDtqmeOlwf0KMCBDEr4I
# xHRGd7+L660x5XltSVhhK64zi9CeC9B6lUdXM0s71EOcRe8+CEJp+3R2O8oo76EO
# 7o5tLuslxdr9Qq82aKcpA9O//X6QE+AcaU/byaCagLD/GLoUb35SfWHh43rOH3bp
# LEx7pZ7avVnpUVmPvkxT8c2a2yC0WMp8hMu60tZR0ChaV76Nhnj37DEYTX9ReNZ8
# hIOYe4jl7/r419CvEYVIrH6sN00yx49boUuumF9i2T8UuKGn9966fR5X6kgXj3o5
# WHhHVO+NBikDO0mlUh902wS/Eeh8F/UFaRp1z5SnROHwSJ+QQRZ1fisD8UTVDSup
# WJNstVkiqLq+ISTdEjJKGjVfIcsgA4l9cbk8Smlzddh4EfvFrpVNnes4c16Jidj5
# XiPVdsn5n10jxmGpxoMc6iPkoaDhi6JjHd5ibfdp5uzIXp4P0wXkgNs+CO/CacBq
# U0R4k+8h6gYldp4FCMgrXdKWfM4N0u25OEAuEa3JyidxW48jwBqIJqImd93NRxvd
# 1aepSeNeREXAu2xUDEW8aqzFQDYmr9ZONuc2MhTMizchNULpUEoA6Vva7b1XCB+1
# rxvbKmLqfY/M/SdV6mwWTyeVy5Z/JkvMFpnQy5wR14GJcv6dQ4aEKOX5AgMBAAGj
# ggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8E
# DDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEw
# HwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYEFJ9XLAN3
# DigVkGalY17uT5IfdqBbMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwzLmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3Rh
# bXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzABhhhodHRw
# Oi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9jYWNlcnRz
# LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1l
# U3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAD2tHh92mVvjOIQSR9lD
# kfYR25tOCB3RKE/P09x7gUsmXqt40ouRl3lj+8QioVYq3igpwrPvBmZdrlWBb0Hv
# qT00nFSXgmUrDKNSQqGTdpjHsPy+LaalTW0qVjvUBhcHzBMutB6HzeledbDCzFzU
# y34VarPnvIWrqVogK0qM8gJhh/+qDEAIdO/KkYesLyTVOoJ4eTq7gj9UFAL1UruJ
# KlTnCVaM2UeUUW/8z3fvjxhN6hdT98Vr2FYlCS7Mbb4Hv5swO+aAXxWUm3WpByXt
# gVQxiBlTVYzqfLDbe9PpBKDBfk+rabTFDZXoUke7zPgtd7/fvWTlCs30VAGEsshJ
# mLbJ6ZbQ/xll/HjO9JbNVekBv2Tgem+mLptR7yIrpaidRJXrI+UzB6vAlk/8a1u7
# cIqV0yef4uaZFORNekUgQHTqddmsPCEIYQP7xGxZBIhdmm4bhYsVA6G2WgNFYagL
# DBzpmk9104WQzYuVNsxyoVLObhx3RugaEGru+SojW4dHPoWrUhftNpFC5H7QEY7M
# hKRyrBe7ucykW7eaCuWBsBb4HOKRFVDcrZgdwaSIqMDiCLg4D+TPVgKx2EgEdeoH
# NHT9l3ZDBD+XgbF+23/zBjeCtxz+dL/9NWR6P2eZRi7zcEO1xwcdcqJsyz/JceEN
# c2Sg8h3KeFUCS7tpFk7CrDqkMIIGrjCCBJagAwIBAgIQBzY3tyRUfNhHrP0oZipe
# WzANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNl
# cnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdp
# Q2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjIwMzIzMDAwMDAwWhcNMzcwMzIyMjM1
# OTU5WjBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5
# BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0
# YW1waW5nIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxoY1Bkmz
# wT1ySVFVxyUDxPKRN6mXUaHW0oPRnkyibaCwzIP5WvYRoUQVQl+kiPNo+n3znIkL
# f50fng8zH1ATCyZzlm34V6gCff1DtITaEfFzsbPuK4CEiiIY3+vaPcQXf6sZKz5C
# 3GeO6lE98NZW1OcoLevTsbV15x8GZY2UKdPZ7Gnf2ZCHRgB720RBidx8ald68Dd5
# n12sy+iEZLRS8nZH92GDGd1ftFQLIWhuNyG7QKxfst5Kfc71ORJn7w6lY2zkpsUd
# zTYNXNXmG6jBZHRAp8ByxbpOH7G1WE15/tePc5OsLDnipUjW8LAxE6lXKZYnLvWH
# po9OdhVVJnCYJn+gGkcgQ+NDY4B7dW4nJZCYOjgRs/b2nuY7W+yB3iIU2YIqx5K/
# oN7jPqJz+ucfWmyU8lKVEStYdEAoq3NDzt9KoRxrOMUp88qqlnNCaJ+2RrOdOqPV
# A+C/8KI8ykLcGEh/FDTP0kyr75s9/g64ZCr6dSgkQe1CvwWcZklSUPRR8zZJTYsg
# 0ixXNXkrqPNFYLwjjVj33GHek/45wPmyMKVM1+mYSlg+0wOI/rOP015LdhJRk8mM
# DDtbiiKowSYI+RQQEgN9XyO7ZONj4KbhPvbCdLI/Hgl27KtdRnXiYKNYCQEoAA6E
# VO7O6V3IXjASvUaetdN2udIOa5kM0jO0zbECAwEAAaOCAV0wggFZMBIGA1UdEwEB
# /wQIMAYBAf8CAQAwHQYDVR0OBBYEFLoW2W1NhS9zKXaaL3WMaiCPnshvMB8GA1Ud
# IwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNV
# HSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0
# dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2Vy
# dHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0f
# BDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcB
# MA0GCSqGSIb3DQEBCwUAA4ICAQB9WY7Ak7ZvmKlEIgF+ZtbYIULhsBguEE0TzzBT
# zr8Y+8dQXeJLKftwig2qKWn8acHPHQfpPmDI2AvlXFvXbYf6hCAlNDFnzbYSlm/E
# UExiHQwIgqgWvalWzxVzjQEiJc6VaT9Hd/tydBTX/6tPiix6q4XNQ1/tYLaqT5Fm
# niye4Iqs5f2MvGQmh2ySvZ180HAKfO+ovHVPulr3qRCyXen/KFSJ8NWKcXZl2szw
# cqMj+sAngkSumScbqyQeJsG33irr9p6xeZmBo1aGqwpFyd/EjaDnmPv7pp1yr8TH
# wcFqcdnGE4AJxLafzYeHJLtPo0m5d2aR8XKc6UsCUqc3fpNTrDsdCEkPlM05et3/
# JWOZJyw9P2un8WbDQc1PtkCbISFA0LcTJM3cHXg65J6t5TRxktcma+Q4c6umAU+9
# Pzt4rUyt+8SVe+0KXzM5h0F4ejjpnOHdI/0dKNPH+ejxmF/7K9h+8kaddSweJywm
# 228Vex4Ziza4k9Tm8heZWcpw8De/mADfIBZPJ/tgZxahZrrdVcA6KYawmKAr7ZVB
# tzrVFZgxtGIJDwq9gdkT/r+k0fNX2bwE+oLeMt8EifAAzV3C+dAjfwAL5HYCJtnw
# ZXZCpimHCUcr5n8apIUP/JiW9lVUKx+A+sDyDivl1vupL0QVSucTDh3bNzgaoSv2
# 7dZ8/DCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZIhvcNAQEM
# BQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE
# CxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJ
# RCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVowYjELMAkG
# A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp
# Z2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MIIC
# IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjwwIjBpM+zC
# pyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J58soR0uRf
# 1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMHhOZ0O21x
# 4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6Zu53yEio
# ZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQecN4x7ax
# xLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4bA3VdeGbZ
# OjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9WV1CdoeJ
# l2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCUtNJhbesz
# 2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvoZKYz0YkH
# 4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/JvNNBERJb
# 5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCPorF+CiaZ
# 9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMBAf8wHQYD
# VR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXroq/0ksuC
# MS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRtMGswJAYI
# KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3
# aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v
# dENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdpY2VydC5j
# b20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgwBgYEVR0g
# ADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cHvZqsoYcs
# 7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8UgPITtAq
# 3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTnf+hZqPC/
# Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxUjG/voVA9
# /HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8jLfR+cWoj
# ayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDGCA3YwggNyAgEB
# MHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYD
# VQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFt
# cGluZyBDQQIQC65mvFq6f5WHxvnpBOMzBDANBglghkgBZQMEAgEFAKCB0TAaBgkq
# hkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI1MDMwNDEy
# NDE1NFowKwYLKoZIhvcNAQkQAgwxHDAaMBgwFgQU29OF7mLb0j575PZxSFCHJNWG
# W0UwLwYJKoZIhvcNAQkEMSIEIDe5ddb3L9PCqy+IOIjhV2AQI0vawOL2NtK5vMPp
# VDG3MDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEIHZ2n6jyYy8fQws6IzCu1lZ1/tdz
# 2wXWZbkFk5hDj5rbMA0GCSqGSIb3DQEBAQUABIICAIwJ5qXVd0XfU4CLzT23bTZ6
# VotTUODBKEsJWSs9T+6+uIP6+pVB1YSBO1CbGT9B+QHg9QkW9VlcYtRpSGaMpCNy
# WxJzPM/SeOH4kD1rWRuNA880j4lqHih037kUqJhHD1jwU9YYSQMsVIOidG55HL/U
# dBtJmUFT0cFCeFqp4oVu1rA3GMbzEjAWpsjYgsG3HNeRSw1+m2K23ackNe1UXPAW
# v9vv//2X1kKMyPK1j1Osmsnq7RWhYBBhdxe3rz3RINdh0pYtReR/GY0Y+BwNUt1q
# TOb1ljxkICt8bHLMrQNV/U/c7h0e9jwmVP63SOHtIjJnNAPcN6nXHSFz2glxx0zu
# tpOZvrB+uJVMH4tYUxnD5d9lK+iHC9CrMM7BZUBMca3XMbA1ebx3rcOQiJjBfm8Q
# JU8OW/j5F3e6Su+lAdyCL/8xcjCDJwfYRFTrPhfnZKWyHAC5PFEbi9S6JLlz2Hdh
# pj1ASpHqvwp2oWUdov94JMZhOipy64/UC/B+Mmrs//LXtIuDjqd6ikXavw8wCnGD
# djaLbRgY20Y2hvceiqk8hcvrrGkpc0SrkIckOTPI5BHBgl0zYJaNx/EuHX7NaGf0
# lPW0H8UgA04R8beENQlx7qZNNyrFdXHfpfCezPWp1l9WuLce6zS5BKtnr91jEBuN
# LljV0peRh4JoynrGbVIn
# SIG # End signature block