Src/Private/Private.ps1

function Add-PScriboNumberStyle
{
<#
    .SYNOPSIS
        Defines a new PScribo numbered list formatting style.

    .DESCRIPTION
        Creates a number list formatting style that can be applied to the PScribo 'List' keyword.

    .NOTES
        Not all plugins support all options.
#>

    [CmdletBinding()]
    param
    (
        ## Number formatting style name/id
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0, ParameterSetName = 'Predefined')]
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0, ParameterSetName = 'Custom')]
        [ValidateNotNullOrEmpty()]
        [Alias('Name')]
        [System.String] $Id,

        ## NOTE: Only supported in Text, Html and Word output.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Predefined')]
        [ValidateSet('Number','Letter','Roman')]
        [System.String] $Format,

        ## Custom number 'XYZ-###' NOTE: Only supported in Text and Word output.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Custom')]
        [System.String] $Custom,

        ## Number format suffix, e.g. '.' or ')'. NOTE: Only supported in text and Word output
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Predefined')]
        [ValidateLength(1, 1)]
        [System.String] $Suffix = '.',

        ## Only applicable to 'Letter' and 'Roman' formats
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Predefined')]
        [System.Management.Automation.SwitchParameter] $Uppercase,

        ## Set as default table style. NOTE: Cannot set custom styles as default as they're not supported by all plugins.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Predefined')]
        [System.Management.Automation.SwitchParameter] $Default,

        ## Number alignment.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Predefined')]
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Custom')]
        [ValidateSet('Left', 'Right')]
        [System.String] $Align = 'Right',

        ## Override the default Word indentation level.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Predefined')]
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Custom')]
        [System.Int32] $Indent,

        ## Override the default Word hanging indentation level.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Predefined')]
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Custom')]
        [System.Int32] $Hanging
    )
    begin
    {
        if ($Custom)
        {
            if (-not $Custom.Contains('%'))
            {
                throw ($localized.InvalidCustomNumberStyleError -f $Custom)
            }
        }
    }
    process
    {
        $pscriboDocument.Properties['NumberStyles']++
        $numberStyle = [PSCustomObject] @{
            Id        = $Id.Replace(' ', $pscriboDocument.Options['SpaceSeparator'])
            Name      = $Id
            Format    = if ($PSBoundParameters.ContainsKey('Format')) { $Format } else { 'Custom' }
            Custom    = $Custom
            Align     = $Align
            Uppercase = $Uppercase
            Suffix    = $Suffix
            Indent    = $Indent
            Hanging   = $Hanging
        }
        $pscriboDocument.NumberStyles[$Id] = $numberStyle
        if ($Default)
        {
            $pscriboDocument.DefaultNumberStyle = $numberStyle.Id
        }
    }
}

function Add-PScriboStyle
{
<#
    .SYNOPSIS
        Initializes a new PScribo style object.
#>

    [CmdletBinding()]
    param
    (
        ## Style name
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        ## Style id
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Id = $Name -Replace(' ',''),

        ## Font size (pt)
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.UInt16] $Size = 11,

        ## Font name (array of names for HTML output)
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String[]] $Font,

        ## Font color/colour
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('Colour')]
        [ValidateNotNullOrEmpty()]
        [System.String] $Color = 'Black',

        ## Background color/colour
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('BackgroundColour')]
        [ValidateNotNullOrEmpty()]
        [System.String] $BackgroundColor,

        ## Bold typeface
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Bold,

        ## Italic typeface
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Italic,

        ## Underline typeface
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Underline,

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

        ## Html CSS class id - to override Style.Id in HTML output.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $ClassId = $Id,

        ## Hide style from UI (Word)
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('Hide')]
        [System.Management.Automation.SwitchParameter] $Hidden,

        ## Set as default style
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Default
    )
    begin
    {
        if (-not (Test-PScriboStyleColor -Color $Color))
        {
            throw ($localized.InvalidHtmlColorError -f $Color);
        }
        if ($BackgroundColor)
        {
            if (-not (Test-PScriboStyleColor -Color $BackgroundColor))
            {
                throw ($localized.InvalidHtmlBackgroundColorError -f $BackgroundColor);
            }
            else
            {
                $BackgroundColor = Resolve-PScriboStyleColor -Color $BackgroundColor;
            }
        }
        if (-not ($Font))
        {
            $Font = $pscriboDocument.Options['DefaultFont'];
        }
    }
    process
    {
        $pscriboDocument.Properties['Styles']++;
        $style = [PSCustomObject] @{
            Id              = $Id;
            Name            = $Name;
            Font            = $Font;
            Size            = $Size;
            Color           = (Resolve-PScriboStyleColor -Color $Color).ToLower();
            BackgroundColor = $BackgroundColor.ToLower();
            Bold            = $Bold.ToBool();
            Italic          = $Italic.ToBool();
            Underline       = $Underline.ToBool();
            Align           = $Align;
            ClassId         = $ClassId;
            Hidden          = $Hidden.ToBool();
        }
        $pscriboDocument.Styles[$Id] = $style;
        if ($Default)
        {
            $pscriboDocument.DefaultStyle = $style.Id;
        }
    }
}

function Add-PScriboTableStyle
{
<#
    .SYNOPSIS
        Defines a new PScribo table formatting style.

    .DESCRIPTION
        Creates a standard table formatting style that can be applied
        to the PScribo table keyword, e.g. a combination of header and
        row styles and borders.

    .NOTES
        Not all plugins support all options.
#>

    [CmdletBinding()]
    param
    (
        ## Table Style name/id
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [Alias('Name')]
        [System.String] $Id,

        ## Header Row Style Id
        [Parameter(ValueFromPipelineByPropertyName, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [System.String] $HeaderStyle = 'Normal',

        ## Row Style Id
        [Parameter(ValueFromPipelineByPropertyName, Position = 2)]
        [ValidateNotNullOrEmpty()]
        [System.String] $RowStyle = 'Normal',

        ## Alternating Row Style Id
        [Parameter(ValueFromPipelineByPropertyName, Position = 3)]
        [AllowNull()]
        [Alias('AlternatingRowStyle')]
        [System.String] $AlternateRowStyle = $RowStyle,

        ## Caption Style Id
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $CaptionStyle = 'Normal',

        ## Table border size/width (pt)
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.Single] $BorderWidth = 0,

        ## Table border colour
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias('BorderColour')]
        [System.String] $BorderColor = '000',

        ## Table cell top padding (pt)
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Single] $PaddingTop = 1.0,

        ## Table cell left padding (pt)
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Single] $PaddingLeft = 4.0,

        ## Table cell bottom padding (pt)
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Single] $PaddingBottom = 0.0,

        ## Table cell right padding (pt)
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Single] $PaddingRight = 4.0,

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

        ## Caption prefix
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $CaptionPrefix = 'Table',

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('Above', 'Below')]
        [System.String] $CaptionLocation = 'Below',

        ## Set as default table style
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Default
    )
    begin
    {
        if ($BorderWidth -gt 0)
        {
            $borderStyle = 'Solid'
        }
        else
        {
            $borderStyle = 'None'
        }
        if (-not ($pscriboDocument.Styles.ContainsKey($HeaderStyle)))
        {
            throw ($localized.UndefinedTableHeaderStyleError -f $HeaderStyle)
        }
        if (-not ($pscriboDocument.Styles.ContainsKey($RowStyle)))
        {
            throw ($localized.UndefinedTableRowStyleError -f $RowStyle)
        }
        if (-not ($pscriboDocument.Styles.ContainsKey($AlternateRowStyle)))
        {
            throw ($localized.UndefinedAltTableRowStyleError -f $AlternateRowStyle)
        }
        if (-not (Test-PScriboStyleColor -Color $BorderColor))
        {
            throw ($localized.InvalidTableBorderColorError -f $BorderColor)
        }
    }
    process
    {
        $pscriboDocument.Properties['TableStyles']++;
        $tableStyle = [PSCustomObject] @{
            Id                = $Id.Replace(' ', $pscriboDocument.Options['SpaceSeparator'])
            Name              = $Id
            HeaderStyle       = $HeaderStyle
            RowStyle          = $RowStyle
            AlternateRowStyle = $AlternateRowStyle
            CaptionStyle      = $CaptionStyle
            PaddingTop        = ConvertTo-Mm -Point $PaddingTop
            PaddingLeft       = ConvertTo-Mm -Point $PaddingLeft
            PaddingBottom     = ConvertTo-Mm -Point $PaddingBottom
            PaddingRight      = ConvertTo-Mm -Point $PaddingRight
            Align             = $Align
            BorderWidth       = ConvertTo-Mm -Point $BorderWidth
            BorderStyle       = $borderStyle
            BorderColor       = Resolve-PScriboStyleColor -Color $BorderColor
            CaptionPrefix     = $CaptionPrefix
            CaptionLocation   = $CaptionLocation
        }
        $pscriboDocument.TableStyles[$Id] = $tableStyle
        if ($Default)
        {
            $pscriboDocument.DefaultTableStyle = $tableStyle.Id
        }
    }
}

function ConvertFrom-NumberStyle
{
<#
    .SYNOPSIS
        Converts a number to its string representation, based upon the number style
#>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Int32] $Value,

        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $NumberStyle
    )
    process
    {
        switch ($NumberStyle.Format)
        {
            'Number'
            {
                $numberString = $Value.ToString()
            }
            'Letter'
            {
                $numberString = [System.Char] ($Value + 64)
            }
            'Roman'
            {
                $numberString = ConvertTo-RomanNumeral -Value $Value
            }
            'Custom'
            {
                $numberCount = 0
                $customStringChars = $numberStyle.Custom.ToCharArray()
                $customStringLength = $numberStyle.Custom.Length
                for ($n = $customStringLength - 1; $n -ge 0; $n--)
                {
                    if ($customStringChars[$n] -eq '%')
                    {
                        $numberCount++
                    }
                }

                $searchString = ''.PadRight($numberCount, '%')
                $replacementString = $Value.ToString().PadLeft($numberCount, '0')
                return $numberStyle.Custom.Replace($searchString, $replacementString)
            }
        }

        $numberString = '{0}{1}' -f $numberString, $NumberStyle.Suffix
        if ($NumberStyle.Uppercase)
        {
            return $numberString.ToUpper()
        }
        else
        {
            return $numberString.ToLower()
        }
    }
}

function ConvertTo-Em
{
<#
    .SYNOPSIS
        Convert pixels or millimeters into EMU
#>

    [CmdletBinding()]
    [OutputType([System.Single])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Pixel')]
        [Alias('px')]
        [System.Single] $Pixel,

        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Millimeter')]
        [Alias('mm','Millimetre')]
        [System.Single] $Millimeter
    )
    process
    {
        if ($PSCmdlet.ParameterSetName -eq 'Pixel')
        {
            $em = $pixel * 9525
            return [System.Math]::Round($em, 0)
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'Millimeter')
        {
            $em = $Millimeter / 4.23333333333333
            return [System.Math]::Round($em, 2)
        }
    }
}

function ConvertTo-Image
{
<#
    .SYNOPSIS
        Creates an image from a byte[]
#>

    [CmdletBinding()]
    [OutputType([System.Drawing.Image])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Byte[]] $Bytes
    )
    process
    {
        try
        {
            [System.IO.MemoryStream] $memoryStream = New-Object -TypeName 'System.IO.MemoryStream' -ArgumentList @(,$Bytes)
            [System.Drawing.Image] $image = [System.Drawing.Image]::FromStream($memoryStream)
            Write-Output -InputObject $image
        }
        catch
        {
            $_
        }
        finally
        {
            if ($null -ne $memoryStream)
            {
                $memoryStream.Close()
            }
        }
    }
}

function ConvertTo-In
{
<#
    .SYNOPSIS
        Convert millimeters into inches
#>

    [CmdletBinding()]
    [OutputType([System.Single])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [Alias('mm','Millimetre')]
        [System.Single] $Millimeter
    )
    process
    {
        $in = $Millimeter / 25.4
        return [System.Math]::Round($in, 2)
    }
}

function ConvertTo-InvariantCultureString
{
<#
    .SYNOPSIS
        Convert to a number to a string with a culture-neutral representation #6, #42.
#>

    [CmdletBinding()]
    param
    (
        ## The sinle/double
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.Object] $Object,

        ## Format string
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $Format
    )
    process
    {
        if ($PSBoundParameters.ContainsKey('Format'))
        {
            return $Object.ToString($Format, [System.Globalization.CultureInfo]::InvariantCulture);
        }
        else
        {
            return $Object.ToString([System.Globalization.CultureInfo]::InvariantCulture);
        }
    }
}

function ConvertTo-Mm
{
<#
    .SYNOPSIS
        Convert points, inches or pixels into millimeters
#>

    [CmdletBinding()]
    [OutputType([System.Single])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Point')]
        [Alias('pt')]
        [System.Single] $Point,

        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Inch')]
        [Alias('in')]
        [System.Single] $Inch,

        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Pixel')]
        [Alias('px')]
        [System.Single] $Pixel,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Pixel')]
        [System.Int16] $Dpi = 96
    )
    process
    {
        if ($PSCmdlet.ParameterSetName -eq 'Point')
        {
            return [System.Math]::Round(($Point / 72) * 25.4, 2)
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'Inch')
        {
            $mm = $Inch * 25.4
            return [System.Math]::Round($mm, 2)
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'Pixel')
        {
            $mm = (25.4 / $Dpi) * $Pixel
            return [System.Math]::Round($mm, 2)
        }
    }
}

function ConvertTo-Octips
{
<#
    .SYNOPSIS
        Convert millimeters into octips

    .NOTES
        1 "octip" = 1/8th pt
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    [OutputType([System.Single])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [Alias('mm','Millimetre')]
        [System.Single] $Millimeter
    )
    process
    {
        $octips = (ConvertTo-In -Millimeter $Millimeter) * 576
        return [System.Math]::Round($octips, 2)
    }
}

function ConvertTo-PScriboFormattedKeyedListTable
{
<#
    .SYNOPSIS
        Creates a formatted keyed list table (a key'd column per object) for plugin output/rendering.

    .NOTES
        Maintains backwards compatibility with other plugins that do not require styling/formatting.
#>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSObject[]])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $Table
    )
    begin
    {
        $hasColumnWidths = ($null -ne $Table.ColumnWidths)
        $objectKey = $Table.ListKey
    }
    process
    {
        $formattedTable = New-PScriboFormattedTable -Table $Table -HasHeaderRow -HasHeaderColumn

        ## Output the header cells
        $headerRow = New-PScriboFormattedTableRow -TableStyle $Table.Style -IsHeaderRow

        if ($Table.DisplayListKey)
        {
            $newPScriboFormattedTableRowHeaderCellParams = @{
                Content = $Table.ListKey
            }
        }
        else
        {
            $newPScriboFormattedTableRowHeaderCellParams = @{
                Content = ' '
            }
        }
        $listKeyHeaderCell = New-PScriboFormattedTableRowCell @newPScriboFormattedTableRowHeaderCellParams
        $null = $headerRow.Cells.Add($listKeyHeaderCell)

        ## Add all the object key values
        for ($o = 0; $o -lt $Table.Rows.Count; $o++)
        {
            $newPScriboFormattedTableRowHeaderCellParams = @{
                Content = $Table.Rows[$o].PSObject.Properties[$objectKey].Value
            }
            $objectKeyCell = New-PScriboFormattedTableRowCell @newPScriboFormattedTableRowHeaderCellParams
            $null = $headerRow.Cells.Add($objectKeyCell)
        }
        $null = $formattedTable.Rows.Add($headerRow)

        $isAlternateRow = $false
        ## Output remaining object properties (one property per row)
        foreach ($column in $Table.Columns)
        {
            if ((-not $column.EndsWith('__Style', 'CurrentCultureIgnoreCase')) -and
                ($column -ne $objectKey))
            {
                ## Add the object property column
                $newPScriboFormattedTableRowParams = @{
                    TableStyle = $Table.Style;
                    IsAlternateRow = $isAlternateRow
                }
                $row = New-PScriboFormattedTableRow @newPScriboFormattedTableRowParams

                ## Output the column header cell (property name) as header style
                $newPScriboFormattedTableColumnCellParams = @{
                    Content = $column
                }
                if ($hasColumnWidths) {
                    $newPScriboFormattedTableColumnCellParams['Width'] = $Table.ColumnWidths[0]
                }
                $columnCell = New-PScriboFormattedTableRowCell @newPScriboFormattedTableColumnCellParams
                $null = $row.Cells.Add($columnCell)

                ## Add the property value for all other objects
                for ($o = 0; $o -lt $Table.Rows.Count; $o++)
                {
                    $propertyValue = $Table.Rows[$o].PSObject.Properties[$column].Value
                    $newPScriboFormattedTableRowValueCellParams = @{
                        Content = $propertyValue
                    }
                    if ([System.String]::IsNullOrEmpty($propertyValue)) {
                        $newPScriboFormattedTableRowValueCellParams['Content'] = $null
                    }
                    $propertyStyleName = '{0}__Style' -f $column
                    $hasStyleProperty = $Table.Rows[$o].PSObject.Properties.Name.Contains($propertyStyleName)
                    if ($hasStyleProperty) {
                        $newPScriboFormattedTableRowValueCellParams['Style'] = $Table.Rows[$o].PSObject.Properties[$propertyStyleName].Value
                    }
                    if ($hasColumnWidths) {
                        $newPScriboFormattedTableRowValueCellParams['Width'] = $Table.ColumnWidths[$o+1]
                    }
                    $valueCell = New-PScriboFormattedTableRowCell @newPScriboFormattedTableRowValueCellParams
                    $null = $row.Cells.Add($valueCell)
                }

                $null = $formattedTable.Rows.Add($row)
                $isAlternateRow = -not $isAlternateRow
            }
        }
        Write-Output -InputObject $formattedTable
    }
}

function ConvertTo-PScriboFormattedListTable
{
<#
    .SYNOPSIS
        Creates a formatted list table for plugin output/rendering.

    .NOTES
        Maintains backwards compatibility with other plugins that do not require styling/formatting.
#>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSObject[]])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $Table
    )
    begin
    {
        $hasColumnWidths = ($null -ne $Table.ColumnWidths)
    }
    process
    {
        for ($r = 0; $r -lt $Table.Rows.Count; $r++)
        {
            ## We have one table per object
            $formattedTable = New-PScriboFormattedTable -Table $Table -HasHeaderColumn
            $objectProperties = $Table.Rows[$r].PSObject.Properties

            for ($c = 0; $c -lt $Table.Columns.Count; $c++)
            {
                $column = $Table.Columns[$c]
                $newPScriboFormattedTableRowParams = @{
                    TableStyle = $Table.Style;
                    Style = $Table.Rows[$r].'__Style'
                    IsAlternateRow = ($c % 2) -ne 0
                }
                $row = New-PScriboFormattedTableRow @newPScriboFormattedTableRowParams

                ## Add each property name (as header style)
                $newPScriboFormattedTableRowHeaderCellParams = @{
                    Content = $column
                }
                if ($hasColumnWidths)
                {
                    $newPScriboFormattedTableRowHeaderCellParams['Width'] = $Table.ColumnWidths[0]
                }
                $headerCell = New-PScriboFormattedTableRowCell @newPScriboFormattedTableRowHeaderCellParams
                $null = $row.Cells.Add($headerCell)

                ## Add each property value
                $propertyValue = $objectProperties[$column].Value
                if ([System.String]::IsNullOrEmpty($propertyValue))
                {
                    $propertyValue = $null
                }

                $newPScriboFormattedTableRowValueCellParams = @{
                    Content = $propertyValue
                }

                $propertyStyleName = '{0}__Style' -f $column
                $hasStyleProperty = $objectProperties.Name.Contains($propertyStyleName)
                if ($hasStyleProperty)
                {
                    $newPScriboFormattedTableRowValueCellParams['Style'] = $objectProperties[$propertyStyleName].Value
                }
                if ($hasColumnWidths)
                {
                    $newPScriboFormattedTableRowValueCellParams['Width'] = $Table.ColumnWidths[1]
                }
                $valueCell = New-PScriboFormattedTableRowCell @newPScriboFormattedTableRowValueCellParams
                $null = $row.Cells.Add($valueCell)

                $null = $formattedTable.Rows.Add($row)
            }
            Write-Output -InputObject $formattedTable
        }
    }
}

function ConvertTo-PScriboFormattedTable
{
<#
    .SYNOPSIS
        Creates a formatted standard table for plugin output/rendering.

    .NOTES
        Maintains backwards compatibility with other plugins that do not require styling/formatting.
#>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSObject])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $Table
    )
    begin
    {
        $hasColumnWidths = ($null -ne $Table.ColumnWidths)
    }
    process
    {
        $formattedTable = New-PScriboFormattedTable -Table $Table -HasHeaderRow

        # Output the header row and header cells
        $headerRow = New-PScriboFormattedTableRow -TableStyle $Table.Style -IsHeaderRow
        for ($h = 0; $h -lt $Table.Columns.Count; $h++)
        {
            $newPScriboFormattedTableHeaderCellParams = @{
                Content    = $Table.Columns[$h]
            }
            if ($hasColumnWidths)
            {
                $newPScriboFormattedTableHeaderCellParams['Width'] = $Table.ColumnWidths[$h]
            }
            $cell = New-PScriboFormattedTableRowCell @newPScriboFormattedTableHeaderCellParams
            $null = $headerRow.Cells.Add($cell)
        }
        $null = $formattedTable.Rows.Add($headerRow)

        ## Output each object row
        for ($r = 0; $r -lt $Table.Rows.Count; $r++)
        {
            $objectProperties = $Table.Rows[$r].PSObject.Properties

            $newPScriboFormattedTableRowParams = @{
                TableStyle = $Table.Style;
                Style = $Table.Rows[$r].'__Style'
                IsAlternateRow = ($r % 2 -ne 0 )
            }
            $row = New-PScriboFormattedTableRow @newPScriboFormattedTableRowParams

            ## Output object row's cells
            for ($c = 0; $c -lt $Table.Columns.Count; $c++)
            {
                $propertyName = $Table.Columns[$c]
                $propertyStyleName = '{0}__Style' -f $propertyName;
                $hasStyleProperty = $objectProperties.Name.Contains($propertyStyleName)

                $propertyValue = $objectProperties[$propertyName].Value

                $newPScriboFormattedTableRowCellParams = @{
                    Content = $propertyValue
                }
                if ([System.String]::IsNullOrEmpty($propertyValue))
                {
                    $newPScriboFormattedTableRowCellParams['Content'] = $null
                }

                if ($hasColumnWidths)
                {
                    $newPScriboFormattedTableRowCellParams['Width'] = $Table.ColumnWidths[$c]
                }
                if ($hasStyleProperty)
                {
                    $newPScriboFormattedTableRowCellParams['Style'] = $objectProperties[$propertyStyleName].Value # | Where-Object Name -eq $propertyStyleName | Select-Object -ExpandProperty Value
                }

                $cell = New-PScriboFormattedTableRowCell @newPScriboFormattedTableRowCellParams
                $null = $row.Cells.Add($cell)
            }
            $null = $formattedTable.Rows.Add($row)
        }
        Write-Output -InputObject $formattedTable
    }
}

function ConvertTo-PScriboPreformattedTable
{
<#
    .SYNOPSIS
        Creates a formatted table based upon table type for plugin output/rendering.

    .NOTES
        Maintains backwards compatibility with other plugins that do not require styling/formatting.
#>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSObject])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $Table
    )
    process
    {
        if ($Table.IsKeyedList)
        {
            Write-Output -InputObject (ConvertTo-PScriboFormattedKeyedListTable -Table $Table)
        }
        elseif ($Table.IsList)
        {
            Write-Output -InputObject (ConvertTo-PScriboFormattedListTable -Table $Table)
        }
        else
        {
            Write-Output -InputObject (ConvertTo-PScriboFormattedTable -Table $Table)
        }
    }
}

function ConvertTo-PSObjectKeyedListTable
{
<#
    .SYNOPSIS
        Converts a PScribo.Table to a [PSCustomObject] collection representing a keyed list table.
#>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSObject])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $Table
    )
    process
    {
        $listKey = $Table.ListKey
        $rowHeaders = $Table.Rows | Select-Object -ExpandProperty $listKey
        $columnHeaders = $Table.Rows |
                            Select-Object -First 1 -Property * -ExcludeProperty '*__Style' |
                                ForEach-Object { $_.PSObject.Properties.Name } |
                                    Where-Object { $_ -ne $listKey }

        foreach ($columnHeader in $columnHeaders)
        {
            $psCustomObjectParams = [Ordered] @{
                $listKey = $columnHeader
            }
            foreach ($rowHeader in $rowHeaders)
            {
                $psCustomObjectParams[$rowHeader] = $Table.Rows |
                    Where-Object { $_.$listKey -eq $rowHeader } |
                        Select-Object -ExpandProperty $columnHeader
            }
            $psCustomObject = [PSCustomObject] $psCustomObjectParams
            Write-Output -InputObject $psCustomObject
        }
    }
}

function ConvertTo-Pt
{
<#
    .SYNOPSIS
        Convert millimeters into points
#>

    [CmdletBinding()]
    [OutputType([System.Single])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [Alias('mm','Millimetre')]
        [System.Single] $Millimeter
    )
    process
    {
        $pt = (ConvertTo-In -Millimeter $Millimeter) / 0.0138888888888889
        return [System.Math]::Round($pt, 2)
    }
}

function ConvertTo-Px
{
<#
    .SYNOPSIS
        Convert millimeters into pixels (default 96dpi)
#>

    [CmdletBinding()]
    [OutputType([System.Int16])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [Alias('mm','Millimetre')]
        [System.Single] $Millimeter,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int16] $Dpi = 96
    )
    process
    {
        $px = [System.Int16] ((ConvertTo-In -Millimeter $Millimeter) * $Dpi)
        if ($px -lt 1)
        {
            return (1 -as [System.Int16])
        }
        else
        {
            return $px
        }
    }
}

function ConvertTo-RomanNumeral
{
<#
    .SYNOPSIS
        Converts a decimal number to Roman numerals.
#>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.Int32] $Value
    )
    begin
    {
        $conversionTable = [ordered] @{
            1000 = 'M'
            900  = 'CM'
            500  = 'D'
            400  = 'CD'
            100  = 'C'
            90   = 'XC'
            50   = 'L'
            40   = 'XL'
            10   = 'X'
            9    = 'IX'
            5    = 'V'
            4    = 'IV'
            1    = 'I'
        }
    }
    process
    {
        $romanNumeralBuilder = New-Object -TypeName System.Text.StringBuilder
        do
        {
            foreach ($romanNumeral in $conversionTable.GetEnumerator())
            {
                if ($Value -ge $romanNumeral.Key)
                {
                    [ref] $null = $romanNumeralBuilder.Append($romanNumeral.Value)
                    $Value -= $romanNumeral.Key
                    break
                }
            }

        }
        until ($Value -eq 0)
        return $romanNumeralBuilder.ToString()
    }
}

function ConvertTo-Twips
{
<#
    .SYNOPSIS
        Convert millimeters into twips

    .NOTES
        1 twip = 1/20th pt
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    [OutputType([System.Single])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [Alias('mm','Millimetre')]
        [System.Single] $Millimeter
    )
    process
    {
        $twips = (ConvertTo-In -Millimeter $Millimeter) * 1440
        return [System.Math]::Round($twips, 2)
    }
}

function Copy-Object
{
<#
    .SYNOPSIS
        Clones a .NET object by serializing and deserializing it.
    .NOTES
        PowerShell 7.4 throws a "Type 'System.Management.Automation.PSObject' is not marked as serializable." exception.
        This function has been replaced by 'ConvertTo-Json | ConvertFrom-Json' to serialize and deserialize into a
        "clone" object. This works for PScribo objects but may not work for other nested objects in script blocks.
#>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.Object] $InputObject
    )
    process
    {
        try
        {
            $stream = New-Object IO.MemoryStream
            $formatter = New-Object Runtime.Serialization.Formatters.Binary.BinaryFormatter
            $formatter.Serialize($stream, $InputObject)
            $stream.Position = 0
            return $formatter.Deserialize($stream)
        }
        catch
        {
            Write-Error -ErrorRecord $_
        }
        finally
        {
            $stream.Dispose()
        }
    }
}

function Get-ImageMimeType
{
<#
    .SYNOPSIS
        Returns an image's Mime type
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Drawing.Image] $Image
    )
    process
    {
        if ($Image.RawFormat.Equals([System.Drawing.Imaging.ImageFormat]::Jpeg))
        {
            return 'image/jpeg'
        }
        elseif ($Image.RawFormat.Equals([System.Drawing.Imaging.ImageFormat]::Png))
        {
            return 'image/png'
        }
        elseif ($Image.RawFormat.Equals([System.Drawing.Imaging.ImageFormat]::Bmp))
        {
            return 'image/bmp'
        }
        elseif ($Image.RawFormat.Equals([System.Drawing.Imaging.ImageFormat]::Emf))
        {
            return 'image/emf'
        }
        elseif ($Image.RawFormat.Equals([System.Drawing.Imaging.ImageFormat]::Gif))
        {
            return 'image/gif'
        }
        elseif ($Image.RawFormat.Equals([System.Drawing.Imaging.ImageFormat]::Icon))
        {
            return 'image/icon'
        }
        elseif ($Image.RawFormat.Equals([System.Drawing.Imaging.ImageFormat]::Tiff))
        {
            return 'image/tiff'
        }
        elseif ($Image.RawFormat.Equals([System.Drawing.Imaging.ImageFormat]::Wmf))
        {
            return 'image/wmf'
        }
        elseif ($Image.RawFormat.Equals([System.Drawing.Imaging.ImageFormat]::Exif))
        {
            return 'image/exif'
        }
        else
        {
            return 'image/unknown'
        }
    }
}

function Get-PScriboDocumentStyle
{
<#
    .SYNOPSIS
        Returns document style or table style.

    .NOTES
        Enables testing without having to generate a mock document object!
#>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Style')]
        [System.String] $Style,

        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'TableStyle')]
        [System.String] $TableStyle
    )
    process
    {
        if ($PSCmdlet.ParameterSetName -eq 'Style')
        {
            return $Document.Styles[$Style]
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'TableStyle')
        {
            return $Document.TableStyles[$TableStyle]
        }
    }
}

function Get-PScriboHeaderFooter
{
<#
    .SYNOPSIS
        Returns the specified PScribo header/footer object.
#>

    [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
    {
        if ($FirstPage)
        {
            if ($Header -and ($Document.Header.HasFirstPageHeader))
            {
                return $Document.Header.FirstPageHeader
            }
            elseif ($Footer -and ($Document.Footer.HasFirstPageFooter))
            {
                return $Document.Footer.FirstPageFooter
            }
        }
        else
        {
            if ($Header -and ($Document.Header.HasDefaultHeader))
            {
                return $Document.Header.DefaultHeader
            }
            elseif ($Footer -and ($Document.Footer.HasDefaultFooter))
            {
                return $Document.Footer.DefaultFooter
            }
        }
    }
}

function Get-PScriboImage
{
<#
    .SYNOPSIS
        Retrieves PScribo.Images in a document/section
#>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSObject])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.Management.Automation.PSObject[]] $Section,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String[]] $Id
    )
    process
    {
        foreach ($subSection in $Section)
        {
            if ($subSection.Type -eq 'PScribo.Image')
            {
                if ($PSBoundParameters.ContainsKey('Id'))
                {
                    if ($subSection.Id -in $Id)
                    {
                        Write-Output -InputObject $subSection
                    }
                }
                else
                {
                    Write-Output -InputObject $subSection
                }
            }
            elseif ($subSection.Type -eq 'PScribo.Section')
            {
                if ($subSection.Sections.Count -gt 0)
                {
                    ## Recursively search subsections
                    $PSBoundParameters['Section'] = $subSection.Sections
                    Get-PScriboImage @PSBoundParameters
                }
            }
        }
    }
}

function Get-PScriboParagraphRun
{
<#
    .SYNOPSIS
        Converts a string of text into multiple strings to create Word
        paragraph runs for PageNumber and TotalPages field replacements.
#>

    [CmdletBinding()]
    param
    (
        ## Paragraph text to convert into Word paragraph runs for field replacement
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $Text,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.Xml.XmlDocument] $XmlDocument,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.Xml.XmlElement] $XmlElement
    )
    process
    {
        $xmlns = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'

        $pageNumberMatch = '<!#\s*PageNumber\s*#!>'
        $totalPagesMatch = '<!#\s*TotalPages\s*#!>'
        $paragraphRuns = New-Object -TypeName 'System.Collections.ArrayList'

        $pageNumbers = $Text -split $pageNumberMatch
        for ($pn = 0; $pn -lt $pageNumbers.Count; $pn++)
        {
            $totalPages = $pageNumbers[$pn] -split $totalPagesMatch
            for ($tp = 0; $tp -lt $totalPages.Count; $tp++)
            {
                $null = $paragraphRuns.Add($totalPages[$tp])

                if ($tp -lt ($totalPages.Count -1))
                {
                    $null = $paragraphRuns.Add('<!#TOTALPAGES#!>')
                }
            }

            if ($pn -lt ($pageNumbers.Count -1))
            {
                $null = $paragraphRuns.Add('<!#PAGENUMBER#!>')
            }
        }

        foreach ($run in $paragraphRuns)
        {
            if ($run -match '<!#(TOTALPAGES|PAGENUMBER)#!>')
            {
                $r1 = $XmlElement.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlns))
                $fldChar1 = $r1.AppendChild($XmlDocument.CreateElement('w', 'fldChar', $xmlns))
                [ref] $null = $fldChar1.SetAttribute('fldCharType', $xmlns, 'begin')

                $r2 = $XmlElement.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlns))
                $instrText = $r2.AppendChild($XmlDocument.CreateElement('w', 'instrText', $xmlns))
                [ref] $null = $instrText.SetAttribute('space', 'http://www.w3.org/XML/1998/namespace', 'preserve')

                if ($run -match '<!#PAGENUMBER#!>')
                {
                    [ref] $null = $instrText.AppendChild($XmlDocument.CreateTextNode(' PAGE \* MERGEFORMAT '))
                }
                elseif ($run -match '<!#TOTALPAGES#!>')
                {
                    [ref] $null = $instrText.AppendChild($XmlDocument.CreateTextNode(' NUMPAGES \* MERGEFORMAT '))
                }

                $r3 = $XmlElement.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlns))
                $fldChar2 = $r3.AppendChild($XmlDocument.CreateElement('w', 'fldChar', $xmlns))
                [ref] $null = $fldChar2.SetAttribute('fldCharType', $xmlns, 'separate')

                $r4 = $XmlElement.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlns))
                $t2 = $r4.AppendChild($XmlDocument.CreateElement('w', 't', $xmlns))
                [ref] $null = $t2.AppendChild($XmlDocument.CreateTextNode('1'))

                $r5 = $XmlElement.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlns))
                $fldChar3 = $r5.AppendChild($XmlDocument.CreateElement('w', 'fldChar', $xmlns))
                [ref] $null = $fldChar3.SetAttribute('fldCharType', $xmlns, 'end')
            }
            else
            {
                $r = $XmlElement.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlns))
                $t = $r.AppendChild($XmlDocument.CreateElement('w', 't', $xmlns))
                [ref] $null = $t.SetAttribute('space', 'http://www.w3.org/XML/1998/namespace', 'preserve')
                [ref] $null = $t.AppendChild($XmlDocument.CreateTextNode($run))
            }
        }
    }
}

function Get-PScriboPlugin
{
<#
    .SYNOPSIS
        Returns available PScribo plugins.
#>

    [CmdletBinding()]
    param ( )
    process
    {
        Get-ChildItem -Path (Join-Path -Path $pscriboRoot -ChildPath '\Src\Plugins') |
            Select-Object -ExpandProperty Name
    }
}

function Get-UriBytes
{
<#
    .SYNOPSIS
        Gets an image's content as a byte[]
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    [OutputType([System.Byte[]])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.Uri] $Uri
    )
    process
    {
        try
        {
            $webClient = New-Object -TypeName 'System.Net.WebClient'
            [System.IO.Stream] $contentStream = $webClient.OpenRead($uri.AbsoluteUri)
            [System.IO.MemoryStream] $memoryStream = New-Object System.IO.MemoryStream
            $contentStream.CopyTo($memoryStream)
            return $memoryStream.ToArray()
        }
        catch
        {
            $_
        }
        finally
        {
            if ($null -ne $memoryStream) { $memoryStream.Close() }
            if ($null -ne $contentStream) { $contentStream.Close() }
            if ($null -ne $webClient) { $webClient.Dispose() }
        }
    }
}

function Invoke-PScriboListLevel
{
<#
    .SYNOPSIS
        Nested function that processes each nested list numbering.
#>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [System.Management.Automation.PSObject] $List,

        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [System.String] $Number
    )
    process
    {
        $level = $Number.Split('.').Count +1
        $processingListPadding = ''.PadRight($level -1, ' ')
        $processingListMessage = $localized.ProcessingList -f $List.Name
        Write-PScriboMessage -Message ('{0}{1}' -f $processingListPadding, $processingListMessage)

        $itemNumber = 0
        $numberString = $Number
        $hasItem = $false

        foreach ($item in $List.Items)
        {
            if ($item.Type -eq 'PScribo.Item')
            {
                $itemNumber++
                $item.Level = $level
                $item.Index = $itemNumber
                $item.Number = ('{0}.{1}' -f $Number, $itemNumber).TrimStart('.')

                $numberString = $item.Number
                $hasItem = $true
            }
            elseif ($item.Type -eq 'PScribo.List')
            {
                if ($hasItem)
                {
                    Invoke-PScriboListLevel -List $item -Number $numberString
                }
                else
                {
                    Write-PScriboMessage -Message $localized.NoPriorListItemWarning -IsWarning
                }
            }
        }
    }
}

function Invoke-PScriboSection
{
<#
    .SYNOPSIS
        Processes the document/TOC section versioning each level, i.e. 1.2.2.3

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding()]
    param ( )
    process
    {
        $majorNumber = 1;
        foreach ($s in $pscriboDocument.Sections)
        {
            if ($s.Type -like '*.Section')
            {
                if ($pscriboDocument.Options['ForceUppercaseSection'])
                {
                    $s.Name = $s.Name.ToUpper();
                }
                if (-not $s.IsExcluded)
                {
                    Invoke-PScriboSectionLevel -Section $s -Number $majorNumber;
                    $majorNumber++;
                }
            }
        }
    }
}

function Invoke-PScriboSectionLevel
{
<#
    .SYNOPSIS
        Nested function that processes each document/TOC nested section
#>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [System.Management.Automation.PSObject] $Section,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Number
    )
    process
    {
        if ($pscriboDocument.Options['ForceUppercaseSection'])
        {
            $Section.Name = $Section.Name.ToUpper();
        }

        ## Set this section's level
        $Section.Number = $Number;
        $Section.Level = $Number.Split('.').Count -1;
        ### Add to the TOC
        $tocEntry = [PScustomObject] @{ Id = $Section.Id; Number = $Number; Level = $Section.Level; Name = $Section.Name; }
        [ref] $null = $pscriboDocument.TOC.Add($tocEntry);
        ## Set sub-section level seed
        $minorNumber = 1;
        foreach ($s in $Section.Sections)
        {
            if ($s.Type -like '*.Section' -and -not $s.IsExcluded)
            {
                $sectionNumber = ('{0}.{1}' -f $Number, $minorNumber).TrimStart('.');  ## Calculate section version
                Invoke-PScriboSectionLevel -Section $s -Number $sectionNumber;
                $minorNumber++;
            }
        } #end foreach section
    }
}

function Merge-PScriboPluginOption
{
<#
    .SYNOPSIS
        Merges the specified options along with the plugin-specific default options.
#>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        ## Default/document options
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.Collections.Hashtable] $DocumentOptions,

        ## Default plugin options to merge
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable] $DefaultPluginOptions,

        ## Specified runtime plugin options
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.Collections.Hashtable] $PluginOptions
    )
    process
    {
        $mergedOptions = $DocumentOptions.Clone();

        if ($null -ne $DefaultPluginOptions)
        {
            ## Overwrite the default document option with the plugin default option/value
            foreach ($option in $DefaultPluginOptions.GetEnumerator())
            {
                $mergedOptions[$($option.Key)] = $option.Value;
            }
        }

        if ($null -ne $PluginOptions)
        {
            ## Overwrite the default document/plugin default option/value with the specified/runtime option
            foreach ($option in $PluginOptions.GetEnumerator())
            {
                $mergedOptions[$($option.Key)] = $option.Value;
            }
        }
        return $mergedOptions;
    }
}

function New-PScriboBlankLine
{
<#
    .SYNOPSIS
        Initializes a new PScribo blank line break.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        [Parameter(ValueFromPipeline)]
        [System.UInt32] $Count = 1
    )
    process
    {
        $typeName = 'PScribo.BlankLine';
        $pscriboDocument.Properties['BlankLines']++;
        $pscriboBlankLine = [PSCustomObject] @{
            Id = [System.Guid]::NewGuid().ToString();
            LineCount = $Count;
            Type = $typeName;
        }
        return $pscriboBlankLine;
    }
}

function New-PScriboDocument
{
<#
    .SYNOPSIS
        Initializes a new PScript document object.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseLiteralInitializerForHashtable','')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        ## PScribo document name
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        ## PScribo document Id
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String] $Id = $Name.Replace(' ','')
    )
    begin
    {
        if ($(Test-CharsInPath -Path $Name -SkipCheckCharsInFolderPart -Verbose:$false) -eq 3 )
        {
            throw -Message ($localized.IncorrectCharsInName)
        }
    }
    process
    {
        Write-PScriboMessage -Message ($localized.DocumentProcessingStarted -f $Name)
        $typeName = 'PScribo.Document'
        $pscriboDocument = [PSCustomObject] @{
            Id                 = $Id.ToUpper()
            Type               = $typeName
            Name               = $Name
            Sections           = New-Object -TypeName System.Collections.ArrayList
            Options            = New-Object -TypeName System.Collections.Hashtable([System.StringComparer]::InvariantCultureIgnoreCase)
            Properties         = New-Object -TypeName System.Collections.Hashtable([System.StringComparer]::InvariantCultureIgnoreCase)
            Styles             = New-Object -TypeName System.Collections.Hashtable([System.StringComparer]::InvariantCultureIgnoreCase)
            TableStyles        = New-Object -TypeName System.Collections.Hashtable([System.StringComparer]::InvariantCultureIgnoreCase)
            NumberStyles       = New-Object -TypeName System.Collections.Hashtable([System.StringComparer]::InvariantCultureIgnoreCase)
            Lists              = New-Object -TypeName System.Collections.ArrayList # Store all list references for Word numbering.xml generation
            DefaultStyle       = $null
            DefaultTableStyle  = $null
            DefaultNumberStyle = $null
            Header             = [PSCustomObject] @{
                                     HasFirstPageHeader = $false
                                     HasDefaultHeader   = $false
                                     FirstPageHeader    = $null
                                     DefaultHeader      = $null
                                 }
            Footer             = [PSCustomObject] @{
                                     HasFirstPageFooter = $false
                                     HasDefaultFooter   = $false
                                     FirstPageFooter    = $null
                                     DefaultFooter      = $null
                                 }
            TOC                = New-Object -TypeName System.Collections.ArrayList
        }
        $defaultDocumentOptionParams = @{
            MarginTopAndBottom = 72
            MarginLeftAndRight = 54
            Orientation        = 'Portrait'
            PageSize           = 'A4'
            DefaultFont        = 'Calibri','Candara','Segoe','Segoe UI','Optima','Arial','Sans-Serif'
        }
        DocumentOption @defaultDocumentOptionParams -Verbose:$false

        ## Set "default" styles
        Style -Name Normal -Size 11 -Default -Verbose:$false
        Style -Name Title -Size 28 -Color '0072af' -Verbose:$false
        Style -Name TOC -Size 16 -Color '0072af' -Hide -Verbose:$false
        Style -Name 'Heading 1' -Size 16 -Color '0072af' -Verbose:$false
        Style -Name 'Heading 2' -Size 14 -Color '0072af' -Verbose:$false
        Style -Name 'Heading 3' -Size 12 -Color '0072af' -Verbose:$false
        Style -Name 'Heading 4' -Size 11 -Color '2f5496' -Italic -Verbose:$false
        Style -Name 'Heading 5' -Size 11 -Color '2f5496' -Verbose:$false
        Style -Name 'Heading 6' -Size 11 -Color '1f3763' -Verbose:$false
        Style -Name TableDefaultHeading -Size 11 -Color 'fff' -BackgroundColor '4472c4' -Bold -Verbose:$false
        Style -Name TableDefaultRow -Size 11 -Verbose:$false
        Style -Name TableDefaultAltRow -Size 11 -BackgroundColor 'd0ddee' -Verbose:$false
        Style -Name Caption -Size 11 -Italic -Verbose:$false
        $tableDefaultStyleParams = @{
            Id                = 'TableDefault'
            BorderWidth       = 1
            BorderColor       = '2a70be'
            HeaderStyle       = 'TableDefaultHeading'
            RowStyle          = 'TableDefaultRow'
            AlternateRowStyle = 'TableDefaultAltRow'
            CaptionStyle      = 'Caption'
        }
        TableStyle @tableDefaultStyleParams -Default -Verbose:$false
        NumberStyle -Id 'Number' -Format Number -Default -Verbose:$false
        NumberStyle -Id 'Letter' -Format Letter -Verbose:$false
        NumberStyle -Id 'Roman' -Format Roman -Verbose:$false
        return $pscriboDocument
    }
}

function New-PScriboFormattedTable
{
<#
    .SYNOPSIS
        Creates a formatted table with row and column styling for plugin output/rendering.

    .NOTES
        Maintains backwards compatibility with other plugins that do not require styling/formatting.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSObject])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $Table,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $HasHeaderRow,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $HasHeaderColumn
    )
    process
    {
        $tableStyle = $document.TableStyles[$Table.Style]
        return [PSCustomObject] @{
            Id              = $Table.Id
            Name            = $Table.Name
            Number          = $Table.Number
            Type            = $Table.Type
            ColumnWidths    = $Table.ColumnWidths
            Rows            = New-Object -TypeName System.Collections.ArrayList
            Width           = $Table.Width
            Tabs            = if ($null -eq $Table.Tabs) { 0 } else { $Table.Tabs }
            PaddingTop      = $tableStyle.PaddingTop
            PaddingLeft     = $tableStyle.PaddingLeft
            PaddingBottom   = $tableStyle.PaddingBottom;
            PaddingRight    = $tableStyle.PaddingRight
            Align           = $tableStyle.Align
            BorderWidth     = $tableStyle.BorderWidth
            BorderStyle     = $tableStyle.BorderStyle
            BorderColor     = $tableStyle.BorderColor
            Style           = $Table.Style
            HasHeaderRow    = $HasHeaderRow  ## First row has header style applied (list/combo list tables)
            HasHeaderColumn = $HasHeaderColumn  ## First column has header style applied (list/combo list tables)
            HasCaption      = $Table.HasCaption
            Caption         = $Table.Caption
            IsList          = $Table.IsList
            IsKeyedList     = $Table.IsKeyedList
        }
    }
}

function New-PScriboFormattedTableRow
{
<#
    .SYNOPSIS
        Creates a formatted table row for plugin output/rendering.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSObject])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $TableStyle,

        [Parameter(ValueFromPipeline)]
        [AllowNull()]
        [System.String] $Style = $null,

        [Parameter(ValueFromPipeline, ParameterSetName = 'Header')]
        [System.Management.Automation.SwitchParameter] $IsHeaderRow,

        [Parameter(ValueFromPipeline, ParameterSetName = 'Row')]
        [System.Management.Automation.SwitchParameter] $IsAlternateRow
    )
    process
    {
        if (-not ([System.String]::IsNullOrEmpty($Style)))
        {
            ## Use the explictit style
            $IsStyleInherited = $false
        }
        elseif ($IsHeaderRow)
        {
            $Style = $document.TableStyles[$TableStyle].HeaderStyle
            $IsStyleInherited = $true
        }
        elseif ($IsAlternateRow)
        {
            $Style = $document.TableStyles[$TableStyle].AlternateRowStyle
            $IsStyleInherited = $true
        }
        else
        {
            $Style = $document.TableStyles[$TableStyle].RowStyle
            $IsStyleInherited = $true
        }

        return [PSCustomObject] @{
            Style            = $Style;
            IsStyleInherited = $IsStyleInherited;
            Cells            = New-Object -TypeName System.Collections.ArrayList;
        }
    }
}

function New-PScriboFormattedTableRowCell
{
<#
    .SYNOPSIS
        Creates a formatted table cell for plugin output/rendering.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSObject])]
    param
    (
        [Parameter(ValueFromPipeline)]
        [AllowNull()]
        [System.String[]] $Content,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Uint16] $Width,

        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.String] $Style = $null
    )
    process
    {
        $isStyleInherited = $true
        $combinedContent = $Content

        if ($Content -is [System.Array])
        {
            $combinedContent = [System.String]::Join([System.Environment]::NewLine, $Content)
        }

        if (-not ([System.String]::IsNullOrEmpty($Style)))
        {
            ## Use the explictit style
            $isStyleInherited = $false
        }

        return [PSCustomObject] @{
            Content          = $combinedContent
            Width            = $Width
            Style            = $Style
            IsStyleInherited = $isStyleInherited
        }
    }
}

function New-PScriboHeaderFooter
{
<#
    .SYNOPSIS
        Initializes a new PScribo header/footer object.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter','Footer')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Header')]
        [System.Management.Automation.SwitchParameter] $Header,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Footer')]
        [System.Management.Automation.SwitchParameter] $Footer
    )
    begin
    {
        ## Ignores dot-sourced script blocks, i.e. style scripts
        $psCallStack = Get-PSCallStack | Where-Object { $_.FunctionName -ne '<ScriptBlock>' }
        if ($psCallStack[2].FunctionName -ne 'Document<Process>')
        {
            throw $localized.HeaderFooterDocumentRootError
        }
    }
    process
    {
        $pscriboHeaderFooter = [PSCustomObject] @{
            Type     = if ($Header) { 'PScribo.Header' } else { 'PScribo.Footer' }
            Sections = (New-Object -TypeName System.Collections.ArrayList)
        }
        return $pscriboHeaderFooter
    }
}

function New-PScriboImage
{
<#
    .SYNOPSIS
        Initializes a new PScribo Image object.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding(DefaultParameterSetName = 'UriSize')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        [Parameter(Mandatory, ParameterSetName = 'UriSize')]
        [Parameter(Mandatory, ParameterSetName = 'UriPercent')]
        [System.String] $Uri,

        [Parameter(Mandatory, ParameterSetName = 'Base64Size')]
        [Parameter(Mandatory, ParameterSetName = 'Base64Percent')]
        [System.String] $Base64,

        [Parameter(ParameterSetName = 'UriSize')]
        [Parameter(ParameterSetName = 'Base64Size')]
        [System.UInt32] $Height,

        [Parameter(ParameterSetName = 'UriSize')]
        [Parameter(ParameterSetName = 'Base64Size')]
        [System.UInt32] $Width,

        [Parameter(Mandatory, ParameterSetName = 'UriPercent')]
        [Parameter(Mandatory, ParameterSetName = 'Base64Percent')]
        [System.UInt32] $Percent,

        [Parameter()]
        [ValidateSet('Left','Center','Right')]
        [System.String] $Align = 'Left',

        [Parameter(Mandatory, ParameterSetName = 'Base64Size')]
        [Parameter(Mandatory, ParameterSetName = 'Base64Percent')]
        [Parameter(ParameterSetName = 'UriSize')]
        [Parameter(ParameterSetName = 'UriPercent')]
        [System.String] $Text = $Uri,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String] $Id = [System.Guid]::NewGuid().ToString()
    )
    process
    {
        $imageNumber = [System.Int32] $pscriboDocument.Properties['Images']++;

        if ($PSBoundParameters.ContainsKey('Uri'))
        {
            $imageBytes = Get-UriBytes -Uri $Uri
        }
        elseif ($PSBoundParameters.ContainsKey('Base64'))
        {
            $imageBytes = [System.Convert]::FromBase64String($Base64)
        }

        $image = ConvertTo-Image -Bytes $imageBytes

        if ($PSBoundParameters.ContainsKey('Percent'))
        {
            $Width = ($image.Width / 100) * $Percent
            $Height = ($image.Height / 100) * $Percent
        }
        elseif (-not ($PSBoundParameters.ContainsKey('Width')) -and (-not $PSBoundParameters.ContainsKey('Height')))
        {
            $Width = $image.Width
            $Height = $image.Height
        }

        $pscriboImage = [PSCustomObject] @{
            Id          = $Id;
            ImageNumber = $imageNumber;
            Text        = $Text
            Type        = 'PScribo.Image';
            Bytes       = $imageBytes;
            Uri         = $Uri;
            Name        = 'Image{0}' -f $imageNumber;
            Align       = $Align;
            MIMEType    = Get-ImageMimeType -Image $image
            WidthEm     = ConvertTo-Em -Pixel $Width;
            HeightEm    = ConvertTo-Em -Pixel $Height;
            Width       = $Width;
            Height      = $Height;
        }
        return $pscriboImage;
    }
}

function New-PScriboItem
{
<#
    .SYNOPSIS
        Initializes new PScribo list item object.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $Text,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Style')]
        [System.String] $Style,

        ## Override the bold style
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Inline')]
        [System.Management.Automation.SwitchParameter] $Bold,

        ## Override the italic style
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Inline')]
        [System.Management.Automation.SwitchParameter] $Italic,

        ## Override the underline style
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Inline')]
        [System.Management.Automation.SwitchParameter] $Underline,

        ## Override the font name(s)
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Inline')]
        [ValidateNotNullOrEmpty()]
        [System.String[]] $Font,

        ## Override the font size (pt)
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Inline')]
        [AllowNull()]
        [System.UInt16] $Size = $null,

        ## Override the font color/colour
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Inline')]
        [AllowNull()]
        [System.String] $Color = $null
    )
    process
    {
        $pscriboItem = [PSCustomObject] @{
            Id               = [System.Guid]::NewGuid().ToString()
            Level            = 0
            Index            = 0
            Number           = ''
            Text             = $Text
            Type             = 'PScribo.Item'
            Style            = $Style
            Bold             = $Bold
            Italic           = $Italic
            Underline        = $Underline
            Font             = $Font
            Size             = $Size
            Color            = $Color
            IsStyleInherited = $PSCmdlet.ParameterSetName -eq 'Default'
            HasStyle         = $PSCmdlet.ParameterSetName -eq 'Style'
            HasInlineStyle   = $PSCmdlet.ParameterSetName -eq 'Inline'
        }
        return $pscriboItem
    }
}

function New-PScriboLineBreak
{
<#
    .SYNOPSIS
        Initializes a new PScribo line break object.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        [Parameter(Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Id = [System.Guid]::NewGuid().ToString()
    )
    process
    {
        $typeName = 'PScribo.LineBreak';
        $pscriboDocument.Properties['LineBreaks']++;
        $pscriboLineBreak = [PSCustomObject] @{
            Id = $Id;
            Type = $typeName;
        }
        return $pscriboLineBreak;
    }
}

function New-PScriboList
{
<#
    .SYNOPSIS
        Initializes new PScribo list object.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        ## Display name used in verbose output when processing.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $Name,

        ## List style Name/Id reference.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $Style,

        ## Numbered list.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Numbered,

        ## Numbered list style.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $NumberStyle = $pscriboDocument.DefaultNumberStyle,

        ## Bullet list style.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('Circle', 'Dash', 'Disc', 'Square')]
        [System.String] $BulletStyle = 'Disc',

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $Level = 1
    )
    process
    {
        $pscriboList = [PSCustomObject] @{
            Id                = [System.Guid]::NewGuid().ToString()
            Name              = $Name
            Type              = 'PScribo.List'
            Items             = (New-Object -TypeName System.Collections.ArrayList)
            Number            = 0
            Level             = $Level
            IsNumbered        = $Numbered.ToBool()
            Style             = $Style
            BulletStyle       = $BulletStyle
            NumberStyle       = $NumberStyle
            IsMultiLevel      = $false
            IsSectionBreak    = $false
            IsSectionBreakEnd = $false
            IsStyleInherited  = -not $PSBoundParameters.ContainsKey('Style')
            HasStyle          = $PSBoundParameters.ContainsKey('Style')
            HasBulletStyle    = -not $Numbered.ToBool()
            HasNumberStyle    = $PSBoundParameters.ContainsKey('NumberStyle')
        }
        return $pscriboList
    }
}

function New-PScriboListReference
{
<#
    .SYNOPSIS
        Initializes new PScribo list reference object.

    .DESCRIPTION
        Creates a placeholder reference to a list stored in $pscriboDocument.Lists.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        ## Display name used in verbose output when processing.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $Name,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $Number
    )
    process
    {
        $pscriboListReference = [PSCustomObject] @{
            Id                = [System.Guid]::NewGuid().ToString()
            Name              = $Name
            Type              = 'PScribo.ListReference'
            Number            = $Number
        }
        return $pscriboListReference
    }
}

function New-PScriboPageBreak
{
<#
    .SYNOPSIS
        Creates a PScribo page break object.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        [Parameter(Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Id = [System.Guid]::NewGuid().ToString()
    )
    process
    {
        $typeName = 'PScribo.PageBreak';
        $pscriboDocument.Properties['PageBreaks']++;
        $pscriboDocument.Properties['Pages']++;
        $pscriboPageBreak = [PSCustomObject] @{
            Id   = $Id;
            Type = $typeName;
        }
        return $pscriboPageBreak;
    }
}

function New-PScriboParagraph
{
<#
    .SYNOPSIS
        Initializes a new PScribo paragraph object.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter','ScriptBlock')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        ## PScribo paragraph run script block.
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Management.Automation.ScriptBlock] $ScriptBlock,

        ## Paragraph Id.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.String] $Id = [System.Guid]::NewGuid().ToString(),

        ## Paragraph style Name/Id reference.
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.String] $Style = $null,

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

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $NoIncrementCounter
    )
    process
    {
        if (-not $NoIncrementCounter)
        {
            $pscriboDocument.Properties['Paragraphs']++
        }

        $pscriboParagraph = [PSCustomObject] @{
            Id                = $Id
            Type              = 'PScribo.Paragraph'
            Style             = $Style
            Tabs              = $Tabs
            Sections          = (New-Object -TypeName System.Collections.ArrayList)
            Orientation       = $script:currentOrientation
            IsSectionBreakEnd = $false
        }
        return $pscriboParagraph
    }
}

function New-PScriboParagraphRun
{
<#
    .SYNOPSIS
        Initializes a new PScribo paragraph run (text) object.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
        [AllowEmptyString()]
        [System.String] $Text,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Style')]
        [System.String] $Style,

        ## No space applied between this text block and the next
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $NoSpace,

        ## Override the bold style
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Inline')]
        [System.Management.Automation.SwitchParameter] $Bold,

        ## Override the italic style
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Inline')]
        [System.Management.Automation.SwitchParameter] $Italic,

        ## Override the underline style
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Inline')]
        [System.Management.Automation.SwitchParameter] $Underline,

        ## Override the font name(s)
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Inline')]
        [ValidateNotNullOrEmpty()]
        [System.String[]] $Font,

        ## Override the font size (pt)
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Inline')]
        [AllowNull()]
        [System.UInt16] $Size = $null,

        ## Override the font color/colour
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Inline')]
        [AllowNull()]
        [System.String] $Color = $null
    )
    process
    {
        $pscriboParagraphRun = [PSCustomObject] @{
            Type              = 'PScribo.ParagraphRun'
            Text              = $Text
            Style             = $Style
            NoSpace           = $NoSpace
            Bold              = $Bold
            Italic            = $Italic
            Underline         = $Underline
            Font              = $Font
            Size              = $Size
            Color             = $Color
            IsStyleInherited  = $PSCmdlet.ParameterSetName -eq 'Default'
            HasStyle          = $PSCmdlet.ParameterSetName -eq 'Style'
            HasInlineStyle    = $PSCmdlet.ParameterSetName -eq 'Inline'
            IsParagraphRunEnd = $false
            Name              = $null # For legacy Xml output
            Value             = $null # For legacy Xml output
        }
        return $pscriboParagraphRun
    }
}

function New-PScriboSection
{
<#
    .SYNOPSIS
        Initializes new PScribo section object.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        ## PScribo section heading/name.
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        ## PScribo style applied to document section.
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.String] $Style = $null,

        ## Section is excluded from TOC/section numbering.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $IsExcluded,

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

        ## Section orientation
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('Portrait','Landscape')]
        [System.String] $Orientation
    )
    begin
    {
        ## Ensure we only have one 'Section' in the call stack (#121)
        $psCallStack = @(Get-PSCallStack | Where-Object { $_.FunctionName -eq 'Section<Process>' })
        if ($PSBoundParameters.ContainsKey('Orientation') -and ($psCallStack.Count -gt 1))
        {
            Write-PScriboMessage -Message $localized.CannotSetOrientationWarning -IsWarning;
            $null = $PSBoundParameters.Remove('Orientation')
        }
    }
    process
    {
        $typeName = 'PScribo.Section';
        $pscriboDocument.Properties['Sections']++;
        $pscriboSection = [PSCustomObject] @{
            Id                = [System.Guid]::NewGuid().ToString();
            Level             = 0;
            Number            = '';
            Name              = $Name;
            Type              = $typeName;
            Style             = $Style;
            Tabs              = $Tabs;
            IsExcluded        = $IsExcluded;
            Sections          = (New-Object -TypeName System.Collections.ArrayList);
            Orientation       = if ($PSBoundParameters.ContainsKey('Orientation')) { $Orientation } else { $script:currentOrientation }
            IsSectionBreak    = $false;
            IsSectionBreakEnd = $false;
        }

        ## Has the orientation changed from the parent scope
        if ($PSBoundParameters.ContainsKey('Orientation') -and ($Orientation -ne $script:currentOrientation))
        {
            $pscriboSection.IsSectionBreak = $true;
            $pscriboDocument.Properties['Pages']++;
            $script:currentOrientation = $Orientation;
        }
        return $pscriboSection;
    }
}

function New-PScriboTable
{
<#
    .SYNOPSIS
        Initializes a new PScribo table object.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        ## Table name/Id
        [Parameter(ValueFromPipelineByPropertyName, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name = ([System.Guid]::NewGuid().ToString()),

        ## Table columns/display order
        [Parameter(Mandatory)]
        [AllowNull()]
        [System.String[]] $Columns,

        ## Table columns widths
        [Parameter(Mandatory)]
        [AllowNull()]
        [System.UInt16[]] $ColumnWidths,

        ## Collection of PScriboTableObjects for table rows
        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [System.Collections.ArrayList] $Rows,

        ## Table style
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Style,

        # List view
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'List')]
        [System.Management.Automation.SwitchParameter] $List,

        ## Combine list view based upon the specified key/property name
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'List')]
        [System.String] $ListKey = $null,

        ## Display the key name in the table output
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'List')]
        [System.Management.Automation.SwitchParameter] $DisplayListKey,

        ## Table width (%), 0 = Autofit
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateRange(0, 100)]
        [System.UInt16] $Width = 100,

        ## Indent table
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateRange(0, 10)]
        [System.UInt16] $Tabs,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.String] $Caption
    )
    process
    {
        $typeName = 'PScribo.Table'
        $pscriboDocument.Properties['Tables']++
        $pscriboTable = [PSCustomObject] @{
            Id                = $Name.Replace(' ', $pscriboDocument.Options['SpaceSeparator']).ToUpper()
            Name              = $Name
            Number            = $pscriboDocument.Properties['Tables']
            Type              = $typeName
            Columns           = $Columns
            ColumnWidths      = $ColumnWidths
            Rows              = $Rows
            Style             = $Style
            Width             = $Width
            Tabs              = $Tabs
            HasCaption        = $PSBoundParameters.ContainsKey('Caption')
            Caption           = $Caption
            CaptionNumber     = 0
            IsList            = $List
            IsKeyedList       = $PSBoundParameters.ContainsKey('ListKey')
            ListKey           = $ListKey
            DisplayListKey    = $DisplayListKey.ToBool()
            Orientation       = $script:currentOrientation
            IsSectionBreakEnd = $false
        }

        ## Remove captions from List tables where there is more than one row
        if ($pscriboTable.HasCaption)
        {
            if (($pscriboTable.IsList) -and (-not $pscriboTable.IsKeyedList))
            {
                if ($pscriboTable.Rows.Count -gt 1)
                {
                    Write-PScriboMessage -Message ($localized.ListTableCaptionRemovedWarning -f $Name) -IsWarning
                    $pscriboTable.HasCaption = $false
                    $pscriboTable.Caption = $null
                }
            }

            ## Do we still have a caption?!
            if ($pscriboTable.HasCaption)
            {
                $pscriboDocument.Properties['TableCaptions']++
                $pscriboTable.CaptionNumber = $pscriboDocument.Properties['TableCaptions']
            }
        }

        return $pscriboTable
    }
}

function New-PScriboTableRow
{
<#
    .SYNOPSIS
        Defines a new PScribo document table row from an object or hashtable.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding(DefaultParameterSetName = 'InputObject')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        ## PSCustomObject to create PScribo table row
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'InputObject')]
        [System.Object] $InputObject,

        ## PSCutomObject properties to include in the table row
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'InputObject')]
        [AllowNull()]
        [System.String[]] $Properties,

        # Custom table header strings (in Display Order). Used for property names.
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'InputObject')]
        [AllowNull()]
        [System.String[]] $Headers = $null,

        ## Array of ordered dictionaries (hashtables) to create PScribo table row
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Hashtable')]
        [System.Collections.Specialized.OrderedDictionary] $Hashtable
    )
    begin
    {
        Write-Debug ('Using parameter set "{0}.' -f $PSCmdlet.ParameterSetName)
    }
    process
    {
        switch ($PSCmdlet.ParameterSetName)
        {
            'Hashtable'
            {
                if (-not $Hashtable.Contains('__Style'))
                {
                    $Hashtable['__Style'] = $null
                }
                ## Create and return custom object from hashtable
                $psCustomObject = [PSCustomObject] $Hashtable
                return $psCustomObject
            }
            Default
            {
                $objectProperties = [Ordered] @{ }
                if ($Properties -notcontains '__Style')
                {
                    $Properties += '__Style'
                }

                ## Build up hashtable of required property names
                for ($i = 0; $i -lt $Properties.Count; $i++)
                {
                    $propertyName = $Properties[$i]
                    $propertyStyleName = '{0}__Style' -f $propertyName
                    if ($InputObject.PSObject.Properties[$propertyStyleName])
                    {
                        if ($Headers)
                        {
                            ## Rename the style property to match the header
                            $headerStyleName = '{0}__Style' -f $Headers[$i]
                            $objectProperties[$headerStyleName] = $InputObject.$propertyStyleName
                        }
                        else
                        {
                            $objectProperties[$propertyStyleName] = $InputObject.$propertyStyleName
                        }
                    }
                    if ($Headers -and $PropertyName -notlike '*__Style')
                    {
                        if ($InputObject.PSObject.Properties[$propertyName])
                        {
                            $objectProperties[$Headers[$i]] = $InputObject.$propertyName
                        }
                    }
                    else
                    {
                        if ($InputObject.PSObject.Properties[$propertyName])
                        {
                            $objectProperties[$propertyName] = $InputObject.$propertyName
                        }
                        else
                        {
                            $objectProperties[$propertyName] = $null
                        }
                    }
                }

                ## Create and return custom object
                return ([PSCustomObject] $objectProperties)
            }
        }
    }
}

function New-PScriboTOC
{
<#
    .SYNOPSIS
        Initializes a new PScribo Table of Contents (TOC) object.

    .NOTES
        This is an internal function and should not be called directly.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param
    (
        [Parameter(ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name = 'Contents',

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ClassId = 'TOC'
    )
    process
    {
        $typeName = 'PScribo.TOC';
        if ($pscriboDocument.Options['ForceUppercaseSection'])
        {
            $Name = $Name.ToUpper();
        }
        $pscriboDocument.Properties['TOCs']++;
        $pscriboTOC = [PSCustomObject] @{
            Id      = [System.Guid]::NewGuid().ToString();
            Name    = $Name;
            Type    = $typeName;
            ClassId = $ClassId;
        }
        return $pscriboTOC;
    }
}

function Resolve-ImageUri
{
<#
    .SYNOPSIS
        Converts an image path into a Uri.

    .NOTES
        A Uri includes information about whether the path is local etc. This is useful for plugins
        to be able to determine whether to embed images or not.
#>

    [CmdletBinding()]
    [OutputType([System.Uri])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $Path
    )
    process
    {
        if (Test-Path -Path $Path)
        {
            $Path = Resolve-Path -Path $Path
        }
        $uri = New-Object -TypeName System.Uri -ArgumentList @($Path)
        return $uri
    }
}

function Resolve-PScriboStyleColor
{
<#
    .SYNOPSIS
        Resolves a HTML color format or Word color constant to a RGB value
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, Position = 0)]
        [ValidateNotNull()]
        [System.String] $Color
    )
    begin
    {
        # http://www.jadecat.com/tuts/colorsplus.html
        $wordColorConstants = @{
            AliceBlue            = 'F0F8FF'
            AntiqueWhite         = 'FAEBD7'
            Aqua                 = '00FFFF'
            Aquamarine           = '7FFFD4'
            Azure                = 'F0FFFF'
            Beige                = 'F5F5DC'
            Bisque               = 'FFE4C4'
            Black                = '000000'
            BlanchedAlmond       = 'FFEBCD'
            Blue                 = '0000FF'
            BlueViolet           = '8A2BE2'
            Brown                = 'A52A2A'
            BurlyWood            = 'DEB887'
            CadetBlue            = '5F9EA0'
            Chartreuse           = '7FFF00'
            Chocolate            = 'D2691E'
            Coral                = 'FF7F50'
            CornflowerBlue       = '6495ED'
            Cornsilk             = 'FFF8DC'
            Crimson              = 'DC143C'
            Cyan                 = '00FFFF'
            DarkBlue             = '00008B'
            DarkCyan             = '008B8B'
            DarkGoldenrod        = 'B8860B'
            DarkGray             = 'A9A9A9'
            DarkGreen            = '006400'
            DarkKhaki            = 'BDB76B'
            DarkMagenta          = '8B008B'
            DarkOliveGreen       = '556B2F'
            DarkOrange           = 'FF8C00'
            DarkOrchid           = '9932CC'
            DarkRed              = '8B0000'
            DarkSalmon           = 'E9967A'
            DarkSeaGreen         = '8FBC8F'
            DarkSlateBlue        = '483D8B'
            DarkSlateGray        = '2F4F4F'
            DarkTurquoise        = '00CED1'
            DarkViolet           = '9400D3'
            DeepPink             = 'FF1493'
            DeepSkyBlue          = '00BFFF'
            DimGray              = '696969'
            DodgerBlue           = '1E90FF'
            Firebrick            = 'B22222'
            FloralWhite          = 'FFFAF0'
            ForestGreen          = '228B22'
            Fuchsia              = 'FF00FF'
            Gainsboro            = 'DCDCDC'
            GhostWhite           = 'F8F8FF'
            Gold                 = 'FFD700'
            Goldenrod            = 'DAA520'
            Gray                 = '808080'
            Green                = '008000'
            GreenYellow          = 'ADFF2F'
            Honeydew             = 'F0FFF0'
            HotPink              = 'FF69B4'
            IndianRed            = 'CD5C5C'
            Indigo               = '4B0082'
            Ivory                = 'FFFFF0'
            Khaki                = 'F0E68C'
            Lavender             = 'E6E6FA'
            LavenderBlush        = 'FFF0F5'
            LawnGreen            = '7CFC00'
            LemonChiffon         = 'FFFACD'
            LightBlue            = 'ADD8E6'
            LightCoral           = 'F08080'
            LightCyan            = 'E0FFFF'
            LightGoldenrodYellow = 'FAFAD2'
            LightGreen           = '90EE90'
            LightGrey            = 'D3D3D3'
            LightPink            = 'FFB6C1'
            LightSalmon          = 'FFA07A'
            LightSeaGreen        = '20B2AA'
            LightSkyBlue         = '87CEFA'
            LightSlateGray       = '778899'
            LightSteelBlue       = 'B0C4DE'
            LightYellow          = 'FFFFE0'
            Lime                 = '00FF00'
            LimeGreen            = '32CD32'
            Linen                = 'FAF0E6'
            Magenta              = 'FF00FF'
            Maroon               = '800000'
            McMintGreen          = 'BED6C9'
            MediumAuqamarine     = '66CDAA'
            MediumBlue           = '0000CD'
            MediumOrchid         = 'BA55D3'
            MediumPurple         = '9370D8'
            MediumSeaGreen       = '3CB371'
            MediumSlateBlue      = '7B68EE'
            MediumSpringGreen    = '00FA9A'
            MediumTurquoise      = '48D1CC'
            MediumVioletRed      = 'C71585'
            MidnightBlue         = '191970'
            MintCream            = 'F5FFFA'
            MistyRose            = 'FFE4E1'
            Moccasin             = 'FFE4B5'
            NavajoWhite          = 'FFDEAD'
            Navy                 = '000080'
            OldLace              = 'FDF5E6'
            Olive                = '808000'
            OliveDrab            = '688E23'
            Orange               = 'FFA500'
            OrangeRed            = 'FF4500'
            Orchid               = 'DA70D6'
            PaleGoldenRod        = 'EEE8AA'
            PaleGreen            = '98FB98'
            PaleTurquoise        = 'AFEEEE'
            PaleVioletRed        = 'D87093'
            PapayaWhip           = 'FFEFD5'
            PeachPuff            = 'FFDAB9'
            Peru                 = 'CD853F'
            Pink                 = 'FFC0CB'
            Plum                 = 'DDA0DD'
            PowderBlue           = 'B0E0E6'
            Purple               = '800080'
            Red                  = 'FF0000'
            RosyBrown            = 'BC8F8F'
            RoyalBlue            = '4169E1'
            SaddleBrown          = '8B4513'
            Salmon               = 'FA8072'
            SandyBrown           = 'F4A460'
            SeaGreen             = '2E8B57'
            Seashell             = 'FFF5EE'
            Sienna               = 'A0522D'
            Silver               = 'C0C0C0'
            SkyBlue              = '87CEEB'
            SlateBlue            = '6A5ACD'
            SlateGray            = '708090'
            Snow                 = 'FFFAFA'
            SpringGreen          = '00FF7F'
            SteelBlue            = '4682B4'
            Tan                  = 'D2B48C'
            Teal                 = '008080'
            Thistle              = 'D8BFD8'
            Tomato               = 'FF6347'
            Turquoise            = '40E0D0'
            Violet               = 'EE82EE'
            Wheat                = 'F5DEB3'
            White                = 'FFFFFF'
            WhiteSmoke           = 'F5F5F5'
            Yellow               = 'FFFF00'
            YellowGreen          = '9ACD32'
        }
    }
    process
    {
        $pscriboColor = $Color
        if ($wordColorConstants.ContainsKey($pscriboColor))
        {
            return $wordColorConstants[$pscriboColor].ToLower()
        }
        elseif ($pscriboColor.Length -eq 6 -or $pscriboColor.Length -eq 3)
        {
            $pscriboColor = '#{0}' -f $pscriboColor
        }
        elseif ($pscriboColor.Length -eq 7 -or $pscriboColor.Length -eq 4)
        {
            if (-not ($pscriboColor.StartsWith('#')))
            {
                return $null
            }
        }
        if ($pscriboColor -notmatch '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$')
        {
            return $null
        }
        return $pscriboColor.TrimStart('#').ToLower()
    }
}

function Resolve-PScriboToken
{
<#
    .SYNOPSIS
        Replaces page number tokens with their (approximated) values.
#>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [AllowEmptyString()]
        [System.String] $InputObject
    )
    process
    {
        $tokenisedText = $InputObject -replace
            '<!#\s?PageNumber\s?#!>', $script:currentPageNumber -replace
            '<!#\s?TotalPages\s?#!>', $Document.Properties.Pages
        return $tokenisedText
    }
}

function Set-PScriboSectionBreakEnd
{
<#
    .SYNOPSIS
        Sets the IsSectionBreakEnd on the last (nested) paragraph/subsection (required by Word plugin)
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','')]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.PSObject] $Section
    )
    process
    {
        $Section.Sections |
            Where-Object { $_.Type -in 'PScribo.Section','PScribo.Paragraph','PScribo.Table' } |
            Select-Object -Last 1 | ForEach-Object {
                    if ($PSItem.Type -in 'PScribo.Paragraph','PScribo.Table')
                    {
                        $PSItem.IsSectionBreakEnd = $true
                    }
                    else
                    {
                        Set-PScriboSectionBreakEnd -Section $PSItem
                    }
                }
    }
}

Function Test-CharsInPath {

<#

    .SYNOPSIS
    PowerShell function intended to verify if in the string what is the path to file or folder are incorrect chars.

    .DESCRIPTION
    PowerShell function intended to verify if in the string what is the path to file or folder are incorrect chars.

    Exit codes

    - 0 - everything OK
    - 1 - nothing to check
    - 2 - an incorrect char found in the path part
    - 3 - an incorrect char found in the file name part
    - 4 - incorrect chars found in the path part and in the file name part

    .PARAMETER Path
    Specifies the path to an item for what path (location on the disk) need to be checked.

    The Path can be an existing file or a folder on a disk provided as a PowerShell object or a string e.g. prepared to be used in file/folder creation.

    .PARAMETER SkipCheckCharsInFolderPart
    Skip checking in the folder part of path.

    .PARAMETER SkipCheckCharsInFileNamePart
    Skip checking in the file name part of path.

    .PARAMETER SkipDividingForParts
    Skip dividing provided path to a directory and a file name.

    Used usually in conjuction with SkipCheckCharsInFolderPart or SkipCheckCharsInFileNamePart.

    .EXAMPLE

    [PS] > Test-CharsInPath -Path $(Get-Item C:\Windows\Temp\new.csv') -Verbose

    VERBOSE: The path provided as a string was devided to, directory part: C:\Windows\Temp ; file name part: new.csv
    0

    Testing existing file. Returned code means that all chars are acceptable in the name of folder and file.

    .EXAMPLE

    [PS] > Test-CharsInPath -Path "C:\newfolder:2\nowy|.csv" -Verbose

    VERBOSE: The path provided as a string was devided to, directory part: C:\newfolder:2\ ; file name part: nowy|.csv
    VERBOSE: The incorrect char | with the UTF code [124] found in FileName part
    3

    Testing the string if can be used as a file name. The returned value means that can't do to an unsupported char in the file name.

    .OUTPUTS
    Exit code as an integer number. See description section to find the exit codes descriptions.

    .LINK
    https://github.com/it-praktyk/New-OutputObject

    .LINK
    https://www.linkedin.com/in/sciesinskiwojciech

    .NOTES
    AUTHOR: Wojciech Sciesinski, wojciech[at]sciesinski[dot]net
    KEYWORDS: PowerShell, FileSystem

    REMARKS:
    # For Windows - based on the Power Tips
    # Finding Invalid File and Path Characters
    # http://community.idera.com/powershell/powertips/b/tips/posts/finding-invalid-file-and-path-characters
    # For PowerShell Core
    # https://docs.microsoft.com/en-us/dotnet/api/system.io.path.getinvalidpathchars?view=netcore-2.0
    # https://www.dwheeler.com/essays/fixing-unix-linux-filenames.html
    # [char]0 = NULL

    CURRENT VERSION
    - 0.6.1 - 2017-07-23

    HISTORY OF VERSIONS
    https://github.com/it-praktyk/New-OutputObject/CHANGELOG.md


#>


    [cmdletbinding()]
    [OutputType([System.Int32])]
    param (

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        $Path,
        [parameter(Mandatory = $false)]
        [switch]$SkipCheckCharsInFolderPart,
        [parameter(Mandatory = $false)]
        [switch]$SkipCheckCharsInFileNamePart,
        [parameter(Mandatory = $false)]
        [switch]$SkipDividingForParts

    )

    BEGIN {

        If (($PSVersionTable.ContainsKey('PSEdition')) -and ($PSVersionTable.PSEdition -eq 'Core') -and $IsLinux) {

            #[char]0 = NULL
            $PathInvalidChars = [char]0

            $FileNameInvalidChars = @([char]0, '/')

            $PathSeparators = @('/')

        }
        Elseif (($PSVersionTable.ContainsKey('PSEdition')) -and ($PSVersionTable.PSEdition -eq 'Core') -and $IsMacOS) {

            $PathInvalidChars = [char]58

            $FileNameInvalidChars = [char]58

            $PathSeparators = @('/')

        }
        #Windows
        Else {

            $PathInvalidChars = [System.IO.Path]::GetInvalidPathChars() #36 chars

            $FileNameInvalidChars = [System.IO.Path]::GetInvalidFileNameChars() #41 chars

            #$FileOnlyInvalidChars = @(':', '*', '?', '\', '/') #5 chars - as a difference

            $PathSeparators = @('/','\')

        }

        $IncorectCharFundInPath = $false

        $IncorectCharFundInFileName = $false

        $NothingToCheck = $true

    }

    END {

        [String]$DirectoryPath = ""

        [String]$FileName = ""

        $PathType = ($Path.GetType()).Name

        If (@('DirectoryInfo', 'FileInfo') -contains $PathType) {

            If (($SkipCheckCharsInFolderPart.IsPresent -and $PathType -eq 'DirectoryInfo') -or ($SkipCheckCharsInFileNamePart.IsPresent -and $PathType -eq 'FileInfo')) {

                Return 1

            }
            ElseIf ($PathType -eq 'DirectoryInfo') {

                [String]$DirectoryPath = $Path.FullName

            }

            elseif ($PathType -eq 'FileInfo') {

                [String]$DirectoryPath = $Path.DirectoryName

                [String]$FileName = $Path.Name

            }

        }

        ElseIf ($PathType -eq 'String') {

            If ( $SkipDividingForParts.IsPresent -and $SkipCheckCharsInFolderPart.IsPresent ) {

                $FileName = $Path

            }
            ElseIf ( $SkipDividingForParts.IsPresent -and $SkipCheckCharsInFileNamePart.IsPresent  ) {

                $DirectoryPath = $Path

            }
            Else {

                #Convert String to Array of chars
                $PathArray = $Path.ToCharArray()

                $PathLength = $PathArray.Length

                For ($i = ($PathLength-1); $i -ge 0; $i--) {

                    If ($PathSeparators -contains $PathArray[$i]) {

                        [String]$DirectoryPath = [String]$Path.Substring(0, $i +1)

                        break

                    }

                }

                If ([String]::IsNullOrEmpty($DirectoryPath)) {

                    [String]$FileName = [String]$Path

                }
                Else {

                    [String]$FileName = $Path.Replace($DirectoryPath, "")

                }

            }

        }
        Else {

            [String]$MessageText = "Input object {0} can't be tested" -f ($Path.GetType()).Name

            Throw $MessageText

        }

        [String]$MessageText = "The path provided as a string was divided to: directory part: {0} ; file name part: {1} ." -f $DirectoryPath, $FileName

        Write-Verbose -Message $MessageText

        If ($SkipCheckCharsInFolderPart.IsPresent -and $SkipCheckCharsInFileNamePart.IsPresent) {

            Return 1

        }

        If (-not ($SkipCheckCharsInFolderPart.IsPresent) -and -not [String]::IsNullOrEmpty($DirectoryPath)) {

            $NothingToCheck = $false

            foreach ($Char in $PathInvalidChars) {

                If ($DirectoryPath.ToCharArray() -contains $Char) {

                    $IncorectCharFundInPath = $true

                    [String]$MessageText = "The incorrect char {0} with the UTF code [{1}] found in the Path part." -f $Char, $([int][char]$Char)

                    Write-Verbose -Message $MessageText

                }

            }

        }

        If (-not ($SkipCheckCharsInFileNamePart.IsPresent) -and -not [String]::IsNullOrEmpty($FileName)) {

            $NothingToCheck = $false

            foreach ($Char in $FileNameInvalidChars) {

                If ($FileName.ToCharArray() -contains $Char) {

                    $IncorectCharFundInFileName = $true

                    [String]$MessageText = "The incorrect char {0} with the UTF code [{1}] found in FileName part." -f $Char, $([int][char]$Char)

                    Write-Verbose -Message $MessageText

                }

            }

        }

        If ($IncorectCharFundInPath -and $IncorectCharFundInFileName) {

            Return 4

        }
        elseif ($NothingToCheck) {

            Return 1

        }

        elseif ($IncorectCharFundInPath) {

            Return 2

        }

        elseif ($IncorectCharFundInFileName) {

            Return 3

        }
        Else {

            Return 0

        }

    }

}

function Test-PScriboStyle
{
<#
    .SYNOPSIS
        Tests whether a style has been defined.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name
    )
    process
    {
        return $PScriboDocument.Styles.ContainsKey($Name)
    }
}

function Test-PScriboStyleColor
{
<#
    .SYNOPSIS
        Tests whether a color string is a valid HTML color.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Color
    )
    process
    {
        if (Resolve-PScriboStyleColor -Color $Color)
        {
            return $true
        }
        else
        {
            return $false
        }
    }
}

function Write-PScriboProcessSectionId
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $SectionId,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $SectionType,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $Length = 40,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Int32] $Indent = 0
    )
    process
    {
        if ($SectionId.Length -gt $Length)
        {
            $sectionDisplayName = '{0}[..]' -f $SectionId.Substring(0, ($Length -4))
        }
        else
        {
            $sectionDisplayName = $SectionId
        }

        $writePScriboMessageParams = @{
            Message = $localized.PluginProcessingSection -f $SectionType, $sectionDisplayName
            Indent  = $Indent
        }
        Write-PScriboMessage @writePScriboMessageParams
    }
}


# SIG # Begin signature block
# MIIuugYJKoZIhvcNAQcCoIIuqzCCLqcCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBpcTbRyY7sRlDv
# 3owFWshs/9CYxLiCUMTjPWzjqG09NaCCE6QwggWQMIIDeKADAgECAhAFmxtXno4h
# 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/byWBypExghpsMIIaaAIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYD
# VQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBH
# NCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQgMjAyMSBDQTECEAh8dPej8V5j
# mSw73gBSFOYwDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAA
# oQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4w
# DAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQghMsd7p6u267ysAQqUymLkqEs
# xz6p1474xox/kLOP/F0wDQYJKoZIhvcNAQEBBQAEggIAGYRFata3StebA5V7+C7q
# vojFnDRXE8+CHIkr9ONLjcEwZiNrg9WF/j7NkJAmB1uqbk05kV+zcZeQampdNI3k
# 2JqPQ7Jq+emI/UU50Z+PY9e87400iKD4gHoFqBaHARt6wy6gM4PgeWZ35FHKo4J7
# PDGQHVXvMyJC4dCwTXCUT3hcQ+RM8o4joc//HJYMgoHmnPnPS38MYlpIwRaenbvN
# PsMfHBHzAKN/BheYIFVweuqPP5aHTU+9n4gSQC68+MhlZ37OoJXE3Z1vW8+E/Nkf
# /MzYs2Y4pjkFdlzyzkBFRLvfnSMMFwLmsAdHbYl18D0jwkK2D6bTTCjXvNcYcJRx
# PJUx5pIKTiT6ORAf96+VxPIgdSNPTfhZt8VJJ1gCpwClphm+O9iQqVWqETgCzBpp
# XFBx3TEP3YUqnb5888wknq80BdVnsMFGShDpOdFN1RQ21jTpyGxPRIESU/iT/Q4V
# tESqPIADhWIrZutHDBgkJvhtkAz3+VSFrO6dTNZ2Lzs54iy45XQ5wm74exCGM2ZA
# Tk1oU9G4OYsMsuoWes0yPY5wHF8M58+Rl5/f7/cdqJRsK/5gFIk62ismOIK4j0im
# 9im81MHE+f/RXQIAG3cTTvyCK3VPApka3+QMhpQ4Ai4d0B4AYi9Xb6JltNF/ahwI
# D+IDrGleEUOEWy907yWzBCGhghc5MIIXNQYKKwYBBAGCNwMDATGCFyUwghchBgkq
# hkiG9w0BBwKgghcSMIIXDgIBAzEPMA0GCWCGSAFlAwQCAQUAMHcGCyqGSIb3DQEJ
# EAEEoGgEZjBkAgEBBglghkgBhv1sBwEwMTANBglghkgBZQMEAgEFAAQgR/3xQv+O
# yw4cDByeGlDKO2IBiMXNU5v5WXSR1xxqzeICEHkQilH3WR/Ws9elYVp4+7EYDzIw
# MjUwMzA3MDg1NTEzWqCCEwMwgga8MIIEpKADAgECAhALrma8Wrp/lYfG+ekE4zME
# MA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2Vy
# dCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNI
# QTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjQwOTI2MDAwMDAwWhcNMzUxMTI1MjM1
# OTU5WjBCMQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNlcnQxIDAeBgNVBAMT
# F0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDI0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
# MIICCgKCAgEAvmpzn/aVIauWMLpbbeZZo7Xo/ZEfGMSIO2qZ46XB/QowIEMSvgjE
# dEZ3v4vrrTHleW1JWGErrjOL0J4L0HqVR1czSzvUQ5xF7z4IQmn7dHY7yijvoQ7u
# jm0u6yXF2v1CrzZopykD07/9fpAT4BxpT9vJoJqAsP8YuhRvflJ9YeHjes4fduks
# THulntq9WelRWY++TFPxzZrbILRYynyEy7rS1lHQKFpXvo2GePfsMRhNf1F41nyE
# g5h7iOXv+vjX0K8RhUisfqw3TTLHj1uhS66YX2LZPxS4oaf33rp9HlfqSBePejlY
# eEdU740GKQM7SaVSH3TbBL8R6HwX9QVpGnXPlKdE4fBIn5BBFnV+KwPxRNUNK6lY
# k2y1WSKour4hJN0SMkoaNV8hyyADiX1xuTxKaXN12HgR+8WulU2d6zhzXomJ2Ple
# I9V2yfmfXSPGYanGgxzqI+ShoOGLomMd3mJt92nm7Mheng/TBeSA2z4I78JpwGpT
# RHiT7yHqBiV2ngUIyCtd0pZ8zg3S7bk4QC4RrcnKJ3FbjyPAGogmoiZ33c1HG93V
# p6lJ415ERcC7bFQMRbxqrMVANiav1k425zYyFMyLNyE1QulQSgDpW9rtvVcIH7Wv
# G9sqYup9j8z9J1XqbBZPJ5XLln8mS8wWmdDLnBHXgYly/p1DhoQo5fkCAwEAAaOC
# AYswggGHMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQM
# MAoGCCsGAQUFBwMIMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATAf
# BgNVHSMEGDAWgBS6FtltTYUvcyl2mi91jGogj57IbzAdBgNVHQ4EFgQUn1csA3cO
# KBWQZqVjXu5Pkh92oFswWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2NybDMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRpbWVTdGFt
# cGluZ0NBLmNybDCBkAYIKwYBBQUHAQEEgYMwgYAwJAYIKwYBBQUHMAGGGGh0dHA6
# Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBYBggrBgEFBQcwAoZMaHR0cDovL2NhY2VydHMu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRpbWVT
# dGFtcGluZ0NBLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAPa0eH3aZW+M4hBJH2UOR
# 9hHbm04IHdEoT8/T3HuBSyZeq3jSi5GXeWP7xCKhVireKCnCs+8GZl2uVYFvQe+p
# PTScVJeCZSsMo1JCoZN2mMew/L4tpqVNbSpWO9QGFwfMEy60HofN6V51sMLMXNTL
# fhVqs+e8haupWiArSozyAmGH/6oMQAh078qRh6wvJNU6gnh5OruCP1QUAvVSu4kq
# VOcJVozZR5RRb/zPd++PGE3qF1P3xWvYViUJLsxtvge/mzA75oBfFZSbdakHJe2B
# VDGIGVNVjOp8sNt70+kEoMF+T6tptMUNlehSR7vM+C13v9+9ZOUKzfRUAYSyyEmY
# tsnpltD/GWX8eM70ls1V6QG/ZOB6b6Yum1HvIiulqJ1Elesj5TMHq8CWT/xrW7tw
# ipXTJ5/i5pkU5E16RSBAdOp12aw8IQhhA/vEbFkEiF2abhuFixUDobZaA0VhqAsM
# HOmaT3XThZDNi5U2zHKhUs5uHHdG6BoQau75KiNbh0c+hatSF+02kULkftARjsyE
# pHKsF7u5zKRbt5oK5YGwFvgc4pEVUNytmB3BpIiowOIIuDgP5M9WArHYSAR16gc0
# dP2XdkMEP5eBsX7bf/MGN4K3HP50v/01ZHo/Z5lGLvNwQ7XHBx1yomzLP8lx4Q1z
# ZKDyHcp4VQJLu2kWTsKsOqQwggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5b
# MA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5
# NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkG
# A1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3Rh
# bXBpbmcgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPB
# PXJJUVXHJQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/
# nR+eDzMfUBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLc
# Z47qUT3w1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mf
# XazL6IRktFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3N
# Ng1c1eYbqMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yem
# j052FVUmcJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g
# 3uM+onP65x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD
# 4L/wojzKQtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDS
# LFc1eSuo80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwM
# O1uKIqjBJgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU
# 7s7pXcheMBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/
# BAgwBgEB/wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0j
# BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E
# PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz
# dGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEw
# DQYJKoZIhvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPO
# vxj7x1Bd4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQ
# TGIdDAiCqBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWae
# LJ7giqzl/Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPBy
# oyP6wCeCRK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfB
# wWpx2cYTgAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8l
# Y5knLD0/a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/
# O3itTK37xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbb
# bxV7HhmLNriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3
# OtUVmDG0YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBl
# dkKmKYcJRyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt
# 1nz8MIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG9w0BAQwF
# ADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL
# ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElE
# IFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBiMQswCQYD
# VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGln
# aWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKn
# JS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/W
# BTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHi
# LQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhm
# V1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHE
# tWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6
# MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mX
# aXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZ
# xd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfh
# vbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvl
# EFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn1
# 5GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
# HQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReuir/SSy4Ix
# LVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0wazAkBggr
# BgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAChjdo
# dHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290
# Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAGBgRVHSAA
# MA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9mqyhhyzs
# hV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxSA8hO0Cre
# +i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/6Fmo8L8v
# C6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSMb++hUD38
# dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt9H5xaiNr
# Iv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMYIDdjCCA3ICAQEw
# dzBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNV
# BAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1w
# aW5nIENBAhALrma8Wrp/lYfG+ekE4zMEMA0GCWCGSAFlAwQCAQUAoIHRMBoGCSqG
# SIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjUwMzA3MDg1
# NTEzWjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBTb04XuYtvSPnvk9nFIUIck1YZb
# RTAvBgkqhkiG9w0BCQQxIgQgu6M7eCoXwRx8iw9O2qR2mBjFMb0fXN2k+BXO1tO5
# H8UwNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQgdnafqPJjLx9DCzojMK7WVnX+13Pb
# BdZluQWTmEOPmtswDQYJKoZIhvcNAQEBBQAEggIAI1iZPtlheZQDkPMfsuDzjoPu
# 12PplVIMYS//21V7JJqGy/0VAmFb36sWFwZUc9gv8O85w4ujmsKS4svXOi+GFrFt
# cQ6jaoPqj9nPvD6XjknAU8ip+ShhBrFOt0ANt1nYcw9K8pQV4n5c6vlfMDmmxfF/
# rSSdlNss5ieu+yTwJvzu01N+OUoyuwM/jihlN/aX4F0XYt6NI8laKPE7F0qZDjqO
# kAvQcnsKe+VXYLwzWlMRsoXkoWlvoT3ZlOTZKiGdF+uT3U+/Zoc2WnnMV35/z4M/
# MBkS7QOwgolc7i9lVWh2tRNT/7d4W0HavJYVbRpdx/0FSgPwx+tmr0TJEH+oZxSx
# EibqTcP0IVXnfmSlq1lN6wtKW1nbpn2DBtdT7A7Zp3puJJ/+B7pYGnLoV53rj4dL
# ZPOVH3e5U7m6dhxcHg1dXgQlDH7Sx8hATAJSl4mkEeYJuBE32gCb2xUQd94qorq1
# LdHphk05Hto8MtNfG3D7WSNFud9a9UuQtho588PX8QbAEDMWYJwZTRi2L7LVUfBi
# iasZUNPVTDd4WYf7vF5KegJ8NbnhOD97FDU0FxpT+xl7Y8vr07yHACD+ClVct0Hs
# FLhDPISDnXG8i5vGZFwhUTpWG/Iijf6sjdAaK9ki3Zns1hNi/R8wWF3ksPnVaqAA
# dYBtMpXMv5qtLYIarWI=
# SIG # End signature block