Release/PScribo-v0.7.3.180-Bundle.ps1

#region PScribo Bundle v0.7.2.180
#requires -Version 3

<#
The MIT License (MIT)
 
Copyright (c) 2015 Iain Brighton
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
#>


$localized = DATA {
# en-US
ConvertFrom-StringData @'
ImportingFile = Importing file '{0}'.
InvalidDirectoryPathError = Path '{0}' is not a valid directory path.'
NoScriptBlockProvidedError = No PScribo section script block is provided (have you put the open curly brace on the next line?).
InvalidHtmlColorError = Invalid Html color '{0}' specified.
InvalidHtmlBackgroundColorError = Invalid Html background color '{0}' specified.
UndefinedTableHeaderStyleError = Undefined table header style '{0}' specified.
UndefinedTableRowStyleError = Undefined table row style '{0}' specified.
UndefinedAltTableRowStyleError = Undefined table alternating row style '{0}' specified.
InvalidTableBorderColorError = Invalid table border color '{0}' specified.
UndefinedStyleError = Undefined style '{0}' specified.
OpenPackageError = Error opening package '{0}'. Ensure the file in not in use by another process.
MaxHeadingLevelWarning = Html5 supports a maximum of 5 heading levels.
TableHeadersWithNoColumnsWarning = Table headers have been specified with no table columns/properties. Headers will be ignored.
TableHeadersCountMismatchWarning = The number of table headers specified does not match the number of specified columns/properties. Headers will be ignored.
ListTableColumnCountWarning = Table columns widths in list format must be 2. Column widths will be ignored.
TableColumnWidthMismatchWarning = The specified number of table columns and column widths do not match. Column widths will be ignored.
TableColumnWidthSumWarning = The table column widths total '{0}'%. Total column width must equal 100%. Column widths will be ignored.
TableWidthOverflowWarning = The table width overflows the page margin and has been adjusted to '{0}'%.
 
DocumentProcessingStarted = Document '{0}' processing started.
DocumentInvokePlugin = Invoking '{0}' plugin.
DocumentOptions = Setting global document options.
DocumentOptionSpaceSeparator = Setting default space separator to '{0}'.
DocumentOptionUppercaseHeadings = Enabling uppercase headings.
DocumentOptionUppercaseSections = Enabling uppercase sections.
DocumentOptionSectionNumbering = Enabling section/heading numbering.
DocumentOptionPageTopMargin = Setting page top margin to '{0}'mm.
DocumentOptionPageRightMargin = Setting page right margin to '{0}'mm.
DocumentOptionPageBottomMargin = Setting page bottom margin to '{0}'mm.
DocumentOptionPageLeftMargin = Setting page left margin to '{0}'mm.
DocumentOptionPageSize = Setting page size to '{0}'.
DocumentOptionPageHeight = Setting page height to '{0}'mm.
DocumentOptionPageWidth = Setting page width to '{0}'mm.
DocumentOptionDefaultFont = Setting default font(s) to '{0}'.
ProcessingBlankLine = Processing blank line.
ProcessingImage = Processing image '{0}'.
ProcessingLineBreak = Processing line break.
ProcessingPageBreak = Processing page break.
ProcessingParagraph = Processing paragraph '{0}'.
ProcessingSection = Processing section '{0}'.
ProcessingSectionStarted = Processing section '{0}' started.
ProcessingSectionCompleted = Processing section '{0}' completed.
PluginProcessingSection = Processing {0} '{1}'.
ProcessingStyle = Setting document style '{0}'.
ProcessingTable = Processing table '{0}'.
ProcessingTableStyle = Setting table style '{0}'.
ProcessingTOC = Processing table of contents '{0}'.
ProcessingDocumentPart = Processing document part '{0}'.
WritingDocumentPart = Writing document part '{0}'.
GeneratingPackageRelationships = Generating package relationships.
PluginUnsupportedSection = Unsupported section '{0}'.
DocumentProcessingCompleted = Document '{0}' processing completed.
                                  
TotalProcessingTime = Total processing time '{0:N2}' seconds.
SavingFile = Saving file '{0}'.
'@
;
}

function BlankLine {
    <#
    .SYNOPSIS
        Initializes a new PScribo blank line object.
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline, Position = 0)] [System.UInt32] $Count = 1
    )
    begin {
        #region BlankLine Private Functions
        function New-PScriboBlankLine {
            <#
            .SYNOPSIS
                Initializes a new PScribo blank line break.
            .NOTES
                This is an internal function and should not be called directly.
            #>

            [CmdletBinding()]
            [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;
            }
        } #end function New-PScriboBlankLine
        #endregion BlankLine Private Functions
    } #end begin
    process {
        WriteLog -Message $localized.ProcessingBlankLine;
        return (New-PScriboBlankLine @PSBoundParameters);
    } #end process
} #end function BlankLine

function ConvertPtToMm {
    <#
    .SYNOPSIS
        Convert points into millimeters
    #>

    [CmdletBinding()]
    [OutputType([System.Single])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)] [Alias('pt')] [System.Single] $Point
    )
    process {
        return [System.Math]::Round(($Point / 72) * 25.4, 2);
    }
}

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

    [CmdletBinding()]
    [OutputType([System.Single])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)] [Alias('px')] [System.Single] $Pixel,
        [Parameter()] [System.Int16] $Dpi = 96
    )
    process {
        return [System.Math]::Round((25.4 / $Dpi) * $Pixel, 2);
    }
}

function ConvertInToMm {
    <#
    .SYNOPSIS
        Convert inches into millimeters
    #>

    [CmdletBinding()]
    [OutputType([System.Single])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)] [Alias('in')] [System.Single] $Inch
    )
    process {
        return [System.Math]::Round($Inch * 25.4, 2);
    }
}

function ConvertMmToIn {
    <#
    .SYNOPSIS
        Convert millimeters into inches
    #>

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

function ConvertMmToPt {
    <#
    .SYNOPSIS
        Convert millimeters into points
    #>

    [CmdletBinding()]
    [OutputType([System.Single])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)] [Alias('mm','Millimetre')] [System.Single] $Millimeter
    )
    return ((ConvertMmToIn $Millimeter) / 0.0138888888888889);
}

function ConvertMmToTwips {
    <#
    .SYNOPSIS
        Convert millimeters into twips
    .NOTES
        1 twip = 1/20th pt
    #>

    [CmdletBinding()]
    [OutputType([System.Single])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)] [Alias('mm','Millimetre')] [System.Single] $Millimeter
    )
    process {
        return (ConvertMmToIn -Millimeter $Millimeter) * 1440;
    }
}

function ConvertMmToOctips {
    <#
    .SYNOPSIS
        Convert millimeters into octips
    .NOTES
        1 "octip" = 1/8th pt
    #>

    [CmdletBinding()]
    [OutputType([System.Single])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)] [Alias('mm','Millimetre')] [System.Single] $Millimeter
    )
    process {
        return (ConvertMmToIn -Millimeter $Millimeter) * 576;
    }
}

function ConvertMmToEm {
    <#
    .SYNOPSIS
        Convert millimeters into em
    #>

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

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

    [CmdletBinding()]
    [OutputType([System.Single])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)] [Alias('mm','Millimetre')] [System.Single] $Millimeter,
        [Parameter()] [System.Int16] $Dpi = 96
    )
    process {
        $pixels = [System.Int16] ((ConvertMmToIn -Millimeter $Millimeter) * $Dpi);
        if ($pixels -lt 1) { return 1; }
        else { return $pixels; }
    }
}

function Document {
    <#
    .SYNOPSIS
        Initializes a new PScribo document object.
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        ## PScribo document name
        [Parameter(Mandatory, Position = 0)] [System.String] $Name,
        ## PScribo document DSL script block containing Section, Paragraph and/or Table etc. commands.
        [Parameter(Position = 1)] [System.Management.Automation.ScriptBlock] $ScriptBlock = $(throw $localized.NoScriptBlockProvidedError) 
    )
    begin {
        $pluginName = 'Document';
        #region Document Private Functions
        function New-PScriboDocument {
            <#
            .SYNOPSIS
                Initializes a new PScript document object.
            .NOTES
                This is an internal function and should not be called directly.
            #>

            [CmdletBinding()]
            [OutputType([System.Management.Automation.PSCustomObject])]
            param (
                ## PScribo document name
                [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String] $Name
            )
            process {
                WriteLog -Message ($localized.DocumentProcessingStarted -f $Name);
                $typeName = 'PScribo.Document';
                $pscriboDocument = [PSCustomObject] @{
                    Id = $Name.Replace(' ', '').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);
                    DefaultStyle = $null;
                    DefaultTableStyle = $null;
                    TOC = New-Object -TypeName System.Collections.ArrayList;
                }
                GlobalOption -MarginTopAndBottom 72 -MarginLeftAndRight 54 -PageSize A4 -Verbose:$false -DefaultFont 'Calibri','Candara','Segoe','Segoe UI','Optima','Arial','Sans-Serif';
                ## Set "default" styles
                Style -Name Normal -Default;
                Style -Name Title -Size 28 -Color 0072af;
                Style -Name TOC -Size 16 -Color 0072af;
                Style -Name 'Heading 1' -Size 16 -Color 0072af;
                Style -Name 'Heading 2' -Size 14 -Color 0072af;
                Style -Name 'Heading 3' -Size 12 -Color 0072af;
                Style -Name TableDefaultHeading -Size 11 -Color fff -Bold -BackgroundColor 4472c4;
                Style -Name TableDefaultRow -Size 11;
                Style -Name TableDefaultAltRow -BackgroundColor d0ddee;
                Style -Name Footer -Size 8 -Color 0072af;
                TableStyle TableDefault -BorderWidth 1 -BorderColor 2a70be -HeaderStyle TableDefaultHeading -RowStyle TableDefaultRow -AlternateRowStyle TableDefaultAltRow -Default;
                return $pscriboDocument;
            } #end process
        } #end function NewPScriboDocument

        function Process-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 ( )
            function Process-PScriboSectionLevel {
                <#
                .SYNOPSIS
                    Nested function that processes each document/TOC nested section
                #>

                [CmdletBinding()]
                param (
                    [Parameter(Mandatory)] [ValidateNotNull()] [PSCustomObject] $Section,
                    [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String] $Number
                )
                
                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
                        Process-PScriboSectionLevel -Section $s -Number $sectionNumber;
                        $minorNumber++;
                    }
                } #end foreach section
            } #end function Process-PScriboSectionLevel

            $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) {
                        Process-PScriboSectionLevel -Section $s -Number $majorNumber;
                        $majorNumber++;
                    }
                } #end if
            } #end foreach
        } #end function process-psscribosection
        #endregion Document Private Functions
    } #end begin
    process {
        $stopwatch = [Diagnostics.Stopwatch]::StartNew();
        $pscriboDocument = New-PScriboDocument -Name $Name;
        ## Call the Document script block
        foreach ($result in & $ScriptBlock) {
            [ref] $null = $pscriboDocument.Sections.Add($result);
        }
        Process-PScriboSection;
        WriteLog -Message ($localized.DocumentProcessingCompleted -f $pscriboDocument.Name);
        $stopwatch.Stop();
        WriteLog -Message ($localized.TotalProcessingTime -f $stopwatch.Elapsed.TotalSeconds);
        return $pscriboDocument;
    } #end process
} #end function Document

function Export-Document {
    <#
    .SYNOPSIS
        Exports a PScribo document object to one or more output formats.
    #>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param (
        ## PScribo document object
        [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $Document,
        ## Output formats
        [Parameter(Mandatory)] [ValidateNotNull()] [System.String[]] $Format,
        ## Output file path
        [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Path = (Get-Location -PSProvider FileSystem),
        ## PScribo document export option
        [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.Collections.Hashtable] $Options
    )
    begin {
        try { $Path = Resolve-Path $Path -ErrorAction SilentlyContinue; }
        catch { }

        if (-not (Test-Path $Path -PathType Container)) {
            ## Check $Path is a directory
            throw ($localized.InvalidDirectoryPathError -f $Path);
        }
    }
    process {
        foreach ($f in $Format) {
            WriteLog -Message ($localized.DocumentInvokePlugin -f $f) -Plugin 'Export';
            ## Call specified output plugin
            #try {
                ## Dynamically generate the output format function name
                $outputFormat = 'Out{0}' -f $f;
                & $outputFormat -Document $Document -Path $Path; # -ErrorAction Stop;
            #}
            #catch [System.Management.Automation.CommandNotFoundException] {
            # Write-Warning ('Output format "{0}" is unsupported.' -f $f);
            #}
        } # end foreach
    } #end process
} #end function Export-Document

function GlobalOption {
    <#
    .SYNOPSIS
        Initializes a new PScribo global options/settings.
    .NOTES
        Options are reset upon each invocation.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Margin')]
    param (
        ## Forces document header to be displayed in upper case.
        [System.Management.Automation.SwitchParameter] $ForceUppercaseHeader,
        ## Forces all section headers to be displayed in upper case.
        [System.Management.Automation.SwitchParameter] $ForceUppercaseSection,
        ## Enable section/heading numbering
        [System.Management.Automation.SwitchParameter] $EnableSectionNumbering,
        ## Default space replacement separator
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('Separator')] [AllowNull()] [ValidateLength(0,1)] [System.String] $SpaceSeparator,
        ## Default page top, bottom, left and right margin (pt)
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Margin')] [System.UInt16] $Margin = 72,
        ## Default page top and bottom margins (pt)
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'CustomMargin')] [System.UInt16] $MarginTopAndBottom,
        ## Default page left and right margins (pt)
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'CustomMargin')] [System.UInt16] $MarginLeftAndRight,
        ## Default page size
        [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('A4','Legal','Letter')] [System.String] $PageSize = 'A4',
        ## Default document font(s)
        [Parameter(ValueFromPipelineByPropertyName)] [System.String[]] $DefaultFont = @('Calibri','Candara','Segoe','Segoe UI','Optima','Arial','Sans-Serif')
    )
    process {
        $localized.DocumentOptions | WriteLog;
        if ($SpaceSeparator) {
            WriteLog -Message ($localized.DocumentOptionSpaceSeparator -f $SpaceSeparator);
            $pscriboDocument.Options['SpaceSeparator'] = $SpaceSeparator;
        }
        if ($ForceUppercaseHeader) {
            $localized.DocumentOptionUppercaseHeadings | WriteLog;
            $pscriboDocument.Options['ForceUppercaseHeader'] = $true;
            $pscriboDocument.Name = $pscriboDocument.Name.ToUpper();
        } #end if ForceUppercaseHeader
        if ($ForceUppercaseSection) {
            $localized.DocumentOptionUppercaseSections | WriteLog;
            $pscriboDocument.Options['ForceUppercaseSection'] = $true;
        } #end if ForceUppercaseSection
        if ($EnableSectionNumbering) {
            $localized.DocumentOptionSectionNumbering | WriteLog;
            $pscriboDocument.Options['EnableSectionNumbering'] = $true;
        }
        if ($DefaultFont) {
            WriteLog -Message ($localized.DocumentOptionDefaultFont -f ([System.String]::Join(', ', $DefaultFont)));
            $pscriboDocument.Options['DefaultFont'] = $DefaultFont;
        }
        
        if ($PSCmdlet.ParameterSetName -eq 'CustomMargin') {
            if ($MarginTopAndBottom -eq 0) { $MarginTopAndBottom = 72; }
            if ($MarginLeftAndRight -eq 0) { $MarginTopAndBottom = 72; }
            $pscriboDocument.Options['MarginTop'] = ConvertPtToMm -Point $MarginTopAndBottom;
            $pscriboDocument.Options['MarginBottom'] = $pscriboDocument.Options['MarginTop'];
            $pscriboDocument.Options['MarginLeft'] = ConvertPtToMm -Point $MarginLeftAndRight;
            $pscriboDocument.Options['MarginRight'] = $pscriboDocument.Options['MarginLeft'];
        }
        else {
            $pscriboDocument.Options['MarginTop'] = ConvertPtToMm -Point $Margin;
            $pscriboDocument.Options['MarginBottom'] = $pscriboDocument.Options['MarginTop'];
            $pscriboDocument.Options['MarginLeft'] = $pscriboDocument.Options['MarginTop'];
            $pscriboDocument.Options['MarginRight'] = $pscriboDocument.Options['MarginTop'];
        }
        WriteLog -Message ($localized.DocumentOptionPageTopMargin -f $pscriboDocument.Options['MarginTop']);
        WriteLog -Message ($localized.DocumentOptionPageRightMargin -f $pscriboDocument.Options['MarginRight']);
        WriteLog -Message ($localized.DocumentOptionPageBottomMargin -f $pscriboDocument.Options['MarginBottom']);
        WriteLog -Message ($localized.DocumentOptionPageLeftMargin -f $pscriboDocument.Options['MarginLeft']);

        ## Convert page size
        ($localized.DocumentOptionPageSize -f $PageSize) | WriteLog;
        switch ($PageSize) {
            'A4' {
                $pscriboDocument.Options['PageWidth'] = 210.0;
                $pscriboDocument.Options['PageHeight'] = 297.0;
            }
            'Legal' {
                $pscriboDocument.Options['PageWidth'] = 215.9;
                $pscriboDocument.Options['PageHeight'] = 355.6;
            }
            'Letter' {
                $pscriboDocument.Options['PageWidth'] = 215.9;
                $pscriboDocument.Options['PageHeight'] = 279.4;
            }
        } #end switch
        ($localized.DocumentOptionPageHeight -f $pscriboDocument.Options['PageHeight']) | WriteLog;
        ($localized.DocumentOptionPageWidth -f $pscriboDocument.Options['PageWidth']) | WriteLog;
    } #end process
} #end function GlobalOption

function LineBreak {
    <#
    .SYNOPSIS
        Initializes a new PScribo line break object.
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        [Parameter(Position = 0)] [ValidateNotNullOrEmpty()] [System.String] $Id = [System.Guid]::NewGuid().ToString()
    )
    begin {
        #region LineBreak Private Functions
        function New-PScriboLineBreak {
            <#
            .SYNOPSIS
                Initializes a new PScribo line break object.
            .NOTES
                This is an internal function and should not be called directly.
            #>

            [CmdletBinding()]
            [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;
            }
        } #end function New-PScriboLineBreak
        #endregion LineBreak Private Functions
    } #end begin
    process {
        WriteLog -Message $localized.ProcessingLineBreak;
        return (New-PScriboLineBreak @PSBoundParameters);
    } #end process
} #end function LineBreak

function WriteLog {
    <#
    .SYNOPSIS
        Writes message to the verbose, warning or debug streams. Output is
        prefixed with the time and PScribo plugin name.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Verbose')]
    param (
        ## Message to send to the Verbose stream
        [Parameter(ValueFromPipeline, ParameterSetName = 'Verbose')]
        [Parameter(ValueFromPipeline, ParameterSetName = 'Warning')]
        [Parameter(ValueFromPipeline, ParameterSetName = 'Debug')]
        [ValidateNotNullOrEmpty()] [System.String] $Message,
        ## PScribo plugin name
        [Parameter()] [System.String] $Plugin,
        ## Redirect message to the Warning stream
        [Parameter(ParameterSetName = 'Warning')] [System.Management.Automation.SwitchParameter] $IsWarning,
        ## Redirect message to the Debug stream
        [Parameter(ParameterSetName = 'Debug')] [System.Management.Automation.SwitchParameter] $IsDebug,
        ## Padding/indent section level
        [Parameter(ValueFromPipeline, ParameterSetName = 'Verbose')]
        [Parameter(ValueFromPipeline, ParameterSetName = 'Warning')]
        [Parameter(ValueFromPipeline, ParameterSetName = 'Debug')]
        [ValidateNotNullOrEmpty()] [System.Int16] $Indent
    )
    process {
        if ([System.String]::IsNullOrEmpty($Plugin)) {
            ## Attempt to resolve the plugin name from the parent scope
            $Plugin = $pluginName;
        }
        ## Center plugin name
        $pluginPaddingSize = [System.Math]::Floor((10 - $Plugin.Length) / 2);
        $pluginPaddingString = ''.PadRight($pluginPaddingSize);
        $Plugin = '{0}{1}' -f $pluginPaddingString, $Plugin;
        $Plugin = $Plugin.PadRight(10)
        $date = Get-Date;
        $sectionLevelPadding = ''.PadRight($Indent);
        $formattedMessage = '[ {0} ] [{1}] - {2}{3}' -f $date.ToString('HH:mm:ss:fff'), $Plugin, $sectionLevelPadding, $Message;
        switch ($PSCmdlet.ParameterSetName) {
            'Warning' { Write-Warning -Message $formattedMessage; }
            'Debug' { Write-Debug -Message $formattedMessage; }
            Default { Write-Verbose -Message $formattedMessage; }
        }
    } #end process
} #end function WriteLog

function PageBreak {
    <#
    .SYNOPSIS
        Creates a PScribo page break object.
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        [Parameter(Position = 0)] [ValidateNotNullOrEmpty()] [System.String] $Id = [System.Guid]::NewGuid().ToString()
    )
    begin {
        #region PageBreak Private Functions
        function New-PScriboPageBreak {
            <#
            .SYNOPSIS
                Creates a PScribo page break object.
            .NOTES
                This is an internal function and should not be called directly.
            #>

            [CmdletBinding()]
            [OutputType([System.Management.Automation.PSCustomObject])]
            param (
                [Parameter(Position = 0)] [ValidateNotNullOrEmpty()] [System.String] $Id = [System.Guid]::NewGuid().ToString()
            )
            process {
                $typeName = 'PScribo.PageBreak';
                $pscriboDocument.Properties['PageBreaks']++;
                $pscriboPageBreak = [PSCustomObject] @{
                    Id = $Id;
                    Type = $typeName;
                }
                return $pscriboPageBreak;
            }
        } #end function New-PScriboPageBreak
        #endregion PageBreak Private Functions
    } #end begin
    process {
        WriteLog -Message $localized.ProcessingPageBreak;
        return (New-PScriboPageBreak -Id $Id);
    }
} #end function PageBreak

function Paragraph {
    <#
    .SYNOPSIS
        Initializes a new PScribo paragraph object.
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        ## Paragraph Id and Xml element name
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0)] [ValidateNotNullOrEmpty()] [System.String] $Name,
        ## Paragraph text. If empty $Name/Id will be used.
        [Parameter(ValueFromPipelineByPropertyName, Position = 1)] [AllowNull()] [System.String] $Text = $null,
        ## Output value override, i.e. for Xml elements. If empty $Text will be used.
        [Parameter(ValueFromPipelineByPropertyName, Position = 2)] [AllowNull()] [System.String] $Value = $null,
        ## Paragraph style Name/Id reference.
        [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.String] $Style = $null,
        ## No new line - ONLY IMPLEMENTED FOR TEXT OUTPUT
        [System.Management.Automation.SwitchParameter] $NoNewLine,
        ## Override the bold style
        [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Bold,
        ## Override the italic style
        [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Italic,
        ## Override the underline style
        [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Underline,
        ## Override the font name(s)
        [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String[]] $Font,
        ## Override the font size (pt)
        [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.UInt16] $Size = $null,
        ## Override the font color/colour
        [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.String] $Color = $null,
        ## Tab indent
        [Parameter(ValueFromPipelineByPropertyName)] [System.Int32] [ValidateRange(0,10)] $Tabs = 0
    )
    begin {
        #region Paragraph Private Functions
        function New-PScriboParagraph {
            <#
            .SYNOPSIS
                Initializes a new PScribo paragraph object.
            .NOTES
                This is an internal function and should not be called directly.
            #>

            [CmdletBinding()]
            [OutputType([System.Management.Automation.PSCustomObject])]
            param (
                ## Paragraph Id (and Xml) element name
                [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0)] [ValidateNotNullOrEmpty()] [System.String] $Name,
                ## Paragraph text. If empty $Name/Id will be used.
                [Parameter(ValueFromPipelineByPropertyName, Position = 1)] [AllowNull()] [System.String] $Text = $null,
                ## Ouptut value override, i.e. for Xml elements. If empty $Text will be used.
                [Parameter(ValueFromPipelineByPropertyName, Position = 2)] [AllowNull()] [System.String] $Value = $null,
                ## Paragraph style Name/Id reference.
                [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.String] $Style = $null,
                ## No new line - ONLY IMPLEMENTED FOR TEXT OUTPUT
                [Switch] $NoNewLine,
                ## Override the bold style
                [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Bold,
                ## Override the italic style
                [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Italic,
                ## Override the underline style
                [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Underline,
                ## Override the font name(s)
                [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String[]] $Font,
                ## Override the font size (pt)
                [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.UInt16] $Size = $null,
                ## Override the font color/colour
                [Parameter(ValueFromPipelineByPropertyName)] [Alias('Colour')] [AllowNull()] [System.String] $Color = $null,
                ## Tab indent
                [Parameter()] [ValidateRange(0,10)] [System.Int32] $Tabs = 0
            )
            begin {
                if (-not ([string]::IsNullOrEmpty($Text))) {
                    $Name = $Name.Replace(' ', $pscriboDocument.Options['SpaceSeparator']).ToUpper();
                }
                if ($Color) {
                    $Color = Resolve-PScriboStyleColor -Color $Color;
                }
            } #end begin
            process {
                $typeName = 'PScribo.Paragraph';
                $pscriboDocument.Properties['Paragraphs']++;
                $pscriboParagraph = [PSCustomObject] @{
                    Id = $Name;
                    Text = $Text;
                    Type = $typeName;
                    Style = $Style;
                    Value = $Value;
                    NewLine = !$NoNewLine;
                    Tabs = $Tabs;
                    Bold = $Bold;
                    Italic = $Italic;
                    Underline = $Underline;
                    Font = $Font;
                    Size = $Size;
                    Color = $Color;
                }
                return $pscriboParagraph;
            } #end process
        } #end function New-PScriboParagraph
        #endregion Paragraph Private Functions
    } #end begin
    process {
        if ($Name.Length -gt 40) { $paragraphDisplayName = '{0}[..]' -f $Name.Substring(0,36); }
        else { $paragraphDisplayName = $Name; }
        WriteLog -Message ($localized.ProcessingParagraph -f $paragraphDisplayName);
        return (New-PScriboParagraph @PSBoundParameters);
    } #end process
} #end function Paragraph

function Section {
    <#
    .SYNOPSIS
        Initializes a new PScribo section object.
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        ## PScribo section heading/name.
        [Parameter(Mandatory, Position = 0)] [System.String] $Name,
        ## PScribo document script block.
        [Parameter(Position = 1)] [ValidateNotNull()] [System.Management.Automation.ScriptBlock] $ScriptBlock = $(throw $localized.NoScriptBlockProvidedError),
        ## PScribo style applied to document section.
        [Parameter()] [System.String] [AllowNull()] $Style = $null,
        ## Section is excluded from TOC/section numbering.
        [Parameter()] [System.Management.Automation.SwitchParameter] $ExcludeFromTOC
    )
    begin {
        #region Section Private Functions
        function New-PScriboSection {
            <#
            .SYNOPSIS
                Initializes new PScribo section object.
            .NOTES
                This is an internal function and should not be called directly.
            #>

            [CmdletBinding()]
            [OutputType([System.Management.Automation.PSCustomObject])]
            param (
                ## PScribo section heading/name.
                [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String] $Name,
                ## PScribo style applied to document section.
                [Parameter()] [AllowNull()] [System.String] $Style = $null,
                ## Section is excluded from TOC/section numbering.
                [Parameter()] [System.Management.Automation.SwitchParameter] $IsExcluded
            )
            process {
                $typeName = 'PScribo.Section';
                $pscriboDocument.Properties['Sections']++;
                $pscriboSection = [PSCustomObject] @{
                    Id = $Name.Replace(' ', $pscriboDocument.Options['SpaceSeparator']).ToUpper();
                    Level = 0;
                    Number = '';
                    Name = $Name;
                    Type = $typeName;
                    Style = $Style;
                    IsExcluded = $IsExcluded;
                    Sections = (New-Object -TypeName System.Collections.ArrayList);
                }
                return $pscriboSection;
            } #end process
        } #end function new-pscribosection
        #endregion Section Private Functions
    } #end begin
    process {
        WriteLog -Message ($localized.ProcessingSectionStarted -f $Name);
        $pscriboSection = New-PScriboSection -Name $Name -Style $Style -IsExcluded:$ExcludeFromTOC;
        WriteLog -Message ('Document: {0}: Section count: {1}.' -f $pscriboSection.Id, $results.Count) -IsDebug;
        foreach ($result in & $ScriptBlock) {
            [ref] $null = $pscriboSection.Sections.Add($result);
        }
        WriteLog -Message ($localized.ProcessingSectionCompleted -f $Name);
        return $pscriboSection;
    } #end process
} #end function Section

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';
        };
    } #end begin
    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();
    } #end process
} #end function ResolvePScriboColor

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; }
    } #end process
} #end function test-pscribostylecolor

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);
    }
} #end function Test-PScriboStyle

function Style {
    <#
    .SYNOPSIS
        Defines a new PScribo formatting style.
    .DESCRIPTION
        Creates a standard format formatting style that can be applied
        to PScribo document keywords, e.g. a combination of font style, font
        weight and font size.
    .NOTES
        Not all plugins support all options.
    #>

    [CmdletBinding()]
    param (
        ## Style name
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0)] [ValidateNotNullOrEmpty()] [System.String] $Name,
        ## Font size (pt)
        [Parameter(ValueFromPipelineByPropertyName, Position = 1)] [System.UInt16] $Size = 11,
        ## Font color/colour
        [Parameter(ValueFromPipelineByPropertyName)] [Alias('Colour')] [ValidateNotNullOrEmpty()] [System.String] $Color = '000',
        ## 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')] [System.String] $Align = 'Left',
        ## Set as default style
        [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Default,
        ## Style id
        [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Id = $Name -Replace(' ',''),
        ## Font name (array of names for HTML output)
        [Parameter(ValueFromPipelineByPropertyName)] [System.String[]] $Font
    )
    begin {
        #region Style Private Functions
        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. Overrides Style.Id in HTML output.
                [Parameter(ValueFromPipelineByPropertyName)] [System.String] $CssClassId = '',
                ## Set as default style
                [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Default
            ) #end param
            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'];
                }
            } #end begin
            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;
                    Italic = $Italic;
                    Underline = $Underline;
                    Align = $Align;
                }
                $pscriboDocument.Styles[$Id] = $style;
                if ($Default) { $pscriboDocument.DefaultStyle = $style.Id; }
            } #end process
        } #end function Add-PScriboStyle
        #endregion Style Private Functions
    }
    process {
        WriteLog -Message ($localized.ProcessingStyle -f $Id);
        Add-PScriboStyle @PSBoundParameters;
    } #end process
} #end function Style

function Set-Style {
    <#
    .SYNOPSIS
        Sets the style for an individual table row or cell.
    #>

    [CmdletBinding()]
    [OutputType([System.Object])]
    param (
        ## PSCustomObject to apply the style to
        [Parameter(Mandatory, ValueFromPipeline)] [System.Object[]] [Ref] $InputObject,
        ## PScribo style Id to apply
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [System.String] $Style,
        ## Property name(s) to apply the selected style to. Leave blank to apply the style to the entire row.
        [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String[]] $Property = '',
        ## Passes the modified object back to the pipeline
        [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $PassThru
    ) #end param
    begin {
        if (-not (Test-PScriboStyle -Name $Style)) {
            Write-Error ($localized.UndefinedStyleError -f $Style);
            return;
        }
    }
    process {
        foreach ($object in $InputObject) {
            foreach ($p in $Property) {
                ## If $Property not set, __Style will apply to the whole row.
                $propertyName = '{0}__Style' -f $p;
                $object | Add-Member -MemberType NoteProperty -Name $propertyName -Value $Style -Force;
            }
        }
        if ($PassThru) {
            Write-Output -InputObject $object -NoEnumerate;
        }
    } #end process
} #end function set-tablestyle

function Table {
    <#
    .SYNOPSIS
        Defines a new PScribo document table.
    #>

    [CmdletBinding(DefaultParameterSetName = 'InputObject')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        ## Table name/Id
        [Parameter(ValueFromPipelineByPropertyName, Position = 0)]
        [ValidateNotNullOrEmpty()] [string] $Name = ([System.Guid]::NewGuid().ToString()),
        # Array of Hashtables
        [Parameter(Mandatory, ParameterSetName = 'Hashtable')]
        [ValidateNotNullOrEmpty()] [System.Collections.Specialized.OrderedDictionary[]] $Hashtable,
        # Array of objects
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'InputObject')]
        [Alias('CustomObject','Object')] [ValidateNotNullOrEmpty()] [System.Object[]] $InputObject,
        # Array of Hashtable key names or Object/PSCustomObject property names to include, in display order.
        # If not supplied then all Hashtable keys or all PSCustomObject properties will be used.
        [Parameter(ValueFromPipelineByPropertyName, Position = 1, ParameterSetName = 'InputObject')]
        [Parameter(ValueFromPipelineByPropertyName, Position = 1, ParameterSetName = 'Hashtable')]
        [Alias('Properties')] [AllowNull()] [System.String[]] $Columns = $null,
        ## Column widths as percentages. Total should not exceed 100.
        [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.UInt16[]] $ColumnWidths,
        # Array of custom table header strings in display order.
        [Parameter(ValueFromPipelineByPropertyName, Position = 2)] [AllowNull()] [System.String[]] $Headers = $null,
        ## Table style
        [Parameter(ValueFromPipelineByPropertyName, Position = 3)] [ValidateNotNullOrEmpty()] [System.String] $Style = 'TableDefault',
        # List view (no headers)
        [System.Management.Automation.SwitchParameter] $List,
        ## Table width (%), 0 = Autofit
        [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(0,100)] [System.UInt16] $Width = 100,
        ## Indent table
        [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(0,10)] [System.UInt16] $Tabs
    ) #end param
    begin {
        #region Table Private Functions
        function New-PScriboTable {
            <#
            .SYNOPSIS
                Initializes a new PScribo table object.
            #>

            [CmdletBinding()]
            [OutputType([System.Management.Automation.PSCustomObject])]
            param (
                ## Table name/Id
                [Parameter(ValueFromPipelineByPropertyName, Position = 0)]
                [ValidateNotNullOrEmpty()] [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
                [System.Management.Automation.SwitchParameter] $List,
                ## Table width (%), 0 = Autofit
                [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(0,100)] [System.UInt16] $Width = 100,
                ## Indent table
                [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(0,10)] [System.UInt16] $Tabs
            ) #end param
            process {
                $typeName = 'PScribo.Table';
                $pscriboDocument.Properties['Tables']++;
                $pscriboTable = [PSCustomObject] @{
                    Id = $Name.Replace(' ', $pscriboDocument.Options['SpaceSeparator']).ToUpper();
                    Name = $Name;
                    Type = $typeName;
                    # Headers = $Headers; ## Headers are stored as they may be required when formatting output, i.e. Word tables
                    Columns = $Columns;
                    ColumnWidths = $ColumnWidths;
                    Rows = $Rows;
                    List = $List;
                    Style = $Style;
                    Width = $Width;
                    Tabs = $Tabs;
                }
                return $pscriboTable;
            } #end process
        } #end function new-pscribotable

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

            [CmdletBinding(DefaultParameterSetName='InputObject')]
            [OutputType([System.Management.Automation.PSCustomObject])]
            param (
                ## PSCustomObject to create PScribo table row
                [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'InputObject')]
                [ValidateNotNull()] [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')]
                [AllowNull()] [System.Collections.Specialized.OrderedDictionary] $Hashtable
            )
            begin {
                Write-Debug ('Using parameter set "{0}.' -f $PSCmdlet.ParameterSetName);
            } #end begin
            process {
                switch ($PSCmdlet.ParameterSetName) {
                    'Hashtable'{
                        if (-not $Hashtable.Contains('__Style')) { $Hashtable['__Style'] = $null; }
                        ## Create and return custom object from hashtable
                        return ([PSCustomObject] $Hashtable);
                    } #end Hashtable
                    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.$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') {
                                $objectProperties[$Headers[$i]] = $InputObject.$propertyName;
                            }
                            else {
                                $objectProperties[$propertyName] = $InputObject.$propertyName;
                            }
                        } #end for
                        ## Create and return custom object
                        return ([PSCustomObject] $objectProperties);
                    } #end Default
                } #end switch
            } #end process
        } #end function New-PScriboTableRow
        #endregion Table Private Functions
        
        Write-Debug ('Using parameter set "{0}".' -f $PSCmdlet.ParameterSetName);
        [System.Collections.ArrayList] $rows = New-Object -TypeName System.Collections.ArrayList;
        WriteLog -Message ($localized.ProcessingTable -f $Name);
        if ($Headers -and (-not $Columns)) {
            WriteLog -Message $localized.TableHeadersWithNoColumnsWarning -IsWarning;
            $Headers = $Columns;
        } #end if
        elseif (($null -ne $Columns) -and ($null -ne $Headers)) {
            ## Check the number of -Headers matches the number of -Properties
            if ($Headers.Count -ne $Columns.Count) {
                WriteLog -Message $localized.TableHeadersCountMismatchWarning -IsWarning;
                $Headers = $Columns;
            }
        } #end if
        if ($ColumnWidths) {
            $columnWidthsSum = $ColumnWidths | Measure-Object -Sum | Select-Object -ExpandProperty Sum;
            if ($columnWidthsSum -ne 100) {
                WriteLog -Message ($localized.TableColumnWidthSumWarning -f $columnWidthsSum) -IsWarning;
                $ColumnWidths = $null;
            }
            elseif ($List -and $ColumnWidths.Count -ne 2) {
                WriteLog -Message $localized.ListTableColumnCountWarning -IsWarning;
                $ColumnWidths = $null;
            }
            elseif (($PSCmdlet.ParameterSetName -eq 'Hashtable') -and (-not $List) -and ($Hashtable[0].Keys.Count -ne $ColumnWidths.Count)) {
                WriteLog -Message $localized.TableColumnWidthMismatchWarning -IsWarning;
                $ColumnWidths = $null;
            }
            elseif (($PSCmdlet.ParameterSetName -eq 'InputObject') -and (-not $List) -and ($Columns.Count -ne $ColumnWidths.Count)) {
                WriteLog -Message $localized.TableColumnWidthMismatchWarning -IsWarning;
                $ColumnWidths = $null;
            }
        } #end if columnwidths
    } #end begin
    process {
        if ($null -eq $Columns) {
            ## Use all available properties
            switch ($PSCmdlet.ParameterSetName) {
                'Hashtable' {
                    $Columns = $Hashtable | Select-Object -First 1 -ExpandProperty Keys | Where-Object { $_ -notlike '*__Style' };
                }
                Default {
                    ## Pipeline objects are not available in the begin scriptblock
                    $object = $InputObject | Select-Object -First 1;
                    if ($object -is [System.Management.Automation.PSCustomObject]) {
                        $Columns = $object.PSObject.Properties | Where-Object Name -notlike '*__Style' | Select-Object -ExpandProperty Name;
                    }
                    else {
                        $Columns = Get-Member -InputObject $object -MemberType Properties | Where-Object Name -notlike '*__Style' | Select-Object -ExpandProperty Name;
                    }
                } #end default
            } #end switch parametersetname
        } # end if not columns
        switch ($PSCmdlet.ParameterSetName) {
            'Hashtable' {
                foreach ($nestedHashtable in $Hashtable) {
                    $customObject = New-PScriboTableRow -Hashtable $nestedHashtable;
                    [ref] $null = $rows.Add($customObject);
                } #end foreach nested hashtable entry
            } #end hashtable
            Default {
                foreach ($object in $InputObject) {
                    $customObject = New-PScriboTableRow -InputObject $object -Properties $Columns -Headers $Headers;
                    [ref] $null = $rows.Add($customObject);
                } #end foreach inputobject
            } #end default
        } #end switch
    } #end process
    end {
        ## Reset the column names as the object have been rewritten with their headers
        if ($Headers) { $Columns = $Headers; }
        $table = @{
            Name = $Name;
            Columns = $Columns;
            ColumnWidths = $ColumnWidths;
            Rows = $rows;
            List = $List;
            Style = $Style;
            Width = $Width;
            Tabs = $Tabs;
        }
        return (New-PScriboTable @table);
    } #end end
} #end function Table

function TableStyle {
    <#
    .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 = 'Default',
        ## Row Style Id
        [Parameter(ValueFromPipelineByPropertyName, Position = 2)] [ValidateNotNullOrEmpty()] [System.String] $RowStyle = 'Default',
        ## Header Row Style Id
        [Parameter(ValueFromPipelineByPropertyName, Position = 3)] [AllowNull()] [Alias('AlternatingRowStyle')] [System.String] $AlternateRowStyle = 'Default',
        ## Table border size/width (pt)
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Border')] [AllowNull()] [System.Single] $BorderWidth = 0,
        ## Table border colour
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Border')] [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',
        ## Set as default table style
        [System.Management.Automation.SwitchParameter] $Default
    ) #end param
    begin {
        #region TableStyle Private Functions
        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',
                ## Header Row Style Id
                [Parameter(ValueFromPipelineByPropertyName, Position = 3)] [AllowNull()] [Alias('AlternatingRowStyle')] [System.String] $AlternateRowStyle = 'Normal',
                ## Table border size/width (pt)
                [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Border')] [AllowNull()] [System.Single] $BorderWidth = 0,
                ## Table border colour
                [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Border')] [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',
                ## Set as default table style
                [System.Management.Automation.SwitchParameter] $Default
            ) #end param
            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);
                }
            } #end begin
            process {
                $pscriboDocument.Properties['TableStyles']++;
                $tableStyle = [PSCustomObject] @{
                    Id = $Id.Replace(' ', $pscriboDocument.Options['SpaceSeparator']);
                    Name = $Id;
                    HeaderStyle = $HeaderStyle;
                    RowStyle = $RowStyle;
                    AlternateRowStyle = $AlternateRowStyle;
                    PaddingTop = ConvertPtToMm $PaddingTop;
                    PaddingLeft = ConvertPtToMm $PaddingLeft;
                    PaddingBottom = ConvertPtToMm $PaddingBottom;
                    PaddingRight = ConvertPtToMm $PaddingRight;
                    Align = $Align;
                    BorderWidth = ConvertPtToMm $BorderWidth;
                    BorderStyle = $borderStyle;
                    BorderColor = Resolve-PScriboStyleColor -Color $BorderColor;
                }
                $pscriboDocument.TableStyles[$Id] = $tableStyle;
                if ($Default) { $pscriboDocument.DefaultTableStyle = $tableStyle.Id; }
            } #end process
        } #end function Add-PScriboTableStyle
        #endregion TableStyle Private Functions
    }
    process {
        WriteLog -Message ($localized.ProcessingTableStyle -f $Id);
        Add-PScriboTableStyle @PSBoundParameters;
    }
} #end function tablestyle

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

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $Name = 'Contents'
    )
    begin {
        #region TOC Private Functions
        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()]
            [OutputType([System.Management.Automation.PSCustomObject])]
            param (
                [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $Name = 'Contents'
            )
            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;
                }
                return $pscriboTOC;
            } #end process
        } #end function New-PScriboLTOC
        #endregion TOC Private Functions
    } #end begin
    process {
        WriteLog -Message ($localized.ProcessingTOC -f $Name);
        return (New-PScriboTOC @PSBoundParameters);
    }
} #end function TOC

function OutHtml {
    <#
    .SYNOPSIS
        Html output plugin for PScribo.
    .DESCRIPTION
        Outputs a Html file representation of a PScribo document object.
    #>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param (
        ## PScribo document object to convert to a text document
        [Parameter(Mandatory, ValueFromPipeline)] [PSCustomObject] $Document,
        ## Output directory path for the .html file
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $Path,
        ### Hashtable of all plugin supported options
        [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.Collections.Hashtable] $Options
    )
    begin {
        $pluginName = 'Html';
        #region OutHtml Private Functions
        function GetHtmlStyle {
            <#
            .SYNOPSIS
                Generates html stylesheet style attributes from a PScribo document style.
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param (
                ## PScribo document style
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $Style
            )
            process {
                $styleBuilder = New-Object -TypeName System.Text.StringBuilder;
                [ref] $null = $styleBuilder.AppendFormat(" font-family: '{0}';", $Style.Font -Join "','");
                [ref] $null = $styleBuilder.AppendFormat(' font-size: {0:0.00}em;', $Style.Size / 12);
                [ref] $null = $styleBuilder.AppendFormat(' text-align: {0};', $Style.Align.ToLower());
                if ($Style.Bold) { [ref] $null = $styleBuilder.Append(' font-weight: bold;'); }
                else { [ref] $null = $styleBuilder.Append(' font-weight: normal;'); }
                if ($Style.Italic) { [ref] $null = $styleBuilder.Append(' font-style: italic;'); }
                if ($Style.Underline) { [ref] $null = $styleBuilder.Append(' text-decoration: underline;'); }
                if ($Style.Color.StartsWith('#')) { [ref] $null = $styleBuilder.AppendFormat(' color: {0};', $Style.Color.ToLower()); }
                else { [ref] $null = $styleBuilder.AppendFormat(' color: #{0};', $Style.Color); }
                if ($Style.BackgroundColor) {
                    if ($Style.BackgroundColor.StartsWith('#')) { [ref] $null = $styleBuilder.AppendFormat(' background-color: {0};', $Style.BackgroundColor.ToLower()); }
                    else { [ref] $null = $styleBuilder.AppendFormat(' background-color: #{0};', $Style.BackgroundColor.ToLower()); }
                }
                return $styleBuilder.ToString();
            }
        } #end function GetHtmlStyle

        function GetHtmlTableStyle {
            <#
            .SYNOPSIS
                Generates html stylesheet style attributes from a PScribo document table style.
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param (
                ## PScribo document table style
                [Parameter(Mandatory, ValueFromPipeline)]
                [System.Object] $TableStyle
            )
            process {
                $tableStyleBuilder = New-Object -TypeName 'System.Text.StringBuilder';
                [ref] $null = $tableStyleBuilder.AppendFormat(' padding: {0}em {1}em {2}em {3}em;',
                                                                                                    (ConvertMmToEm $TableStyle.PaddingTop),
                                                                                                        (ConvertMmToEm $TableStyle.PaddingRight),
                                                                                                            (ConvertMmToEm $TableStyle.PaddingBottom),
                                                                                                                (ConvertMmToEm $TableStyle.PaddingLeft));
                
                [ref] $null = $tableStyleBuilder.AppendFormat(' border-style: {0};', $TableStyle.BorderStyle.ToLower());
                if ($TableStyle.BorderWidth -gt 0) {
                    [ref] $null = $tableStyleBuilder.AppendFormat(' border-width: {0}em;', (ConvertMmToEm $TableStyle.BorderWidth));
                    if ($TableStyle.BorderColor.Contains('#')) {
                        [ref] $null = $tableStyleBuilder.AppendFormat(' border-color: {0};', $TableStyle.BorderColor);
                    }
                    else {
                        [ref] $null = $tableStyleBuilder.AppendFormat(' border-color: #{0};', $TableStyle.BorderColor);
                    }
                }
                [ref] $null = $tableStyleBuilder.Append(' border-collapse: collapse;');
                ## <table align="center"> is deprecated in Html5
                if ($TableStyle.Align -eq 'Center') {
                    [ref] $null = $tableStyleBuilder.Append(' margin-left: auto; margin-right: auto;');
                }
                elseif ($TableStyle.Align -eq 'Right') {
                    [ref] $null = $tableStyleBuilder.Append(' margin-left: auto; margin-right: 0;');
                }
                return $tableStyleBuilder.ToString();
            }
        } #end function outhtmltablestyle

        function GetHtmlTableDiv {
            <#
            .SYNOPSIS
                Generates Html <div style=..><table style=..> tags based upon table width, columns and indentation
            .NOTES
                A <div> is required to ensure that the table stays within the "page" boundaries/margins.
            #>

            param (
                [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [System.Object] $Table
            )
            process {
                $divBuilder = New-Object -TypeName 'System.Text.StringBuilder';
                if ($Table.Tabs -gt 0) {
                    [ref] $null = $divBuilder.AppendFormat('<div style="margin-left: {0}em;">' -f (ConvertMmToEm -Millimeter (12.7 * $Table.Tabs)));
                }
                else {
                    [ref] $null = $divBuilder.Append('<div>' -f (ConvertMmToEm -Millimeter (12.7 * $Table.Tabs)));
                }
                if ($Table.List) {
                    [ref] $null = $divBuilder.AppendFormat('<table class="{0}-list"', $Table.Style.ToLower());
                }
                else {
                    [ref] $null = $divBuilder.AppendFormat('<table class="{0}"', $Table.Style.ToLower());
                }
                $styleElements = @();
                if ($Table.Width -gt 0) {
                    $styleElements += 'width:{0}%;' -f $Table.Width;
                }
                if ($Table.ColumnWidths) {
                    $styleElements += 'table-layout: fixed;';
                    $styleElements += 'word-break: break-word;'
                }
                if ($styleElements.Count -gt 0) {
                    [ref] $null = $divBuilder.AppendFormat(' style="{0}">', [String]::Join(' ', $styleElements));
                }
                else {
                    [ref] $null = $divBuilder.Append('>');
                }
                return $divBuilder.ToString();
            }
        } #end function GetHtmlTableDiv

        function GetHtmlTableColGroup {
            <#
            .SYNOPSIS
                Generates Html <colgroup> tags based on table column widths
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [System.Object] $Table
            )
            process {
                $colGroupBuilder = New-Object -TypeName 'System.Text.StringBuilder';
                if ($Table.ColumnWidths) {
                    [ref] $null = $colGroupBuilder.Append('<colgroup>');
                    foreach ($columnWidth in $Table.ColumnWidths) {
                        if ($null -eq $columnWidth) {
                            [ref] $null = $colGroupBuilder.Append('<col />');
                        }
                        else {
                            [ref] $null = $colGroupBuilder.AppendFormat('<col style="max-width:{0}%; min-width:{0}%; width:{0}%" />', $columnWidth);
                        }
                    }
                    [ref] $null = $colGroupBuilder.AppendLine('</colgroup>');
                }
                return $colGroupBuilder.ToString();
            }
        } #end function GetHtmlTableDiv

        function OutHtmlTOC {
            <#
            .SYNOPSIS
                Generates Html table of contents.
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $TOC
            )
            process {
                $tocBuilder = New-Object -TypeName 'System.Text.StringBuilder';
                [ref] $null = $tocBuilder.AppendFormat('<h1 class="TOC">{0}</h1>', $TOC.Name);
                [ref] $null = $tocBuilder.AppendLine('<table style="width: 100%;">');
                foreach ($tocEntry in $Document.TOC) {
                    $sectionNumberIndent = '&nbsp;&nbsp;&nbsp;' * $tocEntry.Level;
                    if ($Document.Options['EnableSectionNumbering']) {
                        [ref] $null = $tocBuilder.AppendFormat('<tr><td>{0}</td><td>{1}<a href="#{2}" style="text-decoration: none;">{3}</a></td></tr>', $tocEntry.Number, $sectionNumberIndent, $tocEntry.Id, $tocEntry.Name).AppendLine();
                    }
                    else {
                        [ref] $null = $tocBuilder.AppendFormat('<tr><td>{0}<a href="#{1}" style="text-decoration: none;">{2}</a></td></tr>', $sectionNumberIndent, $tocEntry.Id, $tocEntry.Name).AppendLine();
                    }
                }
                [ref] $null = $tocBuilder.AppendLine('</table>');
                return $tocBuilder.ToString();
            } #end process
        } #end function OutHtmlTOC

        function OutHtmlBlankLine {
            <#
            .SYNOPSIS
                Outputs html PScribo.Blankline.
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $BlankLine
            )
            process {
                $blankLineBuilder = New-Object -TypeName System.Text.StringBuilder;
                for ($i = 0; $i -lt $BlankLine.LineCount; $i++) {
                    [ref] $null = $blankLineBuilder.Append('<br />');
                }
                return $blankLineBuilder.ToString();
            } #end process
        } #end function OutHtmlBlankLine

        function OutHtmlStyle {
            <#
            .SYNOPSIS
                Generates an in-line HTML CSS stylesheet from a PScribo document styles and table styles.
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param (
                ## PScribo document styles
                [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] $Styles,
                ## PScribo document tables styles
                [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] $TableStyles
            )
            process {
                $stylesBuilder = New-Object -TypeName 'System.Text.StringBuilder';
                [ref] $null = $stylesBuilder.AppendLine('<style type="text/css">');
                ## Add HTML page layout styling options
                [ref] $null = $stylesBuilder.AppendLine('html { height: 100%; -webkit-background-size: cover; -moz-background-size: cover; -o-background-size: cover; background-size: cover; background: #f8f8f8; }');
                [ref] $null = $stylesBuilder.Append("page { background: white; width: $($Document.Options['PageWidth'])mm; display: block; margin-top: 1em; margin-left: auto; margin-right: auto; margin-bottom: 1em; ");
                [ref] $null = $stylesBuilder.AppendLine('border-style: solid; border-width: 1px; border-color: #c6c6c6; }');
                [ref] $null = $stylesBuilder.AppendLine('@media print { body, page { margin: 0; box-shadow: 0; } }');
                [ref] $null = $stylesBuilder.AppendLine('hr { margin-top: 1.0em; }');
                foreach ($style in $Styles.Keys) {
                    ## Build style
                    $htmlStyle = GetHtmlStyle -Style $Styles[$style];
                    [ref] $null = $stylesBuilder.AppendFormat(' .{0} {{{1} }}', $Styles[$style].Id, $htmlStyle).AppendLine(); 
                }
                foreach ($tableStyle in $TableStyles.Keys) {
                    $tStyle = $TableStyles[$tableStyle];
                    $tableStyleId = $tStyle.Id.ToLower();
                    $htmlTableStyle = GetHtmlTableStyle -TableStyle $tStyle;
                    $htmlHeaderStyle = GetHtmlStyle -Style $Styles[$tStyle.HeaderStyle];
                    $htmlRowStyle = GetHtmlStyle -Style $Styles[$tStyle.RowStyle];
                    $htmlAlternateRowStyle = GetHtmlStyle -Style $Styles[$tStyle.AlternateRowStyle];
                    ## Generate Standard table styles
                    [ref] $null = $stylesBuilder.AppendFormat(' table.{0} {{{1} }}', $tableStyleId, $htmlTableStyle).AppendLine();
                    [ref] $null = $stylesBuilder.AppendFormat(' table.{0} th {{{1}{2} }}', $tableStyleId, $htmlHeaderStyle, $htmlTableStyle).AppendLine();
                    [ref] $null = $stylesBuilder.AppendFormat(' table.{0} tr:nth-child(odd) td {{{1}{2} }}', $tableStyleId, $htmlRowStyle, $htmlTableStyle).AppendLine();
                    [ref] $null = $stylesBuilder.AppendFormat(' table.{0} tr:nth-child(even) td {{{1}{2} }}', $tableStyleId, $htmlAlternateRowStyle, $htmlTableStyle).AppendLine();
                    ## Generate List table styles
                    [ref] $null = $stylesBuilder.AppendFormat(' table.{0}-list {{{1} }}', $tableStyleId, $htmlTableStyle).AppendLine();
                    [ref] $null = $stylesBuilder.AppendFormat(' table.{0}-list td:nth-child(1) {{{1}{2} }}', $tableStyleId, $htmlHeaderStyle, $htmlTableStyle).AppendLine();
                    [ref] $null = $stylesBuilder.AppendFormat(' table.{0}-list td:nth-child(2) {{{1}{2} }}', $tableStyleId, $htmlRowStyle, $htmlTableStyle).AppendLine();
                } #end foreach style
                [ref] $null = $stylesBuilder.AppendLine('</style>');
                return $stylesBuilder.ToString().TrimEnd();
            } #end process
        } #end function OutHtmlStyle

        function OutHtmlSection {
            <#
            .SYNOPSIS
                Output formatted Html section.
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param (
                ## Section to output
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $Section
            )
            process {
                [System.Text.StringBuilder] $sectionBuilder = New-Object System.Text.StringBuilder;
                if ($Document.Options['EnableSectionNumbering']) { [string] $sectionName = '{0} {1}' -f $Section.Number, $Section.Name; }
                else { [string] $sectionName = '{0}' -f $Section.Name; }
                [int] $headerLevel = $Section.Number.Split('.').Count;
                ## Html <h5> is the maximum supported level
                if ($headerLevel -ge 5) {
                    WriteLog -Message $localized.MaxHeadingLevelWarning -IsWarning;
                    $headerLevel = 5;
                }
                if ([string]::IsNullOrEmpty($Section.Style)) { $className = $Document.DefaultStyle; }
                else { $className = $Section.Style; }
                [ref] $null = $sectionBuilder.AppendFormat('<a name="{0}"><h{1} class="{2}">{3}</h{1}></a>', $Section.Id, $headerLevel, $className, $sectionName.TrimStart());
                foreach ($s in $Section.Sections.GetEnumerator()) {
                    if ($s.Id.Length -gt 40) { $sectionId = '{0}[..]' -f $s.Id.Substring(0,36); }
                    else { $sectionId = $s.Id; }
                    WriteLog -Message ($localized.PluginProcessingSection -f $s.Type, $sectionId) -Indent ($s.Level +1);
                    switch ($s.Type) {
                        'PScribo.Section' { [ref] $null = $sectionBuilder.Append((OutHtmlSection -Section $s)); }
                        'PScribo.Paragraph' { [ref] $null = $sectionBuilder.Append((OutHtmlParagraph -Paragraph $s)); }
                        'PScribo.LineBreak' { [ref] $null = $sectionBuilder.Append((OutHtmlLineBreak)); }
                        'PScribo.PageBreak' { [ref] $null = $sectionBuilder.Append((OutHtmlPageBreak)); }
                        'PScribo.Table' { [ref] $null = $sectionBuilder.Append((OutHtmlTable -Table $s)); }
                        'PScribo.BlankLine' { [ref] $null = $sectionBuilder.Append((OutHtmlBlankLine -BlankLine $s)); }
                        Default { WriteLog -Message ($localized.PluginUnsupportedSection -f $s.Type) -IsWarning; }
                    } #end switch
                } #end foreach
                Write-Output ($sectionBuilder.ToString()) -NoEnumerate;
            } #end process
        } # end function OutHtmlSection

        function GetHtmlParagraphStyle {
            <#
            .SYNOPSIS
                Generates html style attribute from PScribo paragraph style overrides.
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [System.Object] $Paragraph
            )
            process {
                $paragraphStyleBuilder = New-Object -TypeName System.Text.StringBuilder;
                if ($Paragraph.Tabs -gt 0) {
                    ## Default to 1/2in tab spacing
                    $tabEm = ConvertMmToEm -Millimeter (12.7 * $Paragraph.Tabs);
                    [ref] $null = $paragraphStyleBuilder.AppendFormat(' margin-left: {0}em;', $tabEm);
                }
                if ($Paragraph.Font) { [ref] $null = $paragraphStyleBuilder.AppendFormat(" font-family: '{0}';", $Paragraph.Font -Join "','"); }
                if ($Paragraph.Size -gt 0) { [ref] $null = $paragraphStyleBuilder.AppendFormat(' font-size: {0:0.00}em;', $Paragraph.Size / 12); } 
                if ($Paragraph.Bold -eq $true) { [ref] $null = $paragraphStyleBuilder.Append(' font-weight: bold;'); }
                if ($Paragraph.Italic -eq $true) { [ref] $null = $paragraphStyleBuilder.Append(' font-style: italic;'); }
                if ($Paragraph.Underline -eq $true) { [ref] $null = $paragraphStyleBuilder.Append(' text-decoration: underline;'); }
                if (-not [System.String]::IsNullOrEmpty($Paragraph.Color) -and $Paragraph.Color.StartsWith('#')) {
                    [ref] $null = $paragraphStyleBuilder.AppendFormat(' color: {0};', $Paragraph.Color.ToLower());
                }
                elseif (-not [System.String]::IsNullOrEmpty($Paragraph.Color)) {
                    [ref] $null = $paragraphStyleBuilder.AppendFormat(' color: #{0};', $Paragraph.Color.ToLower());
                }
                return $paragraphStyleBuilder.ToString().TrimStart();
            } #end process
        } #end function GetHtmlParagraphStyle

        function OutHtmlParagraph {
            <#
            .SYNOPSIS
                Output formatted Html paragraph.
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [System.Object] $Paragraph
            )
            process {
                [System.Text.StringBuilder] $paragraphBuilder = New-Object -TypeName 'System.Text.StringBuilder';
                $text = $Paragraph.Text;
                if ([System.String]::IsNullOrEmpty($text)) {
                    $text = $Paragraph.Id;
                }
                $customStyle = GetHtmlParagraphStyle -Paragraph $Paragraph;
                if ([System.String]::IsNullOrEmpty($Paragraph.Style) -and [System.String]::IsNullOrEmpty($customStyle)) {
                    [ref] $null = $paragraphBuilder.AppendFormat('<div>{0}</div>', $text);
                }
                elseif ([System.String]::IsNullOrEmpty($customStyle)) {
                    [ref] $null = $paragraphBuilder.AppendFormat('<div class="{0}">{1}</div>', $Paragraph.Style, $text);
                }
                else {
                    [ref] $null = $paragraphBuilder.AppendFormat('<div style="{1}">{2}</div>', $Paragraph.Style, $customStyle, $text);
                }
                return $paragraphBuilder.ToString();
            } #end process
        } #end OutHtmlParagraph

        function GetHtmlTableList {
            <#
            .SYNOPSIS
                Generates list html <table> from a PScribo.Table row object.
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [System.Object] $Table,
                [Parameter(Mandatory)] [System.Object] $Row
            )
            process {
                $listTableBuilder = New-Object -TypeName System.Text.StringBuilder;
                [ref] $null = $listTableBuilder.Append((GetHtmlTableDiv -Table $Table));
                [ref] $null = $listTableBuilder.Append((GetHtmlTableColGroup -Table $Table));
                [ref] $null = $listTableBuilder.Append('<tbody>');
                        
                for ($i = 0; $i -lt $Table.Columns.Count; $i++) {
                    $propertyName = $Table.Columns[$i];
                    $propertyDisplayName = $propertyName;
                    if ($Table.Headers) {
                        $propertyDisplayName = $Table.Headers[$i];
                    }
                    [ref] $null = $listTableBuilder.AppendFormat('<tr><td>{0}</td>', $propertyDisplayName);
                    $propertyStyle = '{0}__Style' -f $propertyName;

                    if ($Row.$propertyStyle) {
                        $propertyStyleHtml = (GetHtmlStyle -Style $Document.Styles[$Row.$propertyStyle]);
                        if ([string]::IsNullOrEmpty($Row.$propertyName)) {
                            [ref] $null = $listTableBuilder.AppendFormat('<td style="{0}">&nbsp;</td></tr>', $propertyStyleHtml);
                        }
                        else {
                            [ref] $null = $listTableBuilder.AppendFormat('<td style="{0}">{1}</td></tr>', $propertyStyleHtml, $Row.($propertyName));
                        }
                    }
                    else {
                        if ([string]::IsNullOrEmpty($Row.$propertyName)) {
                            [ref] $null = $listTableBuilder.Append('<td>&nbsp;</td></tr>');
                        }
                        else {
                            [ref] $null = $listTableBuilder.AppendFormat('<td>{0}</td></tr>', $Row.$propertyName);
                        }
                    }
                } #end for each property
                [ref] $null = $listTableBuilder.AppendLine('</tbody></table></div>');
                return $listTableBuilder.ToString();
            } #end process
        } #end function GetHtmlTableList

        function GetHtmlTable {
            <#
            .SYNOPSIS
                Generates html <table> from a PScribo.Table object.
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [System.Object] $Table
            )
            process {
                $standardTableBuilder = New-Object -TypeName System.Text.StringBuilder;
                [ref] $null = $standardTableBuilder.Append((GetHtmlTableDiv -Table $Table));
                [ref] $null = $standardTableBuilder.Append((GetHtmlTableColGroup -Table $Table));

                ## Table headers
                [ref] $null = $standardTableBuilder.Append('<thead><tr>');
                for ($i = 0; $i -lt $Table.Columns.Count; $i++) {
                    if ($null -eq $Table.Headers) {
                        [ref] $null = $standardTableBuilder.AppendFormat('<th>{0}</th>', $Table.Columns[$i]);
                    }
                    else {
                        [ref] $null = $standardTableBuilder.AppendFormat('<th>{0}</th>', $Table.Headers[$i]);
                    }
                }
                [ref] $null = $standardTableBuilder.Append('</tr></thead>');

                ## Table body
                [ref] $null = $standardTableBuilder.AppendLine('<tbody>');
                foreach ($row in $Table.Rows) {
                    [ref] $null = $standardTableBuilder.Append('<tr>');
                    foreach ($propertyName in $Table.Columns) {
                        $propertyStyle = '{0}__Style' -f $propertyName;
                        if ($row.$propertyStyle) {
                            ## Cell styles override row styles
                            $propertyStyleHtml = (GetHtmlStyle -Style $Document.Styles[$row.$propertyStyle]).Trim();
                            [ref] $null = $standardTableBuilder.AppendFormat('<td style="{0}">{1}</td>', $propertyStyleHtml, $row.$propertyName);
                        }
                        elseif (-not [System.String]::IsNullOrEmpty($row.__Style)) {
                            ## We have a row style
                            $rowStyleHtml = (GetHtmlStyle -Style $Document.Styles[$row.__Style]).Trim();
                            [ref] $null = $standardTableBuilder.AppendFormat('<td style="{0}">{1}</td>', $rowStyleHtml, $row.$propertyName);
                        }
                        else {
                            if ($null -ne $row.$propertyName) {
                                ## Check that the property has a value
                                [ref] $null = $standardTableBuilder.AppendFormat('<td>{0}</td>', $row.$propertyName);
                            }
                            else {
                                [ref] $null = $standardTableBuilder.Append('<td>&nbsp</td>');
                            }
                        } #end if $row.PropertyStyle
                    } #end foreach property
                    [ref] $null = $standardTableBuilder.AppendLine('</tr>');
                } #end foreach row
                [ref] $null = $standardTableBuilder.AppendLine('</tbody></table></div>');
                return $standardTableBuilder.ToString();
            } #end process
        } #end function GetHtmlTableList

        function OutHtmlTable {
            <#
            .SYNOPSIS
                Output formatted Html <table> from PScribo.Table object.
            .NOTES
                One table is output per table row with the -List parameter.
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [System.Object] $Table
            )
            process {
                [System.Text.StringBuilder] $tableBuilder = New-Object -TypeName 'System.Text.StringBuilder';
                if ($Table.List) {
                    ## Create a table for each row
                    for ($r = 0; $r -lt $Table.Rows.Count; $r++) {
                        $row = $Table.Rows[$r];
                        if ($r -gt 0) {
                            ## Add a space between each table to mirror Word output rendering
                            [ref] $null = $tableBuilder.AppendLine('<p />');
                        }
                        [ref] $null = $tableBuilder.Append((GetHtmlTableList -Table $Table -Row $row));
                        
                    } #end foreach row
                }
                else {
                    [ref] $null = $tableBuilder.Append((GetHtmlTable -Table $Table));
                } #end if
                return $tableBuilder.ToString();
                #Write-Output ($tableBuilder.ToString()) -NoEnumerate;
            } #end process
        } #end function outhtmltable

        function OutHtmlLineBreak {
            <#
            .SYNOPSIS
                Output formatted Html line break.
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param ( )
            process {
                return '<hr />';
            }
        } #end function OutHtmlLineBreak

        function OutHtmlPageBreak {
            <#
            .SYNOPSIS
                Output formatted Html page break.
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param ( )
            process {
                [System.Text.StringBuilder] $pageBreakBuilder = New-Object 'System.Text.StringBuilder';
                [ref] $null = $pageBreakBuilder.Append('</div></page>');
                $topMargin = ConvertMmToEm $Document.Options['MarginTop'];
                $leftMargin = ConvertMmToEm $Document.Options['MarginLeft'];
                $bottomMargin = ConvertMmToEm $Document.Options['MarginBottom'];
                $rightMargin = ConvertMmToEm $Document.Options['MarginRight'];
                [ref] $null = $pageBreakBuilder.AppendFormat('<page><div class="{0}" style="padding-top: {1}em; padding-left: {2}em; padding-bottom: {3}em; padding-right: {4}em;">', $Document.DefaultStyle, $topMargin, $leftMargin, $bottomMargin, $rightMargin).AppendLine();
                return $pageBreakBuilder.ToString();
            }
        } #end function OutHtmlPageBreak
        #endregion OutHtml Private Functions
    } #end begin
    process {
        $stopwatch = [System.Diagnostics.Stopwatch]::StartNew();
        WriteLog -Message ($localized.DocumentProcessingStarted -f $Document.Name);
        [System.Text.StringBuilder] $htmlBuilder = New-Object System.Text.StringBuilder;
        [ref] $null = $htmlBuilder.AppendLine('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">');
        [ref] $null = $htmlBuilder.AppendLine('<html xmlns="http://www.w3.org/1999/xhtml">');
        [ref] $null = $htmlBuilder.AppendLine('<head><title>{0}</title>' -f $Document.Name);
        [ref] $null = $htmlBuilder.AppendLine('{0}</head><body><page>' -f (OutHtmlStyle -Styles $Document.Styles -TableStyles $Document.TableStyles));
        $topMargin = ConvertMmToEm $Document.Options['MarginTop'];
        $leftMargin = (ConvertMmToEm $Document.Options['MarginLeft']);
        $bottomMargin = (ConvertMmToEm $Document.Options['MarginBottom']);
        $rightMargin = ConvertMmToEm $Document.Options['MarginRight'];
        [ref] $null = $htmlBuilder.AppendFormat('<div class="{0}" style="padding-top: {1}em; padding-left: {2}em; padding-bottom: {3}em; padding-right: {4}em;">', $Document.DefaultStyle, $topMargin, $leftMargin, $bottomMargin, $rightMargin).AppendLine();
        foreach ($s in $Document.Sections.GetEnumerator()) {
            if ($s.Id.Length -gt 40) { $sectionId = '{0}[..]' -f $s.Id.Substring(0,36); }
            else { $sectionId = $s.Id; }
            WriteLog -Message ($localized.PluginProcessingSection -f $s.Type, $sectionId) -Indent ($s.Level +1);
            switch ($s.Type) {
                'PScribo.Section' { [ref] $null = $htmlBuilder.Append((OutHtmlSection -Section $s)); }
                'PScribo.Paragraph' { [ref] $null = $htmlBuilder.Append((OutHtmlParagraph -Paragraph $s)); }
                'PScribo.Table' { [ref] $null = $htmlBuilder.Append((OutHtmlTable -Table $s)); }
                'PScribo.LineBreak' { [ref] $null = $htmlBuilder.Append((OutHtmlLineBreak)); }
                'PScribo.PageBreak' { [ref] $null = $htmlBuilder.Append((OutHtmlPageBreak)); } ## Page breaks are implemented as line breaks with extra padding
                'PScribo.TOC' { [ref] $null = $htmlBuilder.Append((OutHtmlTOC -TOC $s)); }
                'PScribo.BlankLine' { [ref] $null = $htmlBuilder.Append((OutHtmlBlankLine -BlankLine $s)); }
                Default { WriteLog -Message ($localized.PluginUnsupportedSection -f $s.Type) -IsWarning; }
            } #end switch
        } #end foreach section
        $stopwatch.Stop();
        WriteLog -Message ($localized.DocumentProcessingCompleted -f $Document.Name);
        $destinationPath = Join-Path $Path ('{0}.html' -f $Document.Name);
        WriteLog -Message ($localized.SavingFile -f $destinationPath);
        $htmlBuilder.ToString().TrimEnd() | Out-File -FilePath $destinationPath -Force -Encoding utf8;
        [ref] $null = $htmlBuilder;
        WriteLog -Message ($localized.TotalProcessingTime -f $stopwatch.Elapsed.TotalSeconds);
        Write-Output (Get-Item -Path $destinationPath);
    } #end process
} #end function OutHtml

function OutText {
    <#
    .SYNOPSIS
        Text output plugin for PScribo.
    .DESCRIPTION
        Outputs a text file representation of a PScribo document object.
    #>

    [CmdletBinding()]
    param (
        ## ThePScribo document object to convert to a text document
        [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $Document,
        ## Output directory path for the .txt file
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.String] $Path,
        ### Hashtable of all plugin supported options
        [Parameter()] [AllowNull()] [System.Collections.Hashtable] $Options
    )
    begin {
        $pluginName = 'Text';
        #region OutText Private Functions
        function New-PScriboTextOptions {
            <#
            .SYNOPSIS
                Sets the text plugin specific formatting/output options.
            .NOTES
                All plugin options should be prefixed with the plugin name.
            #>

            [CmdletBinding()]
            param (
                ## Text/output width. 0 = none/no wrap.
                [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.Int32] $TextWidth = 120,
                ## Document header separator character.
                [Parameter(ValueFromPipelineByPropertyName)] [ValidateLength(1,1)] [System.String] $HeaderSeparator = '=',
                ## Document section separator character.
                [Parameter(ValueFromPipelineByPropertyName)] [ValidateLength(1,1)] [System.String] $SectionSeparator = '-',
                ## Document section separator character.
                [Parameter(ValueFromPipelineByPropertyName)] [ValidateLength(1,1)] [System.String] $LineBreakSeparator = '_',
                ## Default header/section separator width.
                [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.Int32] $SeparatorWidth = $TextWidth,
                ## Text encoding
                [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('ASCII','Unicode','UTF7','UTF8')] [System.String] $Encoding = 'ASCII'
            )
            process {
                ## Flag that the Text options have been set. There should be a flag that is checked
                ## by the plugin if the user has set plugin-specific options.
                $options = @{
                    TextWidth = $TextWidth;
                    HeaderSeparator = $HeaderSeparator;
                    SectionSeparator = $SectionSeparator;
                    LineBreakSeparator = $LineBreakSeparator;
                    SeparatorWidth = $SeparatorWidth;
                    Encoding = $Encoding;
                }
                return $options;
            } #end process
        } #end function New-PScriboTextOptions

        function OutTextTOC {
            <#
            .SYNOPSIS
                Output formatted Table of Contents
            #>

            [CmdletBinding()]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $TOC
            )
            process {
                $tocBuilder = New-Object -TypeName System.Text.StringBuilder;
                [ref] $null = $tocBuilder.AppendLine($TOC.Name);
                [ref] $null = $tocBuilder.AppendLine(''.PadRight($Options.SeparatorWidth, $Options.SectionSeparator));
                $maxSectionNumberLength = ($Document.TOC.Number | Measure-Object -Maximum | Select-Object -ExpandProperty Maximum).Length;
                foreach ($tocEntry in $Document.TOC) {
                    $sectionNumberPaddingLength = $maxSectionNumberLength - $tocEntry.Number.Length;
                    $sectionNumberIndent = ''.PadRight($tocEntry.Level, ' ');
                    $sectionPadding = ''.PadRight($sectionNumberPaddingLength, ' ');
                    [ref] $null = $tocBuilder.AppendFormat('{0}{1} {2}{3}', $tocEntry.Number, $sectionPadding, $sectionNumberIndent, $tocEntry.Name).AppendLine();
                } #end foreach TOC entry
                return $tocBuilder.ToString();      
            } #end process
        } #end function OutTextTOC

        function OutTextBlankLine {
            <#
            .SYNOPSIS
                Output formatted text blankline.
            #>

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

        function OutTextSection {
            <#
            .SYNOPSIS
                Output formatted text section.
            #>

            [CmdletBinding()]
            param (
                ## Section to output
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $Section
            )
            process {
                $sectionBuilder = New-Object -TypeName System.Text.StringBuilder;
                if ($Document.Options['EnableSectionNumbering']) { [string] $sectionName = '{0} {1}' -f $Section.Number, $Section.Name; }
                else { [string] $sectionName = '{0}' -f $Section.Name; }
                [ref] $null = $sectionBuilder.AppendLine();
                [ref] $null = $sectionBuilder.AppendLine($sectionName.TrimStart());
                [ref] $null = $sectionBuilder.AppendLine(''.PadRight($Options.SeparatorWidth, $Options.SectionSeparator));
                foreach ($s in $Section.Sections.GetEnumerator()) {
                    if ($s.Id.Length -gt 40) { $sectionId = '{0}..' -f $s.Id.Substring(0,38); }
                    else { $sectionId = $s.Id; }
                    WriteLog -Message ($localized.PluginProcessingSection -f $s.Type, $sectionId) -Indent ($s.Level +1);
                    switch ($s.Type) {
                        'PScribo.Section' { [ref] $null = $sectionBuilder.Append((OutTextSection -Section $s)); }
                        'PScribo.Paragraph' { [ref] $null = $sectionBuilder.Append(($s | OutTextParagraph)); }
                        'PScribo.PageBreak' { [ref] $null = $sectionBuilder.AppendLine((OutTextPageBreak)); }  ## Page breaks implemented as line break with extra padding
                        'PScribo.LineBreak' { [ref] $null = $sectionBuilder.AppendLine((OutTextLineBreak)); }
                        'PScribo.Table' { [ref] $null = $sectionBuilder.AppendLine(($s | OutTextTable)); }
                        'PScribo.BlankLine' { [ref] $null = $sectionBuilder.AppendLine(($s | OutTextBlankLine)); }
                        Default { WriteLog -Message ($localized.PluginUnsupportedSection -f $pluginName, $s.Type) -IsWarning; }
                    } #end switch
                } #end foreach
                return $sectionBuilder.ToString();
            } #end process
        } #end function outtextsection

        function OutTextParagraph {
            <#
            .SYNOPSIS
                Output formatted paragraph text.
            #>

            [CmdletBinding()]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [System.Object] $Paragraph
            )
            process {
                $padding = ''.PadRight(($Paragraph.Tabs * 4), ' ');
                if ([string]::IsNullOrEmpty($Object.Text)) { $text = "$padding$($Paragraph.Id)"; }
                else { $text = "$padding$($Paragraph.Text)"; }
                $formattedText = OutStringWrap -InputObject $text -Width $Options.TextWidth;
                if ($Paragraph.NewLine) { return "$formattedText`r`n"; }
                else { return $formattedText; }
            } #end process
        } #end outtextparagraph

        function OutTextLineBreak {
            <#
            .SYNOPSIS
                Output formatted line break text.
            #>

            [CmdletBinding()]
            param ( )
            process {
                ## Use the specified output width
                if ($Options.TextWidth -eq 0) { $Options.TextWidth = $Host.UI.RawUI.BufferSize.Width -1; }
                $lb = ''.PadRight($Options.SeparatorWidth, $Options.LineBreakSeparator);
                return "$(OutStringWrap -InputObject $lb -Width $Options.TextWidth)`r`n";
            } #end process
        } #end function OutTextLineBreak

        function OutTextPageBreak {
            <#
            .SYNOPSIS
                Output formatted line break text.
            #>

            [CmdletBinding()]
            param ( )
            process {
                return "$(OutTextLineBreak)`r`n";
            } #end process
        } #end function OutTextLineBreak

        function OutTextTable {
            <#
            .SYNOPSIS
                Output formatted text table.
            #>

            [CmdletBinding()]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $Table
            )
            process {
                ## Use the specified output width
                if ($Options.TextWidth -eq 0) { $Options.TextWidth = $Host.UI.RawUI.BufferSize.Width -1; }
                if ($Table.List) {
                    $text = ($Table.Rows | Select-Object -Property * -ExcludeProperty '*__Style' | Format-List | Out-String -Width $Options.TextWidth).Trim();
                } else {
                    ## Don't trim tabs for table headers
                    ## Tables set to AutoSize as otherwise, rendering is different between PoSh v4 and v5
                    $text = ($Table.Rows | Select-Object -Property * -ExcludeProperty '*__Style' | Format-Table -Wrap -AutoSize | Out-String -Width $Options.TextWidth).Trim("`r`n");
                }
                # Ensure there's a space before and after the table.
                return "`r`n$text`r`n";
            } #end process
        } #end function outtexttable

        function OutStringWrap {
            <#
            .SYNOPSIS
                Outputs objects to strings, wrapping as required.
            #>

            [CmdletBinding()]
            [OutputType([System.String])]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [Object[]] $InputObject,
                [Parameter()] [ValidateNotNull()] [System.Int32] $Width = $Host.UI.RawUI.BufferSize.Width
            )
            begin {
                ## 2 is the minimum, therefore default to wiiiiiiiiiide!
                if ($Width -lt 2) { $Width = 4096; }
                WriteLog -Message ('Wrapping text at "{0}" characters.' -f $Width) -IsDebug;
            }
            process {
                foreach ($object in $InputObject) {
                    $textBuilder = New-Object -TypeName System.Text.StringBuilder;
                    $text = (Out-String -InputObject $object).TrimEnd("`r`n");
                    for ($i = 0; $i -le $text.Length; $i += $Width) {
                        if (($i + $Width) -ge ($text.Length -1)) { [ref] $null = $textBuilder.Append($text.Substring($i)); }
                        else { [ref] $null = $textBuilder.AppendLine($text.Substring($i, $Width)); }
                    } #end for
                    return $textBuilder.ToString();
                    $textBuilder = $null;
                } #end foreach
            } #end process
        } #end function OutStringWrap
        #endregion OutText Private Functions
    }
    process {
        $stopwatch = [Diagnostics.Stopwatch]::StartNew();
        WriteLog -Message ($localized.DocumentProcessingStarted -f $Document.Name);
        ## Create default options if not specified
        if ($null -eq $Options) { $Options = New-PScriboTextOptions; }
        [System.Text.StringBuilder] $textBuilder = New-Object System.Text.StringBuilder;
        foreach ($s in $Document.Sections.GetEnumerator()) {
            if ($s.Id.Length -gt 40) { $sectionId = '{0}[..]' -f $s.Id.Substring(0,36); }
            else { $sectionId = $s.Id; }
            WriteLog -Message ($localized.PluginProcessingSection -f $s.Type, $sectionId) -Indent ($s.Level +1);
            switch ($s.Type) {
                'PScribo.Section' { [ref] $null = $textBuilder.Append((OutTextSection -Section $s)); }
                'PScribo.Paragraph' { [ref] $null = $textBuilder.Append(($s | OutTextParagraph)); }
                'PScribo.PageBreak' { [ref] $null = $textBuilder.AppendLine((OutTextPageBreak)); }
                'PScribo.LineBreak' { [ref] $null = $textBuilder.AppendLine((OutTextLineBreak)); }
                'PScribo.Table' { [ref] $null = $textBuilder.AppendLine(($s | OutTextTable)); }
                'PScribo.TOC' { [ref] $null = $textBuilder.AppendLine(($s | OutTextTOC)); }
                'PScribo.BlankLine' { [ref] $null = $textBuilder.AppendLine(($s | OutTextBlankLine)); }
                Default { WriteLog -Message ($localized.PluginUnsupportedSection -f $s.Type) -IsWarning; }
            } #end switch
        } #end foreach
        $stopwatch.Stop();       
        WriteLog -Message ($localized.DocumentProcessingCompleted -f $Document.Name);
        $destinationPath = Join-Path -Path $Path ('{0}.txt' -f $Document.Name);
        WriteLog -Message ($localized.SavingFile -f $destinationPath);
        Set-Content -Value ($textBuilder.ToString()) -Path $destinationPath -Encoding $Options.Encoding;
        [ref] $null = $textBuilder;
        WriteLog -Message ($localized.TotalProcessingTime -f $stopwatch.Elapsed.TotalSeconds);
        ## Return the file reference to the pipeline
        Write-Output (Get-Item -Path $destinationPath);
    } #end process
} #end function OutText

function OutWord {
    <#
    .SYNOPSIS
        Microsoft Word output plugin for PScribo.
    .DESCRIPTION
        Outputs a Word document representation of a PScribo document object.
    #>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param (
        ## ThePScribo document object to convert to a text document
        [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $Document,
        ## Output directory path for the .txt file
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.String] $Path,
        ### Hashtable of all plugin supported options
        [Parameter()] [AllowNull()] [System.Collections.Hashtable] $Options
    )
    begin {
        $pluginName = 'Word';
        #region OutWord Private Functions

        function ConvertToWordColor {
            <#
            .SYNOPSIS
                Converts an HTML color to RRGGBB value as Word does not support short Html color codes
            #>

            [CmdletBinding()]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [System.String] $Color
            )
            process {
                $Color = $Color.TrimStart('#');
                if ($Color.Length -eq 3) {
                    $Color = '{0}{0}{1}{1}{2}{2}' -f $Color[0], $Color[1],$Color[2];
                }
                return $Color.ToUpper();
            }
        } #end function ConvertToWordColor

        function OutWordSection {
            <#
            .SYNOPSIS
                Output formatted Word section (paragraph).
            #>

            [CmdletBinding()]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $Section,
                [Parameter(Mandatory)] [System.Xml.XmlElement] $RootElement,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument
            )
            process {
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';

                $p = $RootElement.AppendChild($XmlDocument.CreateElement('w', 'p', $xmlnsMain));
                $pPr = $p.AppendChild($XmlDocument.CreateElement('w', 'pPr', $xmlnsMain));
                if (-not [System.String]::IsNullOrEmpty($Section.Style)) {
                    #if (-not $Section.IsExcluded) {
                        ## If it's excluded we need a non-Heading style :( Could explicitly set the style on the run?
                        $pStyle = $pPr.AppendChild($XmlDocument.CreateElement('w', 'pStyle', $xmlnsMain));
                        [ref] $null = $pStyle.SetAttribute('val', $xmlnsMain, $Section.Style);
                    #}
                }
                $spacing = $pPr.AppendChild($XmlDocument.CreateElement('w', 'spacing', $xmlnsMain));
                ## Increment heading spacing by 2pt for each section level, starting at 8pt for level 0, 10pt for level 1 etc
                $spacingPt = (($Section.Level * 2) + 8) * 20;
                [ref] $null = $spacing.SetAttribute('before', $xmlnsMain, $spacingPt);
                [ref] $null = $spacing.SetAttribute('after', $xmlnsMain, $spacingPt);
                $r = $p.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlnsMain));
                $t = $r.AppendChild($XmlDocument.CreateElement('w', 't', $xmlnsMain));
                if ($Document.Options['EnableSectionNumbering']) { [string] $sectionName = '{0} {1}' -f $Section.Number, $Section.Name; }
                else { [string] $sectionName = '{0}' -f $Section.Name; }
                [ref] $null = $t.AppendChild($XmlDocument.CreateTextNode($sectionName));

                foreach ($s in $Section.Sections.GetEnumerator()) {
                    if ($s.Id.Length -gt 40) { $sectionId = '{0}[..]' -f $s.Id.Substring(0,36); }
                    else { $sectionId = $s.Id; }
                    WriteLog -Message ($localized.PluginProcessingSection -f $s.Type, $sectionId) -Indent ($s.Level +1);
                    switch ($s.Type) {
                        'PScribo.Section' { $s | OutWordSection -RootElement $RootElement -XmlDocument $XmlDocument; }
                        'PScribo.Paragraph' { [ref] $null = $RootElement.AppendChild((OutWordParagraph -Paragraph $s -XmlDocument $XmlDocument)); }
                        'PScribo.PageBreak' { [ref] $null = $RootElement.AppendChild((OutWordPageBreak -PageBreak $s -XmlDocument $xmlDocument)); }
                        'PScribo.LineBreak' { [ref] $null = $RootElement.AppendChild((OutWordLineBreak -LineBreak $s -XmlDocument $xmlDocument)); }
                        'PScribo.Table' { OutWordTable -Table $s -XmlDocument $xmlDocument -Element $RootElement; }
                        'PScribo.BlankLine' { OutWordBlankLine -BlankLine $s -XmlDocument $xmlDocument -Element $RootElement; }
                        Default { WriteLog -Message ($localized.PluginUnsupportedSection -f $s.Type) -IsWarning; }
                    } #end switch
                } #end foreach
        
            } #end process
        } #end function OutWordSection

        function OutWordParagraph {
            <#
                .SYNOPSIS
                    Output formatted Word paragraph.
            #>

            [CmdletBinding()]
            [OutputType([System.Xml.XmlElement])]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $Paragraph,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument
            )
            process {
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';

                $p = $XmlDocument.CreateElement('w', 'p', $xmlnsMain);
                $pPr = $p.AppendChild($XmlDocument.CreateElement('w', 'pPr', $xmlnsMain));
                if ($Paragraph.Tabs -gt 0) {
                    $ind = $pPr.AppendChild($XmlDocument.CreateElement('w', 'ind', $xmlnsMain));
                    [ref] $null = $ind.SetAttribute('left', $xmlnsMain, (720 * $Paragraph.Tabs));
                }
                if (-not [System.String]::IsNullOrEmpty($Paragraph.Style)) {
                    $pStyle = $pPr.AppendChild($XmlDocument.CreateElement('w', 'pStyle', $xmlnsMain));
                    [ref] $null = $pStyle.SetAttribute('val', $xmlnsMain, $Paragraph.Style);
                }
                $spacing = $pPr.AppendChild($XmlDocument.CreateElement('w', 'spacing', $xmlnsMain));
                [ref] $null = $spacing.SetAttribute('before', $xmlnsMain, 0);
                [ref] $null = $spacing.SetAttribute('after', $xmlnsMain, 0);
        
                $r = $p.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlnsMain));
                $rPr = $r.AppendChild($XmlDocument.CreateElement('w', 'rPr', $xmlnsMain));
                ## Apply custom paragraph styles to the run..
                if ($Paragraph.Font) {
                    $rFonts = $rPr.AppendChild($XmlDocument.CreateElement('w', 'rFonts', $xmlnsMain));
                    [ref] $null = $rFonts.SetAttribute('ascii', $xmlnsMain, $Paragraph.Font[0]);
                    [ref] $null = $rFonts.SetAttribute('hAnsi', $xmlnsMain, $Paragraph.Font[0]);
                }
                if ($Paragraph.Size -gt 0) {
                    $sz = $rPr.AppendChild($XmlDocument.CreateElement('w', 'sz', $xmlnsMain));
                    [ref] $null = $sz.SetAttribute('val', $xmlnsMain, $Paragraph.Size * 2);
                }
                if ($Paragraph.Bold -eq $true) {
                    [ref] $null = $rPr.AppendChild($XmlDocument.CreateElement('w', 'b', $xmlnsMain));
                }
                if ($Paragraph.Italic -eq $true) {
                    [ref] $null = $rPr.AppendChild($XmlDocument.CreateElement('w', 'i', $xmlnsMain));
                }
                if ($Paragraph.Underline -eq $true) {
                    $u = $rPr.AppendChild($XmlDocument.CreateElement('w', 'u', $xmlnsMain));
                    [ref] $null = $u.SetAttribute('val', $xmlnsMain, 'single');
                }
                if (-not [System.String]::IsNullOrEmpty($Paragraph.Color)) {
                    $color = $rPr.AppendChild($XmlDocument.CreateElement('w', 'color', $xmlnsMain));
                    [ref] $null = $color.SetAttribute('val', $xmlnsMain, (ConvertToWordColor -Color $Paragraph.Color));
                }
        
                $t = $r.AppendChild($XmlDocument.CreateElement('w', 't', $xmlnsMain));
                [ref] $null = $t.SetAttribute('space', 'http://www.w3.org/XML/1998/namespace', 'preserve'); ## needs to be xml:space="preserve" NOT w:space...
                if ([System.String]::IsNullOrEmpty($Paragraph.Text)) {
                    [ref] $null = $t.AppendChild($XmlDocument.CreateTextNode($Paragraph.Id));
                }
                else {
                    [ref] $null = $t.AppendChild($XmlDocument.CreateTextNode($Paragraph.Text));
                }
                return $p;
            } #end process
        } #end function OutWordParagraph

        function OutWordPageBreak {
            <#
            .SYNOPSIS
                Output formatted Word page break.
            #>

            [CmdletBinding()]
            [OutputType([System.Xml.XmlElement])]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $PageBreak,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument
            )
            process {
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
                $p = $XmlDocument.CreateElement('w', 'p', $xmlnsMain);
                $r = $p.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlnsMain));
                $br = $r.AppendChild($XmlDocument.CreateElement('w', 'br', $xmlnsMain));
                [ref] $null = $br.SetAttribute('type', $xmlnsMain, 'page');
                return $p;
            }
        } #end function OutWordPageBreak

        function OutWordLineBreak {
            <#
            .SYNOPSIS
                Output formatted Word line break.
            #>

            [CmdletBinding()]
            [OutputType([System.Xml.XmlElement])]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $LineBreak,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument
            )
            process {
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
                $p = $XmlDocument.CreateElement('w', 'p', $xmlnsMain);
                $pPr = $p.AppendChild($XmlDocument.CreateElement('w', 'pPr', $xmlnsMain));
                $pBdr = $pPr.AppendChild($XmlDocument.CreateElement('w', 'pBdr', $xmlnsMain));
                $bottom = $pBdr.AppendChild($XmlDocument.CreateElement('w', 'bottom', $xmlnsMain));
                [ref] $null = $bottom.SetAttribute('val', $xmlnsMain, 'single');
                [ref] $null = $bottom.SetAttribute('sz', $xmlnsMain, 6);
                [ref] $null = $bottom.SetAttribute('space', $xmlnsMain, 1);
                [ref] $null = $bottom.SetAttribute('color', $xmlnsMain, 'auto');
                return $p;
            }
        } #end function OutWordLineBreak

        function GetWordTable {
            <#
            .SYNOPSIS
                Creates a scaffold Word <w:tbl> element
            #>

            [CmdletBinding()]
            [OutputType([System.Xml.XmlElement])]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [System.Object] $Table,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument
            )
            process {
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
                $tableStyle = $Document.TableStyles[$Table.Style];
                $tbl = $XmlDocument.CreateElement('w', 'tbl', $xmlnsMain);
                $tblPr = $tbl.AppendChild($XmlDocument.CreateElement('w', 'tblPr', $xmlnsMain));
                if ($Table.Tabs -gt 0) {
                    $tblInd = $tblPr.AppendChild($XmlDocument.CreateElement('w', 'tblInd', $xmlnsMain));
                    [ref] $null = $tblInd.SetAttribute('w', $xmlnsMain, (720 * $Table.Tabs));
                }
                if ($Table.ColumnWidths) {
                    $tblLayout = $tblPr.AppendChild($XmlDocument.CreateElement('w', 'tblLayout', $xmlnsMain));
                    [ref] $null = $tblLayout.SetAttribute('type', $xmlnsMain, 'fixed');
                }
                elseif ($Table.Width -eq 0) {
                    $tblLayout = $tblPr.AppendChild($XmlDocument.CreateElement('w', 'tblLayout', $xmlnsMain));
                    [ref] $null = $tblLayout.SetAttribute('type', $xmlnsMain, 'autofit');
                }
        
                if ($Table.Width -gt 0) {
                    $tblW = $tblPr.AppendChild($XmlDocument.CreateElement('w', 'tblW', $xmlnsMain));
                    [ref] $null = $tblW.SetAttribute('type', $xmlnsMain, 'pct');
                    $tableWidthRenderPct = $Table.Width;

                    if ($Table.Tabs -gt 0) {
                        ## We now need to deal with tables being pushed outside the page margin
                        $pageWidthMm = $Document.Options['PageWidth'] - ($Document.Options['PageMarginLeft'] + $Document.Options['PageMarginRight']);
                        $indentWidthMm = ConvertPtToMm -Point ($Table.Tabs * 36);
                        $tableRenderMm = (($pageWidthMm / 100) * $Table.Width) + $indentWidthMm;
                        if ($tableRenderMm -gt $pageWidthMm) {
                            ## We've over-flowed so need to work out the maximum percentage
                            $maxTableWidthMm = $pageWidthMm - $indentWidthMm;
                            $tableWidthRenderPct = [System.Math]::Round(($maxTableWidthMm / $pageWidthMm) * 100, 2);
                            WriteLog -Message ($localized.TableWidthOverflowWarning -f $tableWidthRenderPct) -IsWarning;
                        }
                    }
                    [ref] $null = $tblW.SetAttribute('w', $xmlnsMain, $tableWidthRenderPct * 50);
                }
        
                $spacing = $tblPr.AppendChild($XmlDocument.CreateElement('w', 'spacing', $xmlnsMain));
                [ref] $null = $spacing.SetAttribute('before', $xmlnsMain, 72);
                [ref] $null = $spacing.SetAttribute('after', $xmlnsMain, 72);
        
                #$tblLook = $tblPr.AppendChild($XmlDocument.CreateElement('w', 'tblLook', $xmlnsMain));
                #[ref] $null = $tblLook.SetAttribute('val', $xmlnsMain, '04A0');
                #[ref] $null = $tblLook.SetAttribute('firstRow', $xmlnsMain, 1);
                ## <w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/>
                #$tblStyle = $tblPr.AppendChild($XmlDocument.CreateElement('w', 'tblStyle', $xmlnsMain));
                #[ref] $null = $tblStyle.SetAttribute('val', $xmlnsMain, $Table.Style);

                if ($tableStyle.BorderWidth -gt 0) {
                    $tblBorders = $tblPr.AppendChild($XmlDocument.CreateElement('w', 'tblBorders', $xmlnsMain));
                    foreach ($border in @('top','bottom','start','end','insideH','insideV')) {
                        $b = $tblBorders.AppendChild($XmlDocument.CreateElement('w', $border, $xmlnsMain));
                        [ref] $null = $b.SetAttribute('sz', $xmlnsMain, (ConvertMmToOctips $tableStyle.BorderWidth));
                        [ref] $null = $b.SetAttribute('val', $xmlnsMain, 'single');
                        [ref] $null = $b.SetAttribute('color', $xmlnsMain, (ConvertToWordColor -Color $tableStyle.BorderColor));
                    }
                }
        
                $tblCellMar = $tblPr.AppendChild($XmlDocument.CreateElement('w', 'tblCellMar', $xmlnsMain));
                $top = $tblCellMar.AppendChild($XmlDocument.CreateElement('w', 'top', $xmlnsMain));
                [ref] $null = $top.SetAttribute('w', $xmlnsMain, (ConvertMmToTwips $tableStyle.PaddingTop));
                [ref] $null = $top.SetAttribute('type', $xmlnsMain, 'dxa');
                $left = $tblCellMar.AppendChild($XmlDocument.CreateElement('w', 'start', $xmlnsMain));
                [ref] $null = $left.SetAttribute('w', $xmlnsMain, (ConvertMmToTwips $tableStyle.PaddingLeft));
                [ref] $null = $left.SetAttribute('type', $xmlnsMain, 'dxa');
                $bottom = $tblCellMar.AppendChild($XmlDocument.CreateElement('w', 'bottom', $xmlnsMain));
                [ref] $null = $bottom.SetAttribute('w', $xmlnsMain, (ConvertMmToTwips $tableStyle.PaddingBottom));
                [ref] $null = $bottom.SetAttribute('type', $xmlnsMain, 'dxa');
                $right = $tblCellMar.AppendChild($XmlDocument.CreateElement('w', 'end', $xmlnsMain));
                [ref] $null = $right.SetAttribute('w', $xmlnsMain, (ConvertMmToTwips $tableStyle.PaddingRight));
                [ref] $null = $right.SetAttribute('type', $xmlnsMain, 'dxa');

                $tblGrid = $tbl.AppendChild($XmlDocument.CreateElement('w', 'tblGrid', $xmlnsMain));
                $columnCount = $Table.Columns.Count;
                if ($Table.List) {
                    $columnCount = 2;
                }
                for ($i = 0; $i -lt $Table.Columns.Count; $i++) {
                    $gridCol = $tblGrid.AppendChild($XmlDocument.CreateElement('w', 'gridCol', $xmlnsMain));
                }

                return $tbl;
            } #end process
        } #end functino GetWordTable

        function OutWordTable {
            <#
            .SYNOPSIS
                Output formatted Word table.
            .NOTES
                Specifies that the current row should be repeated at the top each new page on which the table is displayed. E.g, <w:tblHeader />.
            #>

            [CmdletBinding()]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [System.Object] $Table,
                ## Root element to append the table(s) to. List view will create multiple tables
                [Parameter(Mandatory)] [ValidateNotNull()] [System.Xml.XmlElement] $Element,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument
            )
            process {
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
                $tableStyle = $Document.TableStyles[$Table.Style];
                $headerStyle = $Document.Styles[$tableStyle.HeaderStyle];

                if ($Table.List) {
            
                    for ($r = 0; $r -lt $Table.Rows.Count; $r++) {
                        $row = $Table.Rows[$r];
                        if ($r -gt 0) {
                            ## Add a space between each table as Word renders them together..
                            [ref] $null = $Element.AppendChild($XmlDocument.CreateElement('w', 'p', $xmlnsMain));
                        }

                        ## Create <tr><tc></tc></tr> for each property
                        $tbl = $Element.AppendChild((GetWordTable -Table $Table -XmlDocument $XmlDocument));
  
                        $properties = @($row.PSObject.Properties);
                        for ($i = 0; $i -lt $properties.Count; $i++) {
                            $propertyName = $properties[$i].Name;
                            ## Ignore __Style properties
                            if (-not $propertyName.EndsWith('__Style')) {
                        
                                $tr = $tbl.AppendChild($XmlDocument.CreateElement('w', 'tr', $xmlnsMain));
                                $tc1 = $tr.AppendChild($XmlDocument.CreateElement('w', 'tc', $xmlnsMain));
                                $tcPr1 = $tc1.AppendChild($XmlDocument.CreateElement('w', 'tcPr', $xmlnsMain));
                        
                                if ($null -ne $Table.ColumnWidths) {
                                    ## TODO: Refactor out
                                    $columnWidthTwips = ConvertMmToTwips -Millimeter ($Table.ColumnWidths[0] * $tableWidthOnePct);
                                    $tcW1 = $tcPr1.AppendChild($XmlDocument.CreateElement('w', 'tcW', $xmlnsMain));
                                    [ref] $null = $tcW1.SetAttribute('w', $xmlnsMain, $Table.ColumnWidths[0] * 50);
                                    [ref] $null = $tcW1.SetAttribute('type', $xmlnsMain, 'pct');
                                }
                                if ($headerStyle.BackgroundColor) {
                                    [ref] $null = $tc1.AppendChild((GetWordTableStyleCellPr -Style $headerStyle -XmlDocument $XmlDocument));
                                }
                                $p1 = $tc1.AppendChild($XmlDocument.CreateElement('w', 'p', $xmlnsMain));
                                $pPr1 = $p1.AppendChild($XmlDocument.CreateElement('w', 'pPr', $xmlnsMain));
                                $pStyle1 = $pPr1.AppendChild($XmlDocument.CreateElement('w', 'pStyle', $xmlnsMain));
                                [ref] $null = $pStyle1.SetAttribute('val', $xmlnsMain, $tableStyle.HeaderStyle);
                                $r1 = $p1.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlnsMain));
                                $t1 = $r1.AppendChild($XmlDocument.CreateElement('w', 't', $xmlnsMain));
                                [ref] $null = $t1.AppendChild($XmlDocument.CreateTextNode($propertyName));                 

                                $tc2 = $tr.AppendChild($XmlDocument.CreateElement('w', 'tc', $xmlnsMain));
                                $tcPr2 = $tc2.AppendChild($XmlDocument.CreateElement('w', 'tcPr', $xmlnsMain));
                       
                                if ($null -ne $Table.ColumnWidths) {
                                    ## TODO: Refactor out
                                    $tcW2 = $tcPr2.AppendChild($XmlDocument.CreateElement('w', 'tcW', $xmlnsMain));
                                    [ref] $null = $tcW2.SetAttribute('w', $xmlnsMain, $Table.ColumnWidths[1] * 50);
                                    [ref] $null = $tcW2.SetAttribute('type', $xmlnsMain, 'pct');
                                }

                                $p2 = $tc2.AppendChild($XmlDocument.CreateElement('w', 'p', $xmlnsMain));
                                $cellPropertyStyle = '{0}__Style' -f $propertyName;
                                if ($row.$cellPropertyStyle) {
                                    if ($cellStyle.Id -ne $row.$cellPropertyStyle) {
                                        ## Retrieve the style if we don't already have it
                                        $cellStyle = $Document.Styles[$row.$cellPropertyStyle];
                                    }
                                    if ($cellStyle.BackgroundColor) {
                                        [ref] $null = $tc2.AppendChild((GetWordTableStyleCellPr -Style $cellStyle -XmlDocument $XmlDocument));
                                    }
                                    if ($row.$cellPropertyStyle) {
                                        $pPr2 = $p2.AppendChild($XmlDocument.CreateElement('w', 'pPr', $xmlnsMain));
                                        $pStyle2 = $pPr2.AppendChild($XmlDocument.CreateElement('w', 'pStyle', $xmlnsMain));
                                        [ref] $null = $pStyle2.SetAttribute('val', $xmlnsMain, $row.$cellPropertyStyle);
                                    }
                                }
                        
                                $r2 = $p2.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlnsMain));
                                $t2 = $r2.AppendChild($XmlDocument.CreateElement('w', 't', $xmlnsMain));
                                [ref] $null = $t2.AppendChild($XmlDocument.CreateTextNode($row.($propertyName)));
                            }
                        } #end for each property
                     } #end foreach row
                } #end if Table.List
                else {
            
                    $tbl = $Element.AppendChild((GetWordTable -Table $Table -XmlDocument $XmlDocument));
        
                    $tr = $tbl.AppendChild($XmlDocument.CreateElement('w', 'tr', $xmlnsMain));
                    $trPr = $tr.AppendChild($XmlDocument.CreateElement('w', 'trPr', $xmlnsMain));
                    [ref] $rblHeader = $trPr.AppendChild($XmlDocument.CreateElement('w', 'tblHeader', $xmlnsMain)); ## Flow headers across pages
                    for ($i = 0; $i -lt $Table.Columns.Count; $i++) {
                        $tc = $tr.AppendChild($XmlDocument.CreateElement('w', 'tc', $xmlnsMain));
                        if ($headerStyle.BackgroundColor) {
                            $tcPr = $tc.AppendChild((GetWordTableStyleCellPr -Style $headerStyle -XmlDocument $XmlDocument));
                        }
                        else {
                            $tcPr = $tc.AppendChild($XmlDocument.CreateElement('w', 'tcPr', $xmlnsMain));
                        }
                        $tcW = $tcPr.AppendChild($XmlDocument.CreateElement('w', 'tcW', $xmlnsMain));

                        if (($Table.ColumnWidths -ne $null) -and ($Table.ColumnWidths[$i] -ne $null)) {
                            [ref] $null = $tcW.SetAttribute('w', $xmlnsMain, $Table.ColumnWidths[$i] * 50);
                            [ref] $null = $tcW.SetAttribute('type', $xmlnsMain, 'pct');
                        }
                        else {
                            [ref] $null = $tcW.SetAttribute('w', $xmlnsMain, 0);
                            [ref] $null = $tcW.SetAttribute('type', $xmlnsMain, 'auto');
                        }

                        $p = $tc.AppendChild($XmlDocument.CreateElement('w', 'p', $xmlnsMain));
                        $pPr = $p.AppendChild($XmlDocument.CreateElement('w', 'pPr', $xmlnsMain));
                        $pStyle = $pPr.AppendChild($XmlDocument.CreateElement('w', 'pStyle', $xmlnsMain));
                        [ref] $null = $pStyle.SetAttribute('val', $xmlnsMain, $tableStyle.HeaderStyle);
                        $r = $p.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlnsMain));
                        $t = $r.AppendChild($XmlDocument.CreateElement('w', 't', $xmlnsMain));
                        if ($null -eq $Table.Headers) {
                            [ref] $null = $t.AppendChild($XmlDocument.CreateTextNode($Table.Columns[$i]));
                        }
                        else {
                            [ref] $null = $t.AppendChild($XmlDocument.CreateTextNode($Table.Headers[$i]));
                        }
                    } #end for Table.Columns

                    $isAlternatingRow = $false;
                    foreach ($row in $Table.Rows) {
                        $tr = $tbl.AppendChild($XmlDocument.CreateElement('w', 'tr', $xmlnsMain));
                        foreach ($propertyName in $Table.Columns) {
                            $cellPropertyStyle = '{0}__Style' -f $propertyName;
                            if ($row.$cellPropertyStyle) {
                                ## Cell style overrides row/default styles
                                $cellStyleName = $row.$cellPropertyStyle;
                            }
                            elseif (-not [System.String]::IsNullOrEmpty($row.__Style)) {
                                ## Row style overrides default style
                                $cellStyleName = $row.__Style;
                            }
                            else {
                                ## Use the table row/alternating style..
                                $cellStyleName = $tableStyle.RowStyle;
                                if ($isAlternatingRow) {
                                    $cellStyleName = $tableStyle.AlternateRowStyle;
                                }
                            }

                            if ($cellStyle.Id -ne $cellStyleName) {
                                $cellStyle = $Document.Styles[$cellStyleName];
                            }
                
                            $tc = $tr.AppendChild($XmlDocument.CreateElement('w', 'tc', $xmlnsMain));
                            if ($cellStyle.BackgroundColor) {
                                [ref] $null = $tc.AppendChild((GetWordTableStyleCellPr -Style $cellStyle -XmlDocument $XmlDocument));
                            }
                            $p = $tc.AppendChild($XmlDocument.CreateElement('w', 'p', $xmlnsMain));
                            $pPr = $p.AppendChild($XmlDocument.CreateElement('w', 'pPr', $xmlnsMain));
                            $pStyle = $pPr.AppendChild($XmlDocument.CreateElement('w', 'pStyle', $xmlnsMain));
                            [ref] $null = $pStyle.SetAttribute('val', $xmlnsMain, $cellStyleName);
                            $r = $p.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlnsMain));
                            $t = $r.AppendChild($XmlDocument.CreateElement('w', 't', $xmlnsMain));
                            [ref] $null = $t.AppendChild($XmlDocument.CreateTextNode($row.($propertyName)));
                        } #end foreach property
                        $isAlternatingRow = !$isAlternatingRow;
                    } #end foreach row
                } #end if not Table.List
        
            } #end process
        } #end function OutWordTable

        function OutWordTOC {
            <#
            .SYNOPSIS
                 Output formatted Word table of contents.
            #>

            [CmdletBinding()]
            [OutputType([System.Xml.XmlElement])]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $TOC,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument
            )
            process {
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
                $sdt = $XmlDocument.CreateElement('w', 'sdt', $xmlnsMain);
                $sdtPr = $sdt.AppendChild($XmlDocument.CreateElement('w', 'sdtPr', $xmlnsMain));
                $docPartObj = $sdtPr.AppendChild($XmlDocument.CreateElement('w', 'docPartObj', $xmlnsMain));
                $docObjectGallery = $docPartObj.AppendChild($XmlDocument.CreateElement('w', 'docPartGallery', $xmlnsMain));
                [ref] $null = $docObjectGallery.SetAttribute('val', $xmlnsMain, 'Table of Contents');
                [ref] $null = $docPartObj.AppendChild($XmlDocument.CreateElement('w', 'docPartUnique', $xmlnsMain));
                $sdtEndPr = $sdt.AppendChild($XmlDocument.CreateElement('w', 'stdEndPr', $xmlnsMain));

                $sdtContent = $sdt.AppendChild($XmlDocument.CreateElement('w', 'stdContent', $xmlnsMain));
                $p1 = $sdtContent.AppendChild($XmlDocument.CreateElement('w', 'p', $xmlnsMain));
                $pPr1 = $p1.AppendChild($XmlDocument.CreateElement('w', 'pPr', $xmlnsMain));
                $pStyle1 = $pPr1.AppendChild($XmlDocument.CreateElement('w', 'pStyle', $xmlnsMain));
                [ref] $null = $pStyle1.SetAttribute('val', $xmlnsMain, 'TOC');
                $r1 = $p1.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlnsMain));
                $t1 = $r1.AppendChild($XmlDocument.CreateElement('w', 't', $xmlnsMain));
                [ref] $null = $t1.AppendChild($XmlDocument.CreateTextNode($TOC.Name));

                $p2 = $sdtContent.AppendChild($XmlDocument.CreateElement('w', 'p', $xmlnsMain));
                $pPr2 = $p2.AppendChild($XmlDocument.CreateElement('w', 'pPr', $xmlnsMain));
                $tabs2 = $pPr2.AppendChild($XmlDocument.CreateElement('w', 'tabs', $xmlnsMain));
                $tab2 = $tabs2.AppendChild($XmlDocument.CreateElement('w', 'tab', $xmlnsMain));
                [ref] $null = $tab2.SetAttribute('val', $xmlnsMain, 'right');
                [ref] $null = $tab2.SetAttribute('leader', $xmlnsMain, 'dot');
                [ref] $null = $tab2.SetAttribute('pos', $xmlnsMain, '9016'); #10790?!
                $r2 = $p2.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlnsMain));
                ##TODO: Refactor duplicate code
                $fldChar1 = $r2.AppendChild($XmlDocument.CreateElement('w', 'fldChar', $xmlnsMain));
                [ref] $null = $fldChar1.SetAttribute('fldCharType', $xmlnsMain, 'begin');
                
                $r3 = $p2.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlnsMain));
                $instrText = $r3.AppendChild($XmlDocument.CreateElement('w', 'instrText', $xmlnsMain));
                [ref] $null = $instrText.SetAttribute('space', 'http://www.w3.org/XML/1998/namespace', 'preserve');
                [ref] $null = $instrText.AppendChild($XmlDocument.CreateTextNode(' TOC \o "1-3" \h \z \u '));

                $r4 = $p2.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlnsMain));
                $fldChar2 = $r4.AppendChild($XmlDocument.CreateElement('w', 'fldChar', $xmlnsMain));
                [ref] $null = $fldChar2.SetAttribute('fldCharType', $xmlnsMain, 'separate');

                $p3 = $sdtContent.AppendChild($XmlDocument.CreateElement('w', 'p', $xmlnsMain));
                $r5 = $p3.AppendChild($XmlDocument.CreateElement('w', 'r', $xmlnsMain));
                #$rPr3 = $r3.AppendChild($XmlDocument.CreateElement('w', 'rPr', $xmlnsMain));
                $fldChar3 = $r5.AppendChild($XmlDocument.CreateElement('w', 'fldChar', $xmlnsMain));
                [ref] $null = $fldChar3.SetAttribute('fldCharType', $xmlnsMain, 'end');

                return $sdt;
            } #end process
        } #end function OutWordTOC

        function OutWordBlankLine {
            <#
            .SYNOPSIS
                Output formatted Word xml blank line (paragraph).
            #>

            [CmdletBinding()]
            param (
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $BlankLine,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument,
                [Parameter(Mandatory)] [System.Xml.XmlElement] $Element         
            )
            process {
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
                for ($i = 0; $i -lt $BlankLine.LineCount; $i++) {
                    [ref] $null = $Element.AppendChild($XmlDocument.CreateElement('w', 'p', $xmlnsMain));
                }
            }
        } #end function OutWordLineBreak

        function GetWordStyle {
            <#
            .SYNOPSIS
                Generates Word Xml style element from a PScribo document style.
            #>

            [CmdletBinding()]
            [OutputType([System.Xml.XmlElement])]
            param (
                ## PScribo document style
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $Style,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument,
                [Parameter(Mandatory)] [ValidateSet('Paragraph','Character')] [System.String] $Type
            )
            process {
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
                if ($Type -eq 'Paragraph') {
                    $styleId = $Style.Id;
                    $styleName = $Style.Name;
                    $linkId = '{0}Char' -f $Style.Id;
                }
                else {
                    $styleId = '{0}Char' -f $Style.Id;
                    $styleName = '{0} Char' -f $Style.Name;
                    $linkId = $Style.Id;
                }


                $documentStyle = $XmlDocument.CreateElement('w', 'style', $xmlnsMain);
                [ref] $null = $documentStyle.SetAttribute('type', $xmlnsMain, $Type.ToLower());
                if ($Style.Id -eq $Document.DefaultStyle) {
                    ## Set as default style
                    [ref] $null = $documentStyle.SetAttribute('default', $xmlnsMain, 1);
                    $uiPriority = $documentStyle.AppendChild($XmlDocument.CreateElement('w', 'uiPriority', $xmlnsMain));
                    [ref] $null = $uiPriority.SetAttribute('val', $xmlnsMain, 1);
                }
                elseif (($Style.Id -eq 'Footer') -or ($Style.Id -eq 'Header')) {
                    ## Semi hide the styles named Footer and Header
                    [ref] $null = $documentStyle.AppendChild($XmlDocument.CreateElement('w', 'semiHidden', $xmlnsMain));
                }
                elseif (($document.TableStyles.Values | ForEach-Object { $_.HeaderStyle; $_.RowStyle; $_.AlternateRowStyle; }) -contains $Style.Id) {
                    ## Semi hide styles behind table styles (except default style!)
                    [ref] $null = $documentStyle.AppendChild($XmlDocument.CreateElement('w', 'semiHidden', $xmlnsMain));
                }

                [ref] $null = $documentStyle.SetAttribute('styleId', $xmlnsMain, $styleId);
                $documentStyleName = $documentStyle.AppendChild($xmlDocument.CreateElement('w', 'name', $xmlnsMain));
                [ref] $null = $documentStyleName.SetAttribute('val', $xmlnsMain, $styleName);
                $basedOn = $documentStyle.AppendChild($XmlDocument.CreateElement('w', 'basedOn', $xmlnsMain));
                [ref] $null = $basedOn.SetAttribute('val', $XmlnsMain, 'Normal');
                $link = $documentStyle.AppendChild($XmlDocument.CreateElement('w', 'link', $xmlnsMain));
                [ref] $null = $link.SetAttribute('val', $XmlnsMain, $linkId);
                $next = $documentStyle.AppendChild($XmlDocument.CreateElement('w', 'next', $xmlnsMain));
                [ref] $null = $next.SetAttribute('val', $xmlnsMain, 'Normal');
                $qFormat = $documentStyle.AppendChild($XmlDocument.CreateElement('w', 'qFormat', $xmlnsMain));
                $pPr = $documentStyle.AppendChild($XmlDocument.CreateElement('w', 'pPr', $xmlnsMain));
                $keepNext = $pPr.AppendChild($XmlDocument.CreateElement('w', 'keepNext', $xmlnsMain));
                $keepLines = $pPr.AppendChild($XmlDocument.CreateElement('w', 'keepLines', $xmlnsMain));
                $spacing = $pPr.AppendChild($XmlDocument.CreateElement('w', 'spacing', $xmlnsMain));
                [ref] $null = $spacing.SetAttribute('before', $xmlnsMain, 0);
                [ref] $null = $spacing.SetAttribute('after', $xmlnsMain, 0);
                ## Set the <w:jc> (justification) element
                $jc = $pPr.AppendChild($XmlDocument.CreateElement('w', 'jc', $xmlnsMain));
                if ($Style.Align.ToLower() -eq 'justify') {
                    [ref] $null = $jc.SetAttribute('val', $xmlnsMain, 'distribute');
                }
                else {
                    [ref] $null = $jc.SetAttribute('val', $xmlnsMain, $Style.Align.ToLower());
                }
                if ($Style.BackgroundColor) {
                    $shd = $pPr.AppendChild($XmlDocument.CreateElement('w', 'shd', $xmlnsMain));
                    [ref] $null = $shd.SetAttribute('val', $xmlnsMain, 'clear');
                    [ref] $null = $shd.SetAttribute('color', $xmlnsMain, 'auto');
                    [ref] $null = $shd.SetAttribute('fill', $xmlnsMain, (ConvertToWordColor -Color $Style.BackgroundColor));
                }
                [ref] $null = $documentStyle.AppendChild((GetWordStyleRunPr -Style $Style -XmlDocument $XmlDocument));
        
                return $documentStyle;
            } #end process
        } #end function GetWordStyle

        function GetWordTableStyle {
            <#
            .SYNOPSIS
                Generates Word Xml table style element from a PScribo document table style.
            #>

            [CmdletBinding()]
            param (
                ## PScribo document style
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $TableStyle,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument
            )
            process {
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
                $style = $XmlDocument.CreateElement('w', 'style', $xmlnsMain);
                [ref] $null = $style.SetAttribute('type', $xmlnsMain, 'table');
                [ref] $null = $style.SetAttribute('styleId', $xmlnsMain, $TableStyle.Id);
                $name = $style.AppendChild($XmlDocument.CreateElement('w', 'name', $xmlnsMain));
                [ref] $null = $name.SetAttribute('val', $xmlnsMain, $TableStyle.Id);
                $tblPr = $style.AppendChild($XmlDocument.CreateElement('w', 'tblPr', $xmlnsMain));
                $tblStyleRowBandSize = $tblPr.AppendChild($XmlDocument.CreateElement('w', 'tblStyleRowBandSize', $xmlnsMain));
                [ref] $null = $tblStyleRowBandSize.SetAttribute('val', $xmlnsMain, 1);
                if ($tableStyle.BorderWidth -gt 0) {
                    $tblBorders = $tblPr.AppendChild($XmlDocument.CreateElement('w', 'tblBorders', $xmlnsMain));
                    foreach ($border in @('top','bottom','start','end','insideH','insideV')) {
                        $b = $tblBorders.AppendChild($XmlDocument.CreateElement('w', $border, $xmlnsMain));
                        [ref] $null = $b.SetAttribute('sz', $xmlnsMain, (ConvertMmToOctips $tableStyle.BorderWidth));
                        [ref] $null = $b.SetAttribute('val', $xmlnsMain, 'single');
                        [ref] $null = $b.SetAttribute('color', $xmlnsMain, (ConvertToWordColor -Color $tableStyle.BorderColor));
                    }
                }
                [ref] $null = $style.AppendChild((GetWordTableStylePr -Style $Document.Styles[$TableStyle.HeaderStyle] -Type Header -XmlDocument $XmlDocument));
                [ref] $null = $style.AppendChild((GetWordTableStylePr -Style $Document.Styles[$TableStyle.RowStyle] -Type Row -XmlDocument $XmlDocument));
                [ref] $null = $style.AppendChild((GetWordTableStylePr -Style $Document.Styles[$TableStyle.AlternateRowStyle] -Type AlternateRow -XmlDocument $XmlDocument));
                return $style;
            }
        } #end function GetWordTableStyle

        function GetWordStyleParagraphPr {
            <#
            .SYNOPSIS
                Generates Word paragraph (pPr) formatting properties
            #>

            [CmdletBinding()]
            [OutputType([System.Xml.XmlElement])]
            param (
                [Parameter(Mandatory)] [System.Object] $Style,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument
            )
            process {
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
                $pPr = $XmlDocument.CreateElement('w', 'pPr', $xmlnsMain);
                $spacing = $pPr.AppendChild($XmlDocument.CreateElement('w', 'spacing', $xmlnsMain));
                [ref] $null = $spacing.SetAttribute('before', $xmlnsMain, 0);
                [ref] $null = $spacing.SetAttribute('after', $xmlnsMain, 0);
                $keepNext = $pPr.AppendChild($XmlDocument.CreateElement('w', 'keepNext', $xmlnsMain));
                $keepLines = $pPr.AppendChild($XmlDocument.CreateElement('w', 'keepLines', $xmlnsMain));
                $jc = $pPr.AppendChild($XmlDocument.CreateElement('w', 'jc', $xmlnsMain));
                if ($Style.Align.ToLower() -eq 'justify') { [ref] $null = $jc.SetAttribute('val', $xmlnsMain, 'distribute'); }
                else { [ref] $null = $jc.SetAttribute('val', $xmlnsMain, $Style.Align.ToLower()); }
                return $pPr;
            } #end process
        } #end function GetWordTableCellPr

        function GetWordStyleRunPrColor {
            <#
            .SYNOPSIS
                Generates Word run (rPr) text colour formatting property only.
            .NOTES
                This is only required to override the text colour in table rows/headers
                as I can't get this (yet) applied via the table style?
            #>

            [CmdletBinding()]
            [OutputType([System.Xml.XmlElement])]
            param (
                [Parameter(Mandatory)] [System.Object] $Style,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument
            )
            process {
                $rPr = $XmlDocument.CreateElement('w', 'rPr', $xmlnsMain);
                $color = $rPr.AppendChild($XmlDocument.CreateElement('w', 'color', $xmlnsMain));
                [ref] $null = $color.SetAttribute('val', $xmlnsMain, (ConvertToWordColor -Color $Style.Color));
                return $rPr;
            }
        } #end function GetWordStyleRunPrColor

        function GetWordStyleRunPr {
            <#
                .SYNOPSIS
                    Generates Word run (rPr) formatting properties
            #>

            [CmdletBinding()]
            [OutputType([System.Xml.XmlElement])]
            param (
                [Parameter(Mandatory)] [System.Object] $Style,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument
            )
            process {
                $rPr = $XmlDocument.CreateElement('w', 'rPr', $xmlnsMain); 
                $rFonts = $rPr.AppendChild($XmlDocument.CreateElement('w', 'rFonts', $xmlnsMain));
                [ref] $null = $rFonts.SetAttribute('ascii', $xmlnsMain, $Style.Font[0]);
                [ref] $null = $rFonts.SetAttribute('hAnsi', $xmlnsMain, $Style.Font[0]);
                if ($Style.Bold) {
                    [ref] $null = $rPr.AppendChild($XmlDocument.CreateElement('w', 'b', $xmlnsMain));
                }
                if ($Style.Underline) {
                    [ref] $null = $rPr.AppendChild($XmlDocument.CreateElement('w', 'u', $xmlnsMain));
                }
                if ($Style.Italic) {
                    [ref] $null = $rPr.AppendChild($XmlDocument.CreateElement('w', 'i', $xmlnsMain));
                }
                $color = $rPr.AppendChild($XmlDocument.CreateElement('w', 'color', $xmlnsMain));
                [ref] $null = $color.SetAttribute('val', $xmlnsMain, (ConvertToWordColor -Color $Style.Color));
                $sz = $rPr.AppendChild($XmlDocument.CreateElement('w', 'sz', $xmlnsMain));
                [ref] $null = $sz.SetAttribute('val', $xmlnsMain, $Style.Size * 2);
                return $rPr;
            } #end process
        } #end function GetWordStyleRunPr

        function GetWordTableStyleCellPr {
            <#
            .SYNOPSIS
                Generates Word table cell (tcPr) formatting properties
            #>

            [CmdletBinding()]
            [OutputType([System.Xml.XmlElement])]
            param (
                [Parameter(Mandatory)] [System.Object] $Style,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument
            )
            process {
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
                $tcPr = $XmlDocument.CreateElement('w', 'tcPr', $xmlnsMain);
                if ($Style.BackgroundColor) {
                    $shd = $tcPr.AppendChild($XmlDocument.CreateElement('w', 'shd', $xmlnsMain));
                    [ref] $null = $shd.SetAttribute('val', $xmlnsMain, 'clear');
                    [ref] $null = $shd.SetAttribute('color', $xmlnsMain, 'auto');
                    [ref] $null = $shd.SetAttribute('fill', $xmlnsMain, (ConvertToWordColor -Color $Style.BackgroundColor));
                }
                return $tcPr;
            } #end process
        } #end function GetWordTableCellPr

        function GetWordTableStylePr {
            <#
            .SYNOPSIS
                Generates Word table style (tblStylePr) formatting properties for specified table style type
            #>

            [CmdletBinding()]
            [OutputType([System.Xml.XmlElement])]
            param (
                [Parameter(Mandatory)] [System.Object] $Style,
                [Parameter(Mandatory)] [ValidateSet('Header','Row','AlternateRow')] [System.String] $Type,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument
            )
            process {
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
                $tblStylePr = $XmlDocument.CreateElement('w', 'tblStylePr', $xmlnsMain);
                $tblPr = $tblStylePr.AppendChild($XmlDocument.CreateElement('w', 'tblPr', $xmlnsMain));
                switch ($Type) {
                    'Header' { $tblStylePrType = 'firstRow'; }
                    'Row' { $tblStylePrType = 'band2Horz'; }
                    'AlternateRow' { $tblStylePrType = 'band1Horz'; }
                }
                [ref] $null = $tblStylePr.SetAttribute('type', $xmlnsMain, $tblStylePrType);
                [ref] $null = $tblStylePr.AppendChild((GetWordStyleParagraphPr -Style $Style -XmlDocument $XmlDocument));
                [ref] $null = $tblStylePr.AppendChild((GetWordStyleRunPr -Style $Style -XmlDocument $XmlDocument));
                [ref] $null = $tblStylePr.AppendChild((GetWordTableStyleCellPr -Style $Style -XmlDocument $XmlDocument));
                return $tblStylePr;
            } #end process
        } #end function GetWordTableStylePr

        function GetWordSectionPr {
            <#
            .SYNOPSIS
                Outputs Office Open XML section element to set page size and margins.
            #>

            [CmdletBinding()]
            [OutputType([System.Xml.XmlElement])]
            param (
                [Parameter(Mandatory)] [System.Single] $PageWidth,
                [Parameter(Mandatory)] [System.Single] $PageHeight,
                [Parameter(Mandatory)] [System.Single] $PageMarginTop,
                [Parameter(Mandatory)] [System.Single] $PageMarginLeft,
                [Parameter(Mandatory)] [System.Single] $PageMarginBottom,
                [Parameter(Mandatory)] [System.Single] $PageMarginRight,
                [Parameter(Mandatory)] [System.Xml.XmlDocument] $XmlDocument
            )
            process {
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
                $sectPr = $XmlDocument.CreateElement('w', 'sectPr', $xmlnsMain);
                $pgSz = $sectPr.AppendChild($XmlDocument.CreateElement('w', 'pgSz', $xmlnsMain));
                [ref] $null = $pgSz.SetAttribute('w', $xmlnsMain, (ConvertMmToTwips -Millimeter $PageWidth));
                [ref] $null = $pgSz.SetAttribute('h', $xmlnsMain, (ConvertMmToTwips -Millimeter $PageHeight));
                [ref] $null = $pgSz.SetAttribute('orient', $xmlnsMain, 'portrait');
                $pgMar = $sectPr.AppendChild($XmlDocument.CreateElement('w', 'pgMar', $xmlnsMain));
                [ref] $null = $pgMar.SetAttribute('top', $xmlnsMain, (ConvertMmToTwips -Millimeter $PageMarginTop));
                [ref] $null = $pgMar.SetAttribute('bottom', $xmlnsMain, (ConvertMmToTwips -Millimeter $PageMarginBottom));
                [ref] $null = $pgMar.SetAttribute('left', $xmlnsMain, (ConvertMmToTwips -Millimeter $PageMarginLeft));
                [ref] $null = $pgMar.SetAttribute('right', $xmlnsMain, (ConvertMmToTwips -Millimeter $PageMarginRight));
                return $sectPr;
            } #end process
        } #end GetWordSectionPr

        function OutWordStylesDocument {
            <#
            .SYNOPSIS
                Outputs Office Open XML style document
            #>

            [CmdletBinding()]
            [OutputType([System.Xml.XmlDocument])]
            param (
                ## PScribo document styles
                [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] $Styles,
                ## PScribo document tables styles
                [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.Hashtable] $TableStyles
            )
            process {
                ## Create the Style.xml document
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
                $xmlDocument = New-Object -TypeName 'System.Xml.XmlDocument';
                [ref] $null = $xmlDocument.AppendChild($xmlDocument.CreateXmlDeclaration('1.0', 'utf-8', 'yes'));
                $documentStyles = $xmlDocument.AppendChild($xmlDocument.CreateElement('w', 'styles', $xmlnsMain));
        
                ## Create default style
                $defaultStyle = $documentStyles.AppendChild($xmlDocument.CreateElement('w', 'style', $xmlnsMain));
                [ref] $null = $defaultStyle.SetAttribute('type', $xmlnsMain, 'paragraph');
                [ref] $null = $defaultStyle.SetAttribute('default', $xmlnsMain, '1');
                [ref] $null = $defaultStyle.SetAttribute('styleId', $xmlnsMain, 'Normal');
                $defaultStyleName = $defaultStyle.AppendChild($xmlDocument.CreateElement('w', 'name', $xmlnsMain));
                [ref] $null = $defaultStyleName.SetAttribute('val', $xmlnsMain, 'Normal');
                [ref] $null = $defaultStyle.AppendChild($xmlDocument.CreateElement('w', 'qFormat', $xmlnsMain));
   
                foreach ($style in $Styles.Values) {
                    $documentParagraphStyle = GetWordStyle -Style $style -XmlDocument $xmlDocument -Type Paragraph;
                    [ref] $null = $documentStyles.AppendChild($documentParagraphStyle);
                    $documentCharacterStyle = GetWordStyle -Style $style -XmlDocument $xmlDocument -Type Character;
                    [ref] $null = $documentStyles.AppendChild($documentCharacterStyle);
                }
                foreach ($tableStyle in $TableStyles.Values) {
                    $documentTableStyle = GetWordTableStyle -TableStyle $tableStyle -XmlDocument $xmlDocument;
                    [ref] $null = $documentStyles.AppendChild($documentTableStyle);
                }
                return $xmlDocument;
            } #end process
        } #end function OutWordStyleDocument

        function OutWordSettingsDocument {
            <#
            .SYNOPSIS
                Outputs Office Open XML settings document
            #>

            [CmdletBinding()]
            [OutputType([System.Xml.XmlDocument])]
            param (
                [Parameter()] [System.Management.Automation.SwitchParameter] $UpdateFields
            )
            process {
                ## Create the Style.xml document
                $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
                # <w:settings xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                # xmlns:o="urn:schemas-microsoft-com:office:office"
                # xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
                # xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
                # xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w10="urn:schemas-microsoft-com:office:word"
                # xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
                # xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
                # xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
                # xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main"
                # mc:Ignorable="w14 w15">
                $settingsDocument = New-Object -TypeName 'System.Xml.XmlDocument';
                [ref] $null = $settingsDocument.AppendChild($settingsDocument.CreateXmlDeclaration('1.0', 'utf-8', 'yes'));
                $settings = $settingsDocument.AppendChild($settingsDocument.CreateElement('w', 'settings', $xmlnsMain));
                ## Set compatibility mode to Word 2013
                $compat = $settings.AppendChild($settingsDocument.CreateElement('w', 'compat', $xmlnsMain));
                $compatSetting = $compat.AppendChild($settingsDocument.CreateElement('w', 'compatSetting', $xmlnsMain));
                [ref] $null = $compatSetting.SetAttribute('name', $xmlnsMain, 'compatibilityMode');
                [ref] $null = $compatSetting.SetAttribute('uri', $xmlnsMain, 'http://schemas.microsoft.com/office/word');
                [ref] $null = $compatSetting.SetAttribute('val', $xmlnsMain, 15);
                if ($UpdateFields) {
                    $wupdateFields = $settings.AppendChild($settingsDocument.CreateElement('w', 'updateFields', $xmlnsMain));
                    [ref] $null = $wupdateFields.SetAttribute('val', $xmlnsMain, 'true');
                }
                return $settingsDocument;
            } #end process
        } #end function OutWordSettingsDocument

        #endregion OutWord Private Functions
    }
    process {
        $stopwatch = [Diagnostics.Stopwatch]::StartNew();
        WriteLog -Message ($localized.DocumentProcessingStarted -f $Document.Name);
        $xmlnsMain = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
        $xmlDocument = New-Object -TypeName 'System.Xml.XmlDocument';
        [ref] $null = $xmlDocument.AppendChild($xmlDocument.CreateXmlDeclaration('1.0', 'utf-8', 'yes'));
        $documentXml = $xmlDocument.AppendChild($xmlDocument.CreateElement('w', 'document', $xmlnsMain));
        [ref] $null = $xmlDocument.DocumentElement.SetAttribute('xmlns:xml', 'http://www.w3.org/XML/1998/namespace');
        $body = $documentXml.AppendChild($xmlDocument.CreateElement('w', 'body', $xmlnsMain));
        ## Setup the document page size/margins
        $sectionPrParams = @{
            PageHeight = $Document.Options['PageHeight']; PageWidth = $Document.Options['PageWidth'];
            PageMarginTop = $Document.Options['MarginTop']; PageMarginBottom = $Document.Options['MarginBottom'];
            PageMarginLeft = $Document.Options['MarginLeft']; PageMarginRight = $Document.Options['MarginRight'];
        }
        [ref] $null = $body.AppendChild((GetWordSectionPr @sectionPrParams -XmlDocument $xmlDocument));
        foreach ($s in $Document.Sections.GetEnumerator()) {
            if ($s.Id.Length -gt 40) { $sectionId = '{0}[..]' -f $s.Id.Substring(0,36); }
            else { $sectionId = $s.Id; }
            WriteLog -Message ($localized.PluginProcessingSection -f $s.Type, $sectionId) -Indent ($s.Level +1);
            switch ($s.Type) {
                'PScribo.Section' { $s | OutWordSection -RootElement $body -XmlDocument $xmlDocument; }
                'PScribo.Paragraph' { [ref] $null = $body.AppendChild((OutWordParagraph -Paragraph $s -XmlDocument $xmlDocument)); }
                'PScribo.PageBreak' { [ref] $null = $body.AppendChild((OutWordPageBreak -PageBreak $s -XmlDocument $xmlDocument)); }
                'PScribo.LineBreak' { [ref] $null = $body.AppendChild((OutWordLineBreak -LineBreak $s -XmlDocument $xmlDocument)); }
                'PScribo.Table' { OutWordTable -Table $s -XmlDocument $xmlDocument -Element $body; }
                'PScribo.TOC' { [ref] $null = $body.AppendChild((OutWordTOC -TOC $s -XmlDocument $xmlDocument)); }
                'PScribo.BlankLine' { OutWordBlankLine -BlankLine $s -XmlDocument $xmlDocument -Element $body; }
                Default { WriteLog -Message ($localized.PluginUnsupportedSection -f $s.Type) -IsWarning; }
            } #end switch
        } #end foreach
        ## Generate the Word 'styles.xml' document part
        $stylesXml = OutWordStylesDocument -Styles $Document.Styles -TableStyles $Document.TableStyles;
        ## Generate the Word 'settings.xml' document part
        if ($Document.Properties['TOCs'].Count -gt 0) {
            ## We have a TOC so flag to update the document when opened
            $settingsXml = OutWordSettingsDocument -UpdateFields;
        }
        else {
            $settingsXml = OutWordSettingsDocument;
        }
        $destinationPath = Join-Path -Path $Path ('{0}.docx' -f $Document.Name);
        Add-Type -AssemblyName WindowsBase;
        try {
            $package = [System.IO.Packaging.Package]::Open($destinationPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite);
        }
        catch {
            WriteLog -Message ($localized.OpenPackageError -f $destinationPath) -IsWarning;
            throw $_;
        }
        ## Create document.xml part
        $documentUri = New-Object System.Uri('/word/document.xml', [System.UriKind]::Relative);
        WriteLog -Message ($localized.ProcessingDocumentPart -f $documentUri);
        $documentPart = $package.CreatePart($documentUri, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml');
        $streamWriter = New-Object System.IO.StreamWriter($documentPart.GetStream([System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite));
        $xmlWriter = [System.Xml.XmlWriter]::Create($streamWriter);
        WriteLog -Message ($localized.WritingDocumentPart -f $documentUri);
        $xmlDocument.Save($xmlWriter);
        $xmlWriter.Close();
        $streamWriter.Close();

        ## Create styles.xml part
        $stylesUri = New-Object System.Uri('/word/styles.xml', [System.UriKind]::Relative);
        WriteLog -Message ($localized.ProcessingDocumentPart -f $stylesUri);
        $stylesPart = $package.CreatePart($stylesUri, 'application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml');
        $streamWriter = New-Object System.IO.StreamWriter($stylesPart.GetStream([System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite));
        $xmlWriter = [System.Xml.XmlWriter]::Create($streamWriter);
        WriteLog -Message ($localized.WritingDocumentPart -f $stylesUri);
        $stylesXml.Save($xmlWriter);
        $xmlWriter.Close();
        $streamWriter.Close();

        ## Create settings.xml part
        $settingsUri = New-Object System.Uri('/word/settings.xml', [System.UriKind]::Relative);
        WriteLog -Message ($localized.ProcessingDocumentPart -f $settingsUri);
        $settingsPart = $package.CreatePart($settingsUri, 'application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml');
        $streamWriter = New-Object System.IO.StreamWriter($settingsPart.GetStream([System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite));
        $xmlWriter = [System.Xml.XmlWriter]::Create($streamWriter);
        WriteLog -Message ($localized.WritingDocumentPart -f $settingsUri);
        $settingsXml.Save($xmlWriter);
        $xmlWriter.Close();
        $streamWriter.Close();

        ## Create the Package relationships
        WriteLog -Message $localized.GeneratingPackageRelationships;
        [ref] $null = $package.CreateRelationship($documentUri, [System.IO.Packaging.TargetMode]::Internal, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument', 'rId1');
        [ref] $null = $documentPart.CreateRelationship($stylesUri, [System.IO.Packaging.TargetMode]::Internal, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles', 'rId1');
        [ref] $null = $documentPart.CreateRelationship($settingsUri, [System.IO.Packaging.TargetMode]::Internal, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings', 'rId2');
        WriteLog -Message ($localized.SavingFile -f $destinationPath);
        $package.Flush();
        $package.Close();

        $stopwatch.Stop();       
        WriteLog -Message ($localized.DocumentProcessingCompleted -f $Document.Name);
        WriteLog -Message ($localized.TotalProcessingTime -f $stopwatch.Elapsed.TotalSeconds);
        ## Return the file reference to the pipeline
        Write-Output (Get-Item -Path $destinationPath);
    } #end process
} #end function OutWord

function OutXml {
    <#
    .SYNOPSIS
        Xml output plugin for PScribo.
    .DESCRIPTION
        Outputs a xml representation of a PScribo document object.
    #>

    [CmdletBinding()]
    param (
        ## ThePScribo document object to convert to a xml document
        [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $Document,
        ## Output directory path for the .xml file
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNull()] [System.String] $Path,
        ### Hashtable of all plugin supported options
        [Parameter(ValueFromPipelineByPropertyName)] [AllowNull()] [System.Collections.Hashtable] $Options
    )
    begin {
        #region OutXml Private Functions
        function OutXmlSection {
            <#
            .SYNOPSIS
                Output formatted Xml section.
            #>

            [CmdletBinding()]
            param (
                ## PScribo document section
                [Parameter(Mandatory, ValueFromPipeline)] [System.Object] $Section
            )
            process {
                $sectionId = ($Section.Id -replace '[^a-z0-9-_\.]','').ToLower();
                $element = $xmlDocument.CreateElement($sectionId);
                [ref] $null = $element.SetAttribute("name", $Section.Name);
                foreach ($s in $Section.Sections.GetEnumerator()) {
                    if ($s.Id.Length -gt 40) { $sectionId = '{0}..' -f $s.Id.Substring(0,38); }
                    else { $sectionId = $s.Id; }
                    WriteLog -Message ($localized.PluginProcessingSection -f $s.Type, $sectionId) -Indent ($s.Level +1);
                    switch ($s.Type) {
                        'PScribo.Section' { [ref] $null = $element.AppendChild((OutXmlSection -Section $s)); }
                        'PScribo.Paragraph' { [ref] $null = $element.AppendChild((OutXmlParagraph -Paragraph $s)); }
                        'PScribo.Table' { [ref] $null = $element.AppendChild((OutXmlTable -Table $s)); }
                        'PScribo.PageBreak' { } ## Page breaks are not implemented for Xml output
                        'PScribo.LineBreak' { } ## Line breaks are not implemented for Xml output
                        'PScribo.BlankLine' { } ## Blank lines are not implemented for Xml output
                        'PScribo.TOC' { } ## TOC is not implemented for Xml output
                        Default {
                            WriteLog -Message ($localized.PluginUnsupportedSection -f $s.Type) -IsWarning;
                        }
                    } #end switch
                } #end foreach
                return $element;
            } #end process
        } #end function outxmlsection

        function OutXmlParagraph {
            <#
            .SYNOPSIS
                Output formatted Xml paragraph.
            #>

            [CmdletBinding()]
            param (
                ## PScribo paragraph object
                [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [System.Object] $Paragraph
            )
            process {
                if (-not ([string]::IsNullOrEmpty($Paragraph.Value))) {
                    ## Value override specified
                    $paragraphId = ($Paragraph.Id -replace '[^a-z0-9-_\.]','').ToLower();
                    $paragraphElement = $xmlDocument.CreateElement($paragraphId);
                    [ref] $null = $paragraphElement.AppendChild($xmlDocument.CreateTextNode($Paragraph.Value));
                } #end if
                elseif ([string]::IsNullOrEmpty($Paragraph.Text)) {
                    ## No Id/Name specified, therefore insert as a comment
                    $paragraphElement = $xmlDocument.CreateComment((' {0} ' -f $Paragraph.Id));
                } #end elseif
                else {
                    ## Create an element with the Id/Name
                    $paragraphId = ($Paragraph.Id -replace '[^a-z0-9-_\.]','').ToLower();
                    $paragraphElement = $xmlDocument.CreateElement($paragraphId);
                    [ref] $null = $paragraphElement.AppendChild($xmlDocument.CreateTextNode($Paragraph.Text));
                } #end else
                return $paragraphElement;
            } #end process
        } #end function outxmlparagraph

        function OutXmlTable {
            <#
            .SYNOPSIS
                Output formatted Xml table.
            #>

            [CmdletBinding()]
            param (
                ## PScribo table object
                [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [System.Object] $Table
            )
            process {
                $tableId = ($Table.Id -replace '[^a-z0-9-_\.]','').ToLower();
                $tableElement = $element.AppendChild($xmlDocument.CreateElement($tableId));
                [ref] $null = $tableElement.SetAttribute('name', $Table.Name);
                foreach ($row in $Table.Rows) {
                    $groupElement = $tableElement.AppendChild($xmlDocument.CreateElement('group'));
                    foreach ($property in $row.PSObject.Properties) {
                        if (-not ($property.Name).EndsWith('__Style')) {
                            $propertyId = ($property.Name -replace '[^a-z0-9-_\.]','').ToLower();
                            $rowElement = $groupElement.AppendChild($xmlDocument.CreateElement($propertyId));
                            ## Only add the Name attribute if there's a difference
                            if ($property.Name -ne $propertyId) {
                                [ref] $null = $rowElement.SetAttribute('name', $property.Name);
                            }
                            [ref] $null = $rowElement.AppendChild($xmlDocument.CreateTextNode($row.($property.Name)));
                        } #end if
                    } #end foreach property
                } #end foreach row
                return $tableElement;
            } #end process
        } #end outxmltable
        #endregion OutXml Private Functions

    }
    process {
        $pluginName = 'Xml';
        $stopwatch = [System.Diagnostics.Stopwatch]::StartNew();
        WriteLog -Message ($localized.DocumentProcessingStarted -f $Document.Name);
        $documentName = $Document.Name;

        $xmlDocument = New-Object -TypeName System.Xml.XmlDocument;
        [ref] $null = $xmlDocument.AppendChild($xmlDocument.CreateXmlDeclaration('1.0', 'utf-8', 'yes'));
        $documentId = ($Document.Id -replace '[^a-z0-9-_\.]','').ToLower();
        $element = $xmlDocument.AppendChild($xmlDocument.CreateElement($documentId));
        [ref] $null = $element.SetAttribute("name", $documentName);
        foreach ($s in $Document.Sections.GetEnumerator()) {
            if ($s.Id.Length -gt 40) { $sectionId = '{0}[..]' -f $s.Id.Substring(0,36); }
            else { $sectionId = $s.Id; }
            WriteLog -Message ($localized.PluginProcessingSection -f $s.Type, $sectionId) -Indent ($s.Level +1);
            switch ($s.Type) {
                'PScribo.Section' { [ref] $null = $element.AppendChild((OutXmlSection -Section $s)); }
                'PScribo.Paragraph' { [ref] $null = $element.AppendChild((OutXmlParagraph -Paragraph $s)); }
                'PScribo.Table' { [ref] $null = $element.AppendChild((OutXmlTable -Table $s)); }
                'PScribo.PageBreak'{ } ## Page breaks are not implemented for Xml output
                'PScribo.LineBreak' { } ## Line breaks are not implemented for Xml output
                'PScribo.BlankLine' { } ## Blank lines are not implemented for Xml output
                'PScribo.TOC' { } ## TOC is not implemented for Xml output
                Default {
                    WriteLog -Message ($localized.PluginUnsupportedSection -f $s.Type) -IsWarning;
                }
            } #end switch
        } #end foreach
        $stopwatch.Stop();
        WriteLog -Message ($localized.DocumentProcessingCompleted -f $Document.Name);
        $destinationPath = Join-Path $Path ('{0}.xml' -f $Document.Name);
        WriteLog -Message ($localized.SavingFile -f $destinationPath);
        $xmlDocument.Save($destinationPath);
        WriteLog -Message ($localized.TotalProcessingTime -f $stopwatch.Elapsed.TotalSeconds);
        ## Return the file reference to the pipeline
        Write-Output (Get-Item -Path $destinationPath);
    } #end process
} #end function outxml


#endregion PScribo Bundle v0.7.2.180