Private/Console/Figlet.psm1
|
using namespace System using namespace System.Collections.Generic using namespace System.Text using namespace System.IO using module ..\Enums.psm1 using module ..\Abstracts.psm1 using module .\Colors.psm1 using module .\Rendering.psm1 # --------------------------------------------------------------------------- # FigletHeader – parsed from the first line of a .flf font file # --------------------------------------------------------------------------- class FigletHeader { [char]$Hardblank [int]$Height [int]$Baseline [int]$MaxLength [int]$OldLayout [int]$CommentLines [int]$FullLayout # 0 means "not present" [bool]$HasFullLayout } # --------------------------------------------------------------------------- # FigletCharacter – one glyph from the font # --------------------------------------------------------------------------- class FigletCharacter { [int]$Code [int]$Width [int]$Height [string[]]$Lines FigletCharacter([int]$code, [string[]]$lines) { $this.Code = $code $this.Lines = $lines $max = 0 foreach ($l in $lines) { if ($l.Length -gt $max) { $max = $l.Length } } $this.Width = $max $this.Height = $lines.Length } } # --------------------------------------------------------------------------- # FigletFontParser – reads .flf source text and builds a FigletFont # --------------------------------------------------------------------------- class FigletFontParser { static [FigletFont] Parse([string]$source) { $lines = $source -split "`r?`n" if ($lines.Length -eq 0) { throw 'Could not read header line' } [FigletHeader]$header = [FigletFontParser]::ParseHeader($lines[0]) [char]$eolMarker = [FigletFontParser]::ParseEndOfLineMarker($lines, $header) $index = 32 $indexOverridden = $false $hasOverriddenIndex = $false $buffer = [List[string]]::new() $characters = [List[FigletCharacter]]::new() $accumulatedHeight = 0 for ($i = $header.CommentLines + 1; $i -lt $lines.Length; $i++) { $line = $lines[$i] if ($null -eq $line) { continue } # Check if this is a Unicode index override line (no eolMarker at end) if (-not $line.EndsWith($eolMarker.ToString())) { $words = $line.Trim() -split '\s+' $newIndex = 0 if ($words.Length -gt 0 -and [FigletFontParser]::TryParseIndex($words[0], [ref]$newIndex)) { $index = $newIndex $indexOverridden = $true $hasOverriddenIndex = $true $accumulatedHeight = 0 continue } } if ($hasOverriddenIndex -and -not $indexOverridden) { continue } $buffer.Add($line.TrimEnd([char[]]@($eolMarker))) $accumulatedHeight++ if ($accumulatedHeight -eq $header.Height) { $characters.Add([FigletCharacter]::new($index, $buffer.ToArray())) $buffer.Clear() if (-not $hasOverriddenIndex) { $index++ } $indexOverridden = $false $accumulatedHeight = 0 } } return [FigletFont]::new($characters, $header) } static hidden [char] ParseEndOfLineMarker([string[]]$lines, [FigletHeader]$header) { $idx = $header.CommentLines + 1 if ($idx -lt $lines.Length) { $first = $lines[$idx].Trim() if ($first.Length -gt 0) { return $first[$first.Length - 1] } } return '@' } static hidden [bool] TryParseIndex([string]$indexStr, [ref]$result) { $style = [Globalization.NumberStyles]::Integer if ($indexStr.StartsWith('0x', [StringComparison]::OrdinalIgnoreCase)) { $indexStr = $indexStr.Substring(2) $style = [Globalization.NumberStyles]::HexNumber } $val = 0 if ([int]::TryParse($indexStr, $style, [Globalization.CultureInfo]::InvariantCulture, [ref]$val)) { $result.Value = $val return $true } return $false } static hidden [FigletHeader] ParseHeader([string]$text) { if ([string]::IsNullOrWhiteSpace($text)) { throw 'Invalid Figlet font' } $parts = $text -split ' +' | Where-Object { $_ -ne '' } if ($parts.Length -lt 6) { throw 'Invalid Figlet font header' } if (-not [FigletFontParser]::IsValidSignature($parts[0])) { throw 'Invalid Figlet font header signature' } $h = [FigletHeader]::new() $h.Hardblank = $parts[0][5] $h.Height = [int]::Parse($parts[1], [Globalization.CultureInfo]::InvariantCulture) $h.Baseline = [int]::Parse($parts[2], [Globalization.CultureInfo]::InvariantCulture) $h.MaxLength = [int]::Parse($parts[3], [Globalization.CultureInfo]::InvariantCulture) $h.OldLayout = [int]::Parse($parts[4], [Globalization.CultureInfo]::InvariantCulture) $h.CommentLines = [int]::Parse($parts[5], [Globalization.CultureInfo]::InvariantCulture) if ($parts.Length -gt 7) { $fl = 0 if ([int]::TryParse($parts[7], [Globalization.NumberStyles]::Integer, [Globalization.CultureInfo]::InvariantCulture, [ref]$fl)) { $h.FullLayout = $fl $h.HasFullLayout = $true } } return $h } static hidden [bool] IsValidSignature([string]$s) { return $s.Length -ge 6 -and $s[0] -eq 'f' -and $s[1] -eq 'l' -and $s[2] -eq 'f' -and $s[3] -eq '2' -and $s[4] -eq 'a' } } # --------------------------------------------------------------------------- # FigletFont – holds all parsed glyphs + metadata # --------------------------------------------------------------------------- # .EXAMPLE # # Get the default font # [FigletFont]::Default() # .EXAMPLE # # Get a specific font # [FigletFont]"DEFAULT_3D" # .EXAMPLE # # String Casting # "SWEET" -as [FigletFont] # .EXAMPLE # # Get all available fonts # [FigletFont]::GetAvailableFonts() # .EXAMPLE # # Load a font from a file # [FigletFont]::Load("fonts\standard.flf") # .EXAMPLE # # Render a string # [FigletFont]::Default().Render("Hello World") # .EXAMPLE # # Render a string with alignment # [FigletFont]::Default().Render("Hello World", "Center", "Middle") # .EXAMPLE # # Render a string with overflow # [FigletFont]::Default().Render("Hello World", "Center", "Middle", "Crop") class FigletFont { hidden [Dictionary[int, FigletCharacter]]$_characters # fonts are based on files in fonts folder # ie: ls fonts/ -file | % { ' static [FigletFont]$' + $_.BaseName + ' = ' + "`"$($_.BaseName.ToLowerInvariant())`"" } static [FigletFont]$ACROBATIC static [FigletFont]$ALLIGATOR static [FigletFont]$ALLIGATOR2 static [FigletFont]$ALLIGATOR3 static [FigletFont]$ALPHA static [FigletFont]$ALPHABET static [FigletFont]$AMC_3_LINE static [FigletFont]$AMC_3_LIV1 static [FigletFont]$AMC_AAA01 static [FigletFont]$AMC_NEKO static [FigletFont]$AMC_RAZOR static [FigletFont]$AMC_RAZOR2 static [FigletFont]$AMC_SLASH static [FigletFont]$AMC_SLIDER static [FigletFont]$AMC_THIN static [FigletFont]$AMC_TUBES static [FigletFont]$AMC_UNTITLED static [FigletFont]$AMC3LINE static [FigletFont]$AMC3LIV1 static [FigletFont]$AMCAAA01 static [FigletFont]$AMCNEKO static [FigletFont]$AMCRAZO2 static [FigletFont]$AMCRAZOR static [FigletFont]$AMCSLASH static [FigletFont]$AMCSLDER static [FigletFont]$AMCTHIN static [FigletFont]$AMCTUBES static [FigletFont]$AMCUN1 static [FigletFont]$ANSI_REGULAR static [FigletFont]$ANSI_SHADOW static [FigletFont]$ARROWS static [FigletFont]$ASCII_3D static [FigletFont]$ASCII_NEW_ROMAN_2 static [FigletFont]$ASCII_NEW_ROMAN static [FigletFont]$AVATAR static [FigletFont]$B1FF static [FigletFont]$BANNER static [FigletFont]$BANNER3_D static [FigletFont]$BANNER3 static [FigletFont]$BANNER4 static [FigletFont]$BARBWIRE static [FigletFont]$BASIC static [FigletFont]$BEAR static [FigletFont]$BELL static [FigletFont]$BENJAMIN static [FigletFont]$BIG_CHIEF static [FigletFont]$BIG_MONEY_NE static [FigletFont]$BIG_MONEY_NW static [FigletFont]$BIG_MONEY_SE static [FigletFont]$BIG_MONEY_SW static [FigletFont]$BIG static [FigletFont]$BIGCHIEF static [FigletFont]$BIGFIG static [FigletFont]$BINARY static [FigletFont]$BLOCK static [FigletFont]$BLOCKS static [FigletFont]$BLOODY static [FigletFont]$BOLGER static [FigletFont]$BRACED static [FigletFont]$BRIGHT static [FigletFont]$BROADWAY_KB_2 static [FigletFont]$BROADWAY_KB static [FigletFont]$BROADWAY static [FigletFont]$BUBBLE static [FigletFont]$BULBHEAD static [FigletFont]$CALGPHY2 static [FigletFont]$CALIGRAPHY static [FigletFont]$CALIGRAPHY2 static [FigletFont]$CALVIN_S static [FigletFont]$CARDS static [FigletFont]$CATWALK static [FigletFont]$CHISELED static [FigletFont]$CHUNKY static [FigletFont]$COINSTAK static [FigletFont]$COLA static [FigletFont]$COLOSSAL static [FigletFont]$COMPUTER static [FigletFont]$CONTESSA static [FigletFont]$CONTRAST static [FigletFont]$COSMIC static [FigletFont]$COSMIKE static [FigletFont]$CRAWFORD static [FigletFont]$CRAWFORD2 static [FigletFont]$CRAZY static [FigletFont]$CRICKET static [FigletFont]$CURSIVE static [FigletFont]$CYBERLARGE static [FigletFont]$CYBERMEDIUM static [FigletFont]$CYBERSMALL static [FigletFont]$CYGNET static [FigletFont]$DANC4 static [FigletFont]$DANCING_FONT static [FigletFont]$DANCINGFONT static [FigletFont]$DECIMAL static [FigletFont]$DEF_LEPPARD static [FigletFont]$DEFAULT_3D static [FigletFont]$DEFLEPPARD static [FigletFont]$DELTA_CORPS_PRIEST_1 static [FigletFont]$DIAGONAL_3D_2 static [FigletFont]$DIAGONAL_3D static [FigletFont]$DIAMOND static [FigletFont]$DIET_COLA static [FigletFont]$DIETCOLA static [FigletFont]$DIGITAL static [FigletFont]$DOH static [FigletFont]$DOOM static [FigletFont]$DOS_REBEL static [FigletFont]$DOSREBEL static [FigletFont]$DOT_MATRIX static [FigletFont]$DOTMATRIX static [FigletFont]$DOUBLE_SHORTS static [FigletFont]$DOUBLE static [FigletFont]$DOUBLESHORTS static [FigletFont]$DR_PEPPER static [FigletFont]$DRPEPPER static [FigletFont]$DWHISTLED static [FigletFont]$EFTI_CHESS static [FigletFont]$EFTI_FONT static [FigletFont]$EFTI_ITALIC static [FigletFont]$EFTI_PITI static [FigletFont]$EFTI_ROBOT static [FigletFont]$EFTI_WALL static [FigletFont]$EFTI_WATER static [FigletFont]$EFTICHESS static [FigletFont]$EFTIFONT static [FigletFont]$EFTIPITI static [FigletFont]$EFTIROBOT static [FigletFont]$EFTITALIC static [FigletFont]$EFTIWALL static [FigletFont]$EFTIWATER static [FigletFont]$ELECTRONIC static [FigletFont]$ELITE static [FigletFont]$EPIC static [FigletFont]$FENDER static [FigletFont]$FILTER_1 static [FigletFont]$FIRE_FONT_K static [FigletFont]$FIRE_FONT_S static [FigletFont]$FLIPPED static [FigletFont]$FLOWER_POWER static [FigletFont]$FLOWERPOWER static [FigletFont]$FOUR_MAX static [FigletFont]$FOUR_TOPS static [FigletFont]$FOURTOPS static [FigletFont]$FRAKTUR static [FigletFont]$FUN_FACE static [FigletFont]$FUN_FACES static [FigletFont]$FUNFACE static [FigletFont]$FUNFACES static [FigletFont]$FUZZY static [FigletFont]$GEORGI16 static [FigletFont]$GEORGIA11 static [FigletFont]$GHOST static [FigletFont]$GHOULISH static [FigletFont]$GLENYN static [FigletFont]$GOOFY static [FigletFont]$GOTHIC static [FigletFont]$GRACEFUL static [FigletFont]$GRADIENT static [FigletFont]$GRAFFITI static [FigletFont]$GREEK static [FigletFont]$HALFIWI static [FigletFont]$HEART_LEFT static [FigletFont]$HEART_RIGHT static [FigletFont]$HENRY_3D static [FigletFont]$HENRY3D static [FigletFont]$HEX static [FigletFont]$HIEROGLYPHS static [FigletFont]$HOLLYWOOD static [FigletFont]$HORIZONTAL_LEFT static [FigletFont]$HORIZONTAL_RIGHT static [FigletFont]$HORIZONTALLEFT static [FigletFont]$HORIZONTALRIGHT static [FigletFont]$ICL_1900 static [FigletFont]$IMPOSSIBLE static [FigletFont]$INVITA static [FigletFont]$ISOMETRIC1 static [FigletFont]$ISOMETRIC2 static [FigletFont]$ISOMETRIC3 static [FigletFont]$ISOMETRIC4 static [FigletFont]$ITALIC static [FigletFont]$IVRIT static [FigletFont]$JACKY static [FigletFont]$JAZMINE static [FigletFont]$JERUSALEM static [FigletFont]$JS_BLOCK_LETTERS static [FigletFont]$JS_BRACKET_LETTERS static [FigletFont]$JS_CAPITAL_CURVES static [FigletFont]$JS_CURSIVE static [FigletFont]$JS_STICK_LETTERS static [FigletFont]$KATAKANA static [FigletFont]$KBAN static [FigletFont]$KEYBOARD static [FigletFont]$KNOB static [FigletFont]$KOHOLINT static [FigletFont]$KOMPAKTBLK static [FigletFont]$KONTO_SLANT static [FigletFont]$KONTO static [FigletFont]$KONTOSLANT static [FigletFont]$LARRY_3D_2 static [FigletFont]$LARRY_3D static [FigletFont]$LARRY3D static [FigletFont]$LCD static [FigletFont]$LEAN static [FigletFont]$LETTERS static [FigletFont]$LIL_DEVIL static [FigletFont]$LILDEVIL static [FigletFont]$LINE_BLOCKS static [FigletFont]$LINEBLOCKS static [FigletFont]$LINES_3D static [FigletFont]$LINUX static [FigletFont]$LOCKERGNOME static [FigletFont]$MADRID static [FigletFont]$MARQUEE static [FigletFont]$MAXFOUR static [FigletFont]$MAXIWI static [FigletFont]$MERLIN1 static [FigletFont]$MERLIN2 static [FigletFont]$MIKE static [FigletFont]$MINI static [FigletFont]$MINIWI static [FigletFont]$MIRROR static [FigletFont]$MNEMONIC static [FigletFont]$MODULAR static [FigletFont]$MONO9 static [FigletFont]$MORSE static [FigletFont]$MORSE2 static [FigletFont]$MOSCOW static [FigletFont]$MSHEBREW210 static [FigletFont]$MUZZLE static [FigletFont]$NANCYJ_FANCY static [FigletFont]$NANCYJ_IMPROVED static [FigletFont]$NANCYJ_UNDERLINED static [FigletFont]$NANCYJ static [FigletFont]$NIPPLES static [FigletFont]$NSCRIPT static [FigletFont]$NT_GREEK static [FigletFont]$NTGREEK static [FigletFont]$NV_SCRIPT static [FigletFont]$O8 static [FigletFont]$OBLIQUE_5LINE_2 static [FigletFont]$OBLIQUE_5LINE static [FigletFont]$OCTAL static [FigletFont]$OGRE static [FigletFont]$OLD_BANNER static [FigletFont]$OLDBANNER static [FigletFont]$ONE_ROW static [FigletFont]$OS2 static [FigletFont]$PATORJK_HEX static [FigletFont]$PATORJKS_CHEESE static [FigletFont]$PAWP static [FigletFont]$PEAKS_SLANT static [FigletFont]$PEAKS static [FigletFont]$PEAKSSLANT static [FigletFont]$PEBBLES static [FigletFont]$PEPPER static [FigletFont]$PIXEL_3X5 static [FigletFont]$POISON static [FigletFont]$PUFFY static [FigletFont]$PUZZLE static [FigletFont]$PYRAMID static [FigletFont]$RAMMSTEIN static [FigletFont]$RECTANGLES static [FigletFont]$RED_PHOENIX static [FigletFont]$RELIEF static [FigletFont]$RELIEF2 static [FigletFont]$REV static [FigletFont]$REVERSE static [FigletFont]$ROMAN static [FigletFont]$ROT13 static [FigletFont]$ROTATED static [FigletFont]$ROUNDED static [FigletFont]$ROWAN_CAP static [FigletFont]$ROWANCAP static [FigletFont]$ROZZO static [FigletFont]$RUNIC static [FigletFont]$RUNYC static [FigletFont]$S_BLOOD static [FigletFont]$S_RELIEF static [FigletFont]$SANTA_CLARA static [FigletFont]$SANTACLARA static [FigletFont]$SBLOOD static [FigletFont]$SCRIPT static [FigletFont]$SERIFCAP static [FigletFont]$SHADOW static [FigletFont]$SHIMROD static [FigletFont]$SHORT static [FigletFont]$SIX_FO static [FigletFont]$SL_SCRIPT static [FigletFont]$SLANT_RELIEF static [FigletFont]$SLANT static [FigletFont]$SLIDE static [FigletFont]$SLSCRIPT static [FigletFont]$SMALL_CAPS static [FigletFont]$SMALL_ISOMETRIC1 static [FigletFont]$SMALL_KEYBOARD static [FigletFont]$SMALL_POISON static [FigletFont]$SMALL_SCRIPT static [FigletFont]$SMALL_SHADOW static [FigletFont]$SMALL_SLANT static [FigletFont]$SMALL_TENGWAR static [FigletFont]$SMALL static [FigletFont]$SMALLCAPS static [FigletFont]$SMISOME1 static [FigletFont]$SMKEYBOARD static [FigletFont]$SMPOISON static [FigletFont]$SMSCRIPT static [FigletFont]$SMSHADOW static [FigletFont]$SMSLANT static [FigletFont]$SMTENGWAR static [FigletFont]$SOFT static [FigletFont]$SPEED static [FigletFont]$SPLIFF static [FigletFont]$STACEY static [FigletFont]$STAMPATE static [FigletFont]$STAMPATELLO static [FigletFont]$STANDARD static [FigletFont]$STAR_STRIPS static [FigletFont]$STAR_WARS static [FigletFont]$STARSTRIPS static [FigletFont]$STARWARS static [FigletFont]$STELLAR static [FigletFont]$STENCIL static [FigletFont]$STFOREK static [FigletFont]$STICK_LETTERS static [FigletFont]$STOP static [FigletFont]$STRAIGHT static [FigletFont]$STRONGER_THAN_ALL static [FigletFont]$SUB_ZERO static [FigletFont]$SWAMP_LAND static [FigletFont]$SWAMPLAND static [FigletFont]$SWAN static [FigletFont]$SWEET static [FigletFont]$TANJA static [FigletFont]$TENGWAR static [FigletFont]$TERM static [FigletFont]$TERMINUS_DOTS static [FigletFont]$TERMINUS static [FigletFont]$TEST1 static [FigletFont]$THE_EDGE static [FigletFont]$THICK static [FigletFont]$THIN static [FigletFont]$THIS static [FigletFont]$THORNED static [FigletFont]$THREE_POINT static [FigletFont]$THREEPOINT static [FigletFont]$TICKS_SLANT static [FigletFont]$TICKS static [FigletFont]$TICKSSLANT static [FigletFont]$TILES static [FigletFont]$TINKER_TOY static [FigletFont]$TOMBSTONE static [FigletFont]$TRAIN static [FigletFont]$TREK static [FigletFont]$TSALAGI static [FigletFont]$TUBES_REGULAR static [FigletFont]$TUBES_SMUSHED static [FigletFont]$TUBULAR static [FigletFont]$TWISTED static [FigletFont]$TWO_POINT static [FigletFont]$TWOPOINT static [FigletFont]$UBLK static [FigletFont]$UNIVERS static [FigletFont]$USA_FLAG static [FigletFont]$USAFLAG static [FigletFont]$VARSITY static [FigletFont]$WAVY static [FigletFont]$WEIRD static [FigletFont]$WET_LETTER static [FigletFont]$WETLETTER static [FigletFont]$WHIMSY static [FigletFont]$WOW [int]$Height [int]$Baseline [int]$MaxWidth [char]$Hardblank [int]$SmushingRules FigletFont() { [void]$this.ToDefault() } FigletFont([string]$Name) { [ValidateNotNullOrWhiteSpace()][string]$Name = $Name [void][FigletFont]::From($Name, [ref]$this) } FigletFont([FigletFontName]$Name) { [void][FigletFont]::From($Name, [ref]$this) } FigletFont([List[FigletCharacter]]$characters, [FigletHeader]$header) { $this._characters = [Dictionary[int, FigletCharacter]]::new() foreach ($c in $characters) { $this._characters[$c.Code] = $c } $this.Height = $header.Height $this.Baseline = $header.Baseline $this.MaxWidth = $header.MaxLength $this.Hardblank = $header.Hardblank if ($header.HasFullLayout) { $this.SmushingRules = $header.FullLayout -band 63 } elseif ($header.OldLayout -gt 0) { $this.SmushingRules = $header.OldLayout -band 63 } else { $this.SmushingRules = 0 } } static [FigletFont] Create() { return [FigletFont]::new().ToDefault() } static [FigletFont] Create([string]$Name) { return [FigletFont]::new().To($Name) } static [FigletFont] Create([FigletFontName]$Name) { return [FigletFont]::new().To($Name) } [FigletFont] ToDefault() { return $this.To("STANDARD") } [FigletFont] To([FigletFontName]$Name) { return [FigletFont]::From($Name, [ref]$this) } static [FigletFont] From([FigletFontName]$Name, [ref]$o) { $font_flf = [FigletFont]::GetFontflfPath("$Name") $font = $null -ne [FigletFont]::$Name ? [FigletFont]::$Name : [FigletFont]::Load($font_flf) if ($o.Value -isnot [FigletFont]) { throw "$($o.Value.GetType().FullName) isnot [FigletFont]" } $o.Value._characters = $font._characters $o.Value.Height = $font.Height $o.Value.Baseline = $font.Baseline $o.Value.MaxWidth = $font.MaxWidth $o.Value.Hardblank = $font.Hardblank $o.Value.SmushingRules = $font.SmushingRules [FigletFont]::$Name = $o.Value return $o.Value } static [FigletFont] Default() { if ($null -eq [FigletFont]::STANDARD) { [FigletFont]::STANDARD = [FigletFont]::new("STANDARD") } return [FigletFont]::STANDARD } static [FigletFont] Load([string]$path) { return [FigletFont]::Load([System.IO.FileInfo]::new([PsModuleBase]::GetUnResolvedPath($path))) } static [FigletFont] Load([System.IO.FileInfo]$file) { if (![IO.File]::Exists($file.FullName)) { throw [FileNotFoundException]::new("Could not find font '$file'.") } $ext = $file.Extension.ToLowerInvariant() if ($ext -notin @(".flf", ".tlf")) { # Write-Host "[DEBUG] NOT A FONT FILE!." -f Yellow # skip this file return $null } return [FigletFont]::ParseFlfSource([IO.File]::ReadAllText($file.FullName)) } static [void] LoadAll() { $fonts = [FigletFont]::GetSupportedFontNames() foreach ($name in $fonts) { [FigletFont]::$name = [FigletFont]::new($name) } } static [string[]] GetSupportedFontNames() { [string[]]$names = [Enum]::GetNames([FigletFontName]) return [FigletFont].GetProperties().Where({ $_.PropertyType.Name -eq "FigletFont" -and $_.Name -in $names -and "HiddenAttribute" -notin $_.CustomAttributes.AttributeType.Name } ).Name } static [FigletFont] ParseFlfSource([string]$source) { return [FigletFontParser]::Parse($source) } [int] GetWidth([string]$text) { $sum = 0 foreach ($c in $text.ToCharArray()) { $fc = $this.GetCharacter([int][char]$c) if ($null -ne $fc) { $sum += $fc.Width } } return $sum } [List[FigletCharacter]] GetCharacters([string]$text) { $result = [List[FigletCharacter]]::new() foreach ($c in $text.ToCharArray()) { $fc = $this.GetCharacter([int][char]$c) if ($null -ne $fc) { $result.Add($fc) } } return $result } static [string] GetFontflfPath([string]$Name) { $fonts_in_repo = [IO.DirectoryInfo][IO.Path]::Combine((Resolve-Path .).Path, "Private", "fonts") $fonts_in_module_path = [IO.DirectoryInfo][IO.Path]::Combine((Get-InstalledModule cliHelper.core).InstalledLocation, "Private", "fonts") $font_flf_path = $(if ($fonts_in_repo.Exists) { [IO.Path]::Combine($fonts_in_repo.FullName, "$Name.flf") } elseif ($fonts_in_module_path.Exists) { [IO.Path]::Combine($fonts_in_module_path.FullName, "$Name.flf") } else { "$Name.flf" } ) if (![File]::Exists($font_flf_path)) { throw "Could not find $Name.flf font." } return $font_flf_path } hidden [FigletCharacter] GetCharacter([int]$code) { $fc = $null if ($this._characters.TryGetValue($code, [ref]$fc)) { return $fc } return $null } } # --------------------------------------------------------------------------- # FigletText – IRenderable that draws text using a FigletFont # --------------------------------------------------------------------------- class FigletText : IRenderable { hidden [FigletFont]$_font hidden [string]$_text static hidden [string]$OverscoreChars = '|/\[]{}()<>' [Color]$Color = [Color]::Default # use Default for "no colour" [object]$Justification = $null # [Justify] or $null for left [bool]$Pad = $false [FigletLayoutMode]$LayoutMode = [FigletLayoutMode]::FullSize FigletText([string]$text) { if ([string]::IsNullOrWhiteSpace($text)) { Write-Verbose "Warning! The figlet text is empty" } $this._font = [FigletFont]::Default() $this._text = if ($null -ne $text) { $text } else { '' } } FigletText([FigletFont]$font, [string]$text) { if ($null -eq $font) { throw [ArgumentNullException]::new('font') } $this._font = $font $this._text = if ($null -ne $text) { $text } else { '' } } [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) { $w = $this._font.GetWidth($this._text) $safe = [Math]::Min($w, $maxWidth) return [Measurement]::new($safe, $safe) } [Segment[]] Render([RenderOptions]$options, [int]$maxWidth) { $fg = if ($null -ne $this.Color -and -not $this.Color.IsDefault) { $this.Color } else { [Color]::Default } $style = [Style]::new($fg) $alignment = if ($null -ne $this.Justification) { $this.Justification } else { [Justify]::Left } $result = [List[Segment]]::new() foreach ($row in $this.GetRows($maxWidth)) { if ($row.Count -eq 0) { continue } # Pre-compute fit/smush junctions between adjacent glyphs $junctions = $null if ($this.LayoutMode -ne [FigletLayoutMode]::FullSize -and $row.Count -gt 1) { $junctions = [object[]]::new($row.Count - 1) $smushRules = if ($this.LayoutMode -eq [FigletLayoutMode]::Smushed) { $this._font.SmushingRules } else { 0 } for ($i = 0; $i -lt $row.Count - 1; $i++) { $junctions[$i] = [FigletText]::ComputeJunction( $row[$i], $row[$i + 1], $this.LayoutMode, $smushRules, $this._font.Hardblank) } } for ($lineIdx = 0; $lineIdx -lt $this._font.Height; $lineIdx++) { $lineText = if ($null -ne $junctions) { [FigletText]::BuildLine($row, $lineIdx, $junctions, $this._font.Hardblank) } else { $sb = [StringBuilder]::new() foreach ($fc in $row) { [void]$sb.Append($fc.Lines[$lineIdx]) } $sb.ToString() } # Replace hardblanks with real spaces if ($this._font.Hardblank -ne ' ') { $lineText = $lineText.Replace($this._font.Hardblank, ' ') } $line = [Segment]::new($lineText, $style) $lineWidth = $line.CellCount() switch ($alignment) { ([Justify]::Left) { $result.Add($line) if ($this.Pad -and $lineWidth -lt $maxWidth) { $result.Add([Segment]::Padding($maxWidth - $lineWidth)) } break } ([Justify]::Center) { $left = [Math]::Max(0, [Math]::Floor(($maxWidth - $lineWidth) / 2)) $right = [Math]::Max(0, $maxWidth - $lineWidth) - $left if ($left -gt 0) { $result.Add([Segment]::Padding($left)) } $result.Add($line) if ($this.Pad -and $right -gt 0) { $result.Add([Segment]::Padding($right)) } break } ([Justify]::Right) { if ($lineWidth -lt $maxWidth) { $result.Add([Segment]::Padding($maxWidth - $lineWidth)) } $result.Add($line) break } default { throw [NotSupportedException]::new("Invalid alignment mode: $alignment") } } $result.Add([Segment]::LineBreak) } } return $result.ToArray() } # ---- Internal helpers -------------------------------------------------- hidden [List[List[FigletCharacter]]] GetRows([int]$maxWidth) { $result = [List[List[FigletCharacter]]]::new() $words = $this._text -split '(?<=\s)(?=\S)|(?<=\S)(?=\s)' | Where-Object { $_ -ne '' } $line = [List[FigletCharacter]]::new() $totalWidth = 0 foreach ($word in $words) { $chars = $this._font.GetCharacters($word) $width = 0 foreach ($fc in $chars) { $width += $fc.Width } if ($totalWidth + $width -le $maxWidth) { $line.AddRange($chars) $totalWidth += $width } else { if ($width -le $maxWidth) { if ($line.Count -gt 0) { $result.Add($line) } $line = [List[FigletCharacter]]::new($chars) $totalWidth = $width } else { # Word is wider than maxWidth: split character by character $queue = [Queue[FigletCharacter]]::new($chars) while ($queue.Count -gt 0) { $cur = $queue.Dequeue() if ($totalWidth + $cur.Width -gt $maxWidth -and $line.Count -gt 0) { $result.Add($line) $line = [List[FigletCharacter]]::new() $totalWidth = 0 } $line.Add($cur) $totalWidth += $cur.Width } } } } if ($line.Count -gt 0) { $result.Add($line) } return $result } static hidden [hashtable] ComputeJunction( [FigletCharacter]$left, [FigletCharacter]$right, [FigletLayoutMode]$mode, [int]$smushRules, [char]$hardblank) { $fit = [int]::MaxValue for ($y = 0; $y -lt $left.Height; $y++) { $ln = $left.Lines[$y].Replace($hardblank, ' ') $rn = $right.Lines[$y].Replace($hardblank, ' ') $trailing = $left.Lines[$y].Length - $ln.TrimEnd(' ').Length $leading = $right.Lines[$y].Length - $rn.TrimStart(' ').Length $fit = [Math]::Min($fit, $trailing + $leading) } if ($fit -eq [int]::MaxValue) { $fit = 0 } # Never fit/smush the space character if ($left.Code -eq 32 -or $right.Code -eq 32) { return @{ Amount = 0; MergeChars = $null } } if ($mode -ne [FigletLayoutMode]::Smushed) { return @{ Amount = $fit; MergeChars = $null } } # Try to smush: one extra column with merged boundary chars $mergeChars = [char[]]::new($left.Height) for ($i = 0; $i -lt $left.Height; $i++) { $ll = $left.Lines[$i]; $rl = $right.Lines[$i] $trailing = $ll.Length - $ll.Replace($hardblank, ' ').TrimEnd(' ').Length $trimFromLeft = [Math]::Min($trailing, $fit) $trimFromRight = $fit - $trimFromLeft $leftBound = $ll.Length - $trimFromLeft - 1 $rightBound = $trimFromRight $lc = if ($leftBound -ge 0) { $ll[$leftBound] } else { [char]' ' } $rc = if ($rightBound -lt $rl.Length) { $rl[$rightBound] } else { [char]' ' } if ($lc -eq ' ' -and $rc -eq ' ') { $mergeChars[$i] = ' '; continue } $merged = [FigletText]::SmushChars($lc, $rc, $smushRules, $hardblank) if ($null -eq $merged) { return @{ Amount = $fit; MergeChars = $null } } $mergeChars[$i] = $merged } return @{ Amount = ($fit + 1); MergeChars = $mergeChars } } static hidden [object] SmushChars([char]$left, [char]$right, [int]$rules, [char]$hardblank) { if ($left -eq ' ' -and $right -eq ' ') { return $null } if ($left -eq ' ') { return [object]$right } if ($right -eq ' ') { return [object]$left } # Hardblank rule (rule 6) if ($left -eq $hardblank -and $right -eq $hardblank) { return if ($rules -eq 0 -or ($rules -band 32) -ne 0) { [object]$hardblank } else { $null } } if ($left -eq $hardblank) { $left = ' ' } if ($right -eq $hardblank) { $right = ' ' } if ($left -eq ' ' -and $right -eq ' ') { return $null } if ($left -eq ' ') { return [object]$right } if ($right -eq ' ') { return [object]$left } # Universal smushing if ($rules -eq 0) { return [object]$right } # Rule 1: equal chars if (($rules -band 1) -ne 0 -and $left -eq $right) { return [object]$left } # Rule 2: underscore if (($rules -band 2) -ne 0) { if ($left -eq '_' -and [FigletText]::OverscoreChars.Contains($right.ToString())) { return [object]$right } if ($right -eq '_' -and [FigletText]::OverscoreChars.Contains($left.ToString())) { return [object]$left } } # Rule 3: hierarchy if (($rules -band 4) -ne 0) { $lc = [FigletText]::GetHierarchyClass($left) $rc = [FigletText]::GetHierarchyClass($right) if ($lc -ne 0 -and $rc -ne 0 -and $lc -ne $rc) { return if ($lc -gt $rc) { [object]$left } else { [object]$right } } } # Rule 4: opposite pairs if (($rules -band 8) -ne 0) { $pair = "$left$right" if ($pair -in @('[]', '][', '{}', '}{', '()', ')(')) { return [object]([char]'|') } } # Rule 5: big X if (($rules -band 16) -ne 0) { if ($left -eq '/' -and $right -eq '\') { return [object]([char]'|') } if ($left -eq '\' -and $right -eq '/') { return [object]([char]'Y') } if ($left -eq '>' -and $right -eq '<') { return [object]([char]'X') } } return $null } static hidden [int] GetHierarchyClass([char]$c) { switch ($c) { '|' { return 1 } { $_ -in '/', '\' } { return 2 } { $_ -in '[', ']' } { return 3 } { $_ -in '{', '}' } { return 4 } { $_ -in '(', ')' } { return 5 } { $_ -in '<', '>' } { return 6 } } return 0 } static hidden [string] BuildLine( [List[FigletCharacter]]$row, [int]$lineIdx, [object[]]$junctions, [char]$hardblank) { if ($row.Count -eq 0) { return '' } $sb = [StringBuilder]::new($row[0].Lines[$lineIdx]) for ($ri = 1; $ri -lt $row.Count; $ri++) { $junc = $junctions[$ri - 1] $amount = [int]$junc.Amount $mergeChars = $junc.MergeChars $rightLine = $row[$ri].Lines[$lineIdx] # Count trailing spaces/hardblanks in accumulated left side $trailing = 0 for ($k = $sb.Length - 1; $k -ge 0; $k--) { if ($sb[$k] -eq ' ' -or $sb[$k] -eq $hardblank) { $trailing++ } else { break } } if ($null -ne $mergeChars) { $fit = $amount - 1 $trimFromLeft = [Math]::Min($trailing, $fit) $trimFromRight = $fit - $trimFromLeft $removeLeft = [Math]::Min($trimFromLeft + 1, $sb.Length) [void]$sb.Remove($sb.Length - $removeLeft, $removeLeft) [void]$sb.Append($mergeChars[$lineIdx]) $skip = $trimFromRight + 1 if ($skip -lt $rightLine.Length) { [void]$sb.Append($rightLine.Substring($skip)) } } else { $trimFromLeft = [Math]::Min($trailing, $amount) $trimFromRight = $amount - $trimFromLeft if ($trimFromLeft -gt 0) { [void]$sb.Remove($sb.Length - $trimFromLeft, $trimFromLeft) } if ($trimFromRight -gt 0 -and $trimFromRight -lt $rightLine.Length) { [void]$sb.Append($rightLine.Substring($trimFromRight)) } else { [void]$sb.Append($rightLine) } } } return $sb.ToString() } } |