Private/Show-ContentSlide.ps1
|
function Show-ContentSlide { <# .SYNOPSIS Renders a content slide with optional header and body text. .DESCRIPTION Displays a content slide that may contain a ### heading rendered as figlet text followed by body content. This is the most common slide type for presenting information with structured content. The rendering process: 1. Detects optional ### heading and extracts text 2. Parses body content for code blocks, images, and text segments 3. Filters progressive bullets (*) based on VisibleBullets parameter 4. Converts markdown formatting to Spectre Console markup 5. Renders content in a bordered panel with proper spacing Progressive bullets (*) are revealed one at a time during navigation, while regular bullets (-) appear all at once. Code blocks and images are preserved during bullet filtering to prevent display issues. Content headers use the configured h3 font setting and support color overrides via HTML color tags in the heading text. .PARAMETER Slide The slide object containing the content to render. Must include Content property with the markdown text and Number property for identification. .PARAMETER Settings The presentation settings hashtable containing: - foreground: Default text color - background: Slide background color - border: Border color - borderStyle: Border style (rounded, square, double, heavy, none) - h3: Font name for ### headings (default: 'mini') - h3Color: Optional color override for ### headings .PARAMETER VisibleBullets The number of progressive bullets (*) to reveal. Use this to implement step-by-step bullet reveal during presentation navigation. Default is [int]::MaxValue (all bullets shown). Only affects lines starting with '* ' (asterisk bullets). Regular bullets starting with '- ' (dash bullets) are always shown. .PARAMETER CurrentSlide The current slide number for pagination display. Default is 1. .PARAMETER TotalSlides The total number of slides in the presentation for pagination. Default is 1. .EXAMPLE Show-ContentSlide -Slide $slideObject -Settings $settings Renders a content slide with all bullets visible using presentation settings. .EXAMPLE Show-ContentSlide -Slide $slideObject -Settings $settings -VisibleBullets 2 Renders a content slide showing only the first 2 progressive bullets. Additional bullets are hidden until navigation reveals them. .EXAMPLE $slide = [PSCustomObject]@{ Number = 3 Content = @' ### Features * First feature (progressive) * Second feature (progressive) - Always visible bullet ```powershell Get-Process | Select-Object Name ``` '@ } Show-ContentSlide -Slide $slide -Settings $settings -VisibleBullets 1 Demonstrates progressive bullet reveal with code block. Only first * bullet is visible, while - bullet and code block always appear. .OUTPUTS None. Renders directly to the terminal console using PwshSpectreConsole. .NOTES Bullet Reveal Mechanism: - Progressive bullets (*): Revealed one at a time based on VisibleBullets - Regular bullets (-): Always displayed immediately - Code blocks and images: Never filtered, always visible Heading Color Support: - Inline color tags: <red>Heading</red> or <span style="color:red">Heading</span> - Frontmatter override: h3Color setting in YAML - Fallback: Uses foreground color from settings Content Parsing: - Markdown formatting converted to Spectre markup (bold, italic, code, strikethrough) - Code blocks syntax highlighted when language specified - Images rendered inline when supported by terminal Font Aliases: - h3, headerFont, h3Font all map to the same font setting #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [PSCustomObject]$Slide, [Parameter(Mandatory = $true)] [hashtable]$Settings, [Parameter(Mandatory = $false)] [int]$VisibleBullets = [int]::MaxValue, [Parameter(Mandatory = $false)] [int]$CurrentSlide = 1, [Parameter(Mandatory = $false)] [int]$TotalSlides = 1 ) begin { Write-Verbose "Rendering content slide #$($Slide.Number)" } process { try { # Get terminal dimensions $dimensions = Get-TerminalDimensions $windowWidth = $dimensions.Width $windowHeight = $dimensions.Height # Determine if slide has a header and extract content $hasHeader = $false $headerText = $null $bodyContent = $null if ($Slide.Content -match '^###\s+(.+?)(?:\r?\n|$)') { $hasHeader = $true $headerText = $Matches[1].Trim() Write-Verbose " Header: $headerText" # Check for color tags in heading text and extract color $headingColor = $null if ($headerText -match '<(\w+)>.*?</\1>') { $headingColor = $Matches[1] Write-Verbose " Extracted color from tag: $headingColor" } elseif ($headerText -match "<span\s+style=['""]color:(\w+)['""]>.*?</span>") { $headingColor = $Matches[1] Write-Verbose " Extracted color from span: $headingColor" } # Strip HTML tags from header text $headerText = $headerText -replace "<span\s+style=['""]color:\w+['""]>(.*?)</span>", '$1' $headerText = $headerText -replace '<(\w+)>(.*?)</\1>', '$2' # Extract content after header $bodyContent = $Slide.Content -replace '^###\s+.+?(\r?\n|$)', '' $bodyContent = $bodyContent.Trim() } else { # No header, use all content $bodyContent = $Slide.Content.Trim() } # Parse code blocks and images FIRST before any bullet filtering $codeBlockPattern = '(?s)```(\w+)?\r?\n(.*?)\r?\n```' $imagePattern = '!\[([^\]]*)\]\(([^)]+)\)(?:\{width=(\d+)\})?' $segments = [System.Collections.Generic.List[object]]::new() $lastIndex = 0 $progressiveBulletCount = 0 if ($bodyContent) { # Collect all matches (code blocks and images) and sort by position $allMatches = [System.Collections.Generic.List[object]]::new() foreach ($match in [regex]::Matches($bodyContent, $codeBlockPattern)) { $allMatches.Add(@{ Type = 'Code' Match = $match Index = $match.Index Length = $match.Length Language = $match.Groups[1].Value Content = $match.Groups[2].Value.Trim() }) } foreach ($match in [regex]::Matches($bodyContent, $imagePattern)) { $width = if ($match.Groups[3].Success) { [int]$match.Groups[3].Value } else { 0 } $allMatches.Add(@{ Type = 'Image' Match = $match Index = $match.Index Length = $match.Length AltText = $match.Groups[1].Value Path = $match.Groups[2].Value Width = $width }) } # Sort by position in document $allMatches = $allMatches | Sort-Object -Property Index # Build segments foreach ($item in $allMatches) { # Add text before this item if ($item.Index -gt $lastIndex) { $textBefore = $bodyContent.Substring($lastIndex, $item.Index - $lastIndex).Trim() if ($textBefore) { $segments.Add(@{ Type = 'Text'; Content = $textBefore }) } } # Add the item (code or image) if ($item.Type -eq 'Code') { $segments.Add(@{ Type = 'Code'; Language = $item.Language; Content = $item.Content }) } else { $segments.Add(@{ Type = 'Image'; AltText = $item.AltText; Path = $item.Path; Width = $item.Width }) } $lastIndex = $item.Index + $item.Length } # Add remaining text after last item if ($lastIndex -lt $bodyContent.Length) { $textAfter = $bodyContent.Substring($lastIndex).Trim() if ($textAfter) { $segments.Add(@{ Type = 'Text'; Content = $textAfter }) } } # If no code blocks or images found, treat entire content as text if ($segments.Count -eq 0) { $segments.Add(@{ Type = 'Text'; Content = $bodyContent }) } # Now filter bullets ONLY in text segments # Use a single counter across all segments to track visible bullets $filteredSegments = [System.Collections.Generic.List[object]]::new() $globalVisibleBullets = 0 foreach ($segment in $segments) { if ($segment.Type -eq 'Code' -or $segment.Type -eq 'Image') { # Code blocks and images pass through unchanged $filteredSegments.Add($segment) } else { # Filter bullets in text segments $lines = $segment.Content -split "`r?`n" $filteredLines = [System.Collections.Generic.List[string]]::new() foreach ($line in $lines) { # Check if line is a progressive bullet (*) if ($line -match '^\s*\*\s+') { $progressiveBulletCount++ if ($globalVisibleBullets -lt $VisibleBullets) { $filteredLines.Add($line) $globalVisibleBullets++ } else { # Add blank line placeholder for hidden progressive bullets $filteredLines.Add("") } } else { # All other lines (including - bullets) are always shown $filteredLines.Add($line) } } $filteredSegments.Add(@{ Type = 'Text'; Content = ($filteredLines -join "`n") }) } } # Store total progressive bullet count on the slide object for navigation if (-not $Slide.PSObject.Properties['TotalProgressiveBullets']) { Add-Member -InputObject $Slide -NotePropertyName 'TotalProgressiveBullets' -NotePropertyValue $progressiveBulletCount -Force } # Store the full content height for consistent vertical alignment if (-not $Slide.PSObject.Properties['FullContentHeight']) { $fullText = [Spectre.Console.Text]::new($bodyContent) $fullSize = Get-SpectreRenderableSize -Renderable $fullText -ContainerWidth $windowWidth Add-Member -InputObject $Slide -NotePropertyName 'FullContentHeight' -NotePropertyValue $fullSize.Height -Force } # Store the max line length of all content for consistent horizontal alignment if (-not $Slide.PSObject.Properties['MaxLineLength']) { $lines = $bodyContent -split "`r?`n" # Strip HTML color tags before measuring length (rendered length, not raw) $maxLength = ($lines | ForEach-Object { # Remove <colorname>text</colorname> tags $stripped = $_ -replace '<([a-zA-Z][a-zA-Z0-9]*)>(.*?)</\1>', '$2' # Remove <span style="color:colorname">text</span> tags $stripped = $stripped -replace '<span\s+style=[''"]color:\s*([a-zA-Z][a-zA-Z0-9]*)[''"]>(.*?)</span>', '$2' $stripped.Length } | Measure-Object -Maximum).Maximum Add-Member -InputObject $Slide -NotePropertyName 'MaxLineLength' -NotePropertyValue $maxLength -Force } } # Get border and colors $borderInfo = Get-BorderStyleFromSettings -Settings $Settings $colorName = if ($hasHeader -and $headingColor) { $headingColor } elseif ($Settings.h3Color) { $Settings.h3Color } else { $Settings.foreground } $figletColor = Get-SpectreColorFromSettings -ColorName $colorName -SettingName 'Figlet' # Build the renderable content $renderables = [System.Collections.Generic.List[object]]::new() # Add header figlet if present if ($hasHeader) { # Create header figlet with optional font from settings $figletParams = @{ Text = $headerText Color = $figletColor Justification = 'Center' } # Default to 'mini' font if h3 is 'default', otherwise use specified font $fontName = if ($Settings.h3 -eq 'default') { 'mini' } else { $Settings.h3 } $fontPath = if (Test-Path $fontName) { $fontName } else { Join-Path $PSScriptRoot "../Fonts/$fontName.flf" } if (Test-Path $fontPath) { $figletParams['FontPath'] = $fontPath Write-Verbose " Using h3 font: $fontName" } $figlet = New-FigletText @figletParams $renderables.Add($figlet) } # Add body content with code block and image support if ($bodyContent) { # Render each segment (already parsed and filtered above) foreach ($segment in $filteredSegments) { if ($segment.Type -eq 'Code') { # Render code block in a panel with dark background Write-Verbose " Code block: $($segment.Language)" # Create markup text for the code $codeMarkup = [Spectre.Console.Markup]::Escape($segment.Content) $codeText = [Spectre.Console.Markup]::new("$codeMarkup") # Put code in a panel $codePanel = [Spectre.Console.Panel]::new($codeText) $codePanel.Border = [Spectre.Console.BoxBorder]::Rounded $codePanel.Padding = [Spectre.Console.Padding]::new(2, 1, 2, 1) # Add language label if specified if ($segment.Language) { $codePanel.Header = [Spectre.Console.PanelHeader]::new($segment.Language) } # Center code blocks in single-column content slides $centeredCodePanel = Format-SpectreAligned -Data $codePanel -HorizontalAlignment Center $renderables.Add($centeredCodePanel) } elseif ($segment.Type -eq 'Image') { # Render image Write-Verbose " Image: $($segment.Path)" try { # Resolve relative paths $imagePath = $segment.Path if (-not [System.IO.Path]::IsPathRooted($imagePath)) { $markdownDir = Split-Path -Parent $Slide.SourceFile $imagePath = Join-Path $markdownDir $imagePath } # Calculate max width (default to 80% of available width) $availableWidth = $windowWidth - 8 # Account for panel padding $maxWidth = if ($segment.Width -gt 0) { [math]::Min($segment.Width, $availableWidth) } else { [math]::Floor($availableWidth * 0.8) } # Load and render image $image = Get-SpectreImage -ImagePath $imagePath -MaxWidth $maxWidth # Center the image $centeredImage = Format-SpectreAligned -Data $image -HorizontalAlignment Center $renderables.Add($centeredImage) } catch { # Show alt text in a styled box on failure Write-Warning "Failed to load image: $($segment.Path) - $($_.Exception.Message)" $altText = if ($segment.AltText) { $segment.AltText } else { "Image not available" } $errorMarkup = [Spectre.Console.Markup]::new("[yellow]$([Spectre.Console.Markup]::Escape($altText))[/]") $errorPanel = [Spectre.Console.Panel]::new($errorMarkup) $errorPanel.Border = [Spectre.Console.BoxBorder]::Rounded $errorPanel.Header = [Spectre.Console.PanelHeader]::new("Image Failed") $errorPanel.Padding = [Spectre.Console.Padding]::new(2, 1, 2, 1) $centeredError = Format-SpectreAligned -Data $errorPanel -HorizontalAlignment Center $renderables.Add($centeredError) } } else { # Render text content with centering $lines = $segment.Content -split "`r?`n" # Use stored max line length for consistent alignment during bullet reveal if ($Slide.PSObject.Properties['MaxLineLength']) { $maxLineLength = $Slide.MaxLineLength } else { $maxLineLength = ($lines | Measure-Object -Property Length -Maximum).Maximum } # Calculate padding to center the block within the panel $availableWidth = $windowWidth - 8 # Account for panel padding (4 left + 4 right) $leftPadding = [math]::Max(0, [math]::Floor(($availableWidth - $maxLineLength) / 2)) # Convert markdown formatting to Spectre markup $convertedLines = $lines | ForEach-Object { ConvertTo-SpectreMarkup -Text $_ } # Rebuild content with padding $paddedLines = $convertedLines | ForEach-Object { (" " * $leftPadding) + $_ } $paddedContent = $paddedLines -join "`n" # Create markup text instead of plain text to support formatting $text = [Spectre.Console.Markup]::new($paddedContent) $renderables.Add($text) } } } # Combine renderables into a Rows layout $rows = [Spectre.Console.Rows]::new([object[]]$renderables.ToArray()) # Measure the actual height of the rendered content # (blank placeholder lines maintain consistent height for progressive bullets) # Account for horizontal padding (4 left + 4 right = 8 total) $availableWidth = $windowWidth - 8 $contentSize = Get-SpectreRenderableSize -Renderable $rows -ContainerWidth $availableWidth $actualContentHeight = $contentSize.Height # Calculate padding - be more conservative with bottom padding for images # Sixel images may need extra space that isn't accounted for in the size calculation $borderHeight = 2 $remainingSpace = $windowHeight - $actualContentHeight - $borderHeight # If slide contains images, reduce bottom padding slightly to prevent cutoff $hasImages = $filteredSegments | Where-Object { $_.Type -eq 'Image' } if ($hasImages) { # Add buffer for image rendering $remainingSpace = $remainingSpace - 1 } $topPadding = [math]::Max(0, [math]::Ceiling($remainingSpace / 2.0)) $bottomPadding = [math]::Max(1, $remainingSpace - $topPadding) Write-Verbose " Content height: $actualContentHeight, top padding: $topPadding, bottom padding: $bottomPadding" # Create panel with internal padding $panel = [Spectre.Console.Panel]::new($rows) $panel.Expand = $true $panel.Padding = [Spectre.Console.Padding]::new(4, $topPadding, 4, $bottomPadding) # Add border style and color if ($borderInfo.Style) { $panel.Border = [Spectre.Console.BoxBorder]::$($borderInfo.Style) } if ($borderInfo.Color) { $panel.BorderStyle = [Spectre.Console.Style]::new($borderInfo.Color) } # Add pagination header if enabled if ($Settings.pagination -eq $true) { $paginationParams = @{ CurrentSlide = $CurrentSlide TotalSlides = $TotalSlides Style = $Settings.paginationStyle } if ($borderInfo.Color) { $paginationParams['Color'] = $borderInfo.Color } $paginationText = Get-PaginationText @paginationParams $panel.Header = [Spectre.Console.PanelHeader]::new($paginationText) $panel.Header.Justification = [Spectre.Console.Justify]::Right } # Render panel Out-SpectreHost $panel } catch { $errorRecord = [System.Management.Automation.ErrorRecord]::new( $_.Exception, 'ContentSlideRenderFailed', [System.Management.Automation.ErrorCategory]::InvalidOperation, $Slide ) $PSCmdlet.ThrowTerminatingError($errorRecord) } } end { Write-Verbose "Content slide rendered" } } |