Src/Private/Add-NodeCard.ps1

function Add-NodeCard {
    <#
    .SYNOPSIS
        Creates a Graphviz HTML-like card node.
 
    .DESCRIPTION
        Creates a Graphviz HTML table that visually matches a ChartForgeX Card node: an optional
        icon, a title, an optional subtitle, and optional metadata rows. Use -NodeObject to add
        the card directly to the current PSGraph graph.
 
    .EXAMPLE
        Add-NodeCard -Name 'Web01' -Label 'Web-Server-01' -Subtitle 'Red Hat Linux 10' -IconPath ./Icons/Server.png -AdditionalInfo @{ IP = '10.0.0.1' } -NodeObject
    #>

    [CmdletBinding()]
    [OutputType([System.String])]
    param(
        [Parameter(Mandatory, HelpMessage = 'The Graphviz node name.')]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter(Mandatory, HelpMessage = 'The card title.')]
        [ValidateNotNullOrEmpty()]
        [string] $Label,

        [Parameter(HelpMessage = 'Secondary text displayed below the card title.')]
        [string] $Subtitle,

        [Parameter(HelpMessage = 'Path to the icon displayed on the left of the card title.')]
        [ValidateScript({
                if (Test-Path -Path $_ -PathType Leaf) {
                    $true
                } else {
                    throw "Icon file '$_' was not found."
                }
            })]
        [System.IO.FileInfo] $IconPath,

        [Parameter(HelpMessage = 'A hashtable, ordered dictionary, PSCustomObject, or array containing metadata rows.')]
        [Alias('AditionalInfo', 'Rows', 'RowsOrdered')]
        $AdditionalInfo,

        [Parameter(HelpMessage = 'The card title font size.')]
        [ValidateRange(1, 100)]
        [int] $FontSize = 14,

        [Parameter(HelpMessage = 'The card subtitle font size.')]
        [ValidateRange(1, 100)]
        [int] $SubtitleFontSize = 11,

        [Parameter(HelpMessage = 'The card font name.')]
        [string] $FontName = 'Segoe Ui',

        [Parameter(HelpMessage = 'The card text color.')]
        [string] $FontColor = '#000000',

        [Parameter(HelpMessage = 'The card background color.')]
        [string] $BackgroundColor = '#FFFFFF',

        [Parameter(HelpMessage = 'The card border color.')]
        [string] $BorderColor = '#71797E',

        [Parameter(HelpMessage = 'The card border width.')]
        [ValidateRange(0, 100)]
        [int] $Border = 1,

        [Parameter(HelpMessage = 'The padding inside card cells.')]
        [ValidateRange(0, 100)]
        [int] $CellPadding = 8,

        [Parameter(HelpMessage = 'The named port for edges attached to the card.')]
        [ValidateNotNullOrEmpty()]
        [string] $Port = 'EdgeDot',

        [Parameter(HelpMessage = 'Registers the card as a PSGraph node instead of returning HTML markup.')]
        [switch] $NodeObject,

        [Parameter(HelpMessage = 'Additional Graphviz attributes used when -NodeObject is specified.')]
        [hashtable] $GraphvizAttributes = @{}
    )

    process {
        $escape = [System.Security.SecurityElement]::Escape
        $escapedLabel = $escape.Invoke($Label)
        $title = '<FONT FACE="{0}" COLOR="{1}" POINT-SIZE="{2}"><B>{3}</B></FONT>' -f $escape.Invoke($FontName), $escape.Invoke($FontColor), $FontSize, $escapedLabel
        $subtitleMarkup = if ([string]::IsNullOrWhiteSpace($Subtitle)) {
            ''
        } else {
            '<BR/><FONT FACE="{0}" COLOR="{1}" POINT-SIZE="{2}">{3}</FONT>' -f $escape.Invoke($FontName), $escape.Invoke($FontColor), $SubtitleFontSize, $escape.Invoke($Subtitle)
        }

        $iconCell = if ($IconPath) {
            '<TD ALIGN="CENTER" VALIGN="MIDDLE"><IMG SRC="{0}"/></TD>' -f $escape.Invoke($IconPath.FullName)
        } else {
            ''
        }
        $columnCount = if ($IconPath) { 2 } else { 1 }
        $rows = '<TR>{0}<TD ALIGN="LEFT" VALIGN="MIDDLE">{1}{2}</TD></TR>' -f $iconCell, $title, $subtitleMarkup

        foreach ($item in @($AdditionalInfo)) {
            if ($null -eq $item) {
                continue
            }

            $entries = if ($item -is [System.Collections.IDictionary]) {
                $item.GetEnumerator()
            } elseif ($item -is [psobject]) {
                $item.PSObject.Properties | Where-Object { $_.MemberType -in 'NoteProperty', 'Property' }
            } else {
                throw "AdditionalInfo item '$item' must be a dictionary or object with properties."
            }

            foreach ($entry in $entries) {
                $key = if ($entry -is [System.Collections.DictionaryEntry]) { $entry.Key } else { $entry.Name }
                $value = if ($entry -is [System.Collections.DictionaryEntry]) { $entry.Value } else { $entry.Value }
                $rows += '<TR><TD COLSPAN="{0}" ALIGN="LEFT"><FONT FACE="{1}" COLOR="{2}" POINT-SIZE="{3}">{4}: {5}</FONT></TD></TR>' -f $columnCount, $escape.Invoke($FontName), $escape.Invoke($FontColor), $SubtitleFontSize, $escape.Invoke([string] $key), $escape.Invoke([string] $value)
            }
        }

        $html = '<TABLE PORT="{0}" STYLE="ROUNDED" BORDER="{1}" CELLBORDER="0" CELLSPACING="0" CELLPADDING="{2}" BGCOLOR="{3}" COLOR="{4}">{5}</TABLE>' -f $escape.Invoke($Port), $Border, $CellPadding, $escape.Invoke($BackgroundColor), $escape.Invoke($BorderColor), $rows
        Format-NodeObject -Name $Name -HtmlObject $html -GraphvizAttributes $GraphvizAttributes -AsHtml:(-not $NodeObject)
    }
}