Private/Measure-TextWidth.ps1
|
$script:TerminalTabStopWidth = 8 # Generated as start/end pairs from the merged W/F ranges in Unicode 17.0 EastAsianWidth.txt. $script:TerminalWideIntervalBounds = [int[]]@( [int[]]@(0x1100, 0x115F) [int[]]@(0x231A, 0x231B) [int[]]@(0x2329, 0x232A) [int[]]@(0x23E9, 0x23EC) [int[]]@(0x23F0, 0x23F0) [int[]]@(0x23F3, 0x23F3) [int[]]@(0x25FD, 0x25FE) [int[]]@(0x2614, 0x2615) [int[]]@(0x2630, 0x2637) [int[]]@(0x2648, 0x2653) [int[]]@(0x267F, 0x267F) [int[]]@(0x268A, 0x268F) [int[]]@(0x2693, 0x2693) [int[]]@(0x26A1, 0x26A1) [int[]]@(0x26AA, 0x26AB) [int[]]@(0x26BD, 0x26BE) [int[]]@(0x26C4, 0x26C5) [int[]]@(0x26CE, 0x26CE) [int[]]@(0x26D4, 0x26D4) [int[]]@(0x26EA, 0x26EA) [int[]]@(0x26F2, 0x26F3) [int[]]@(0x26F5, 0x26F5) [int[]]@(0x26FA, 0x26FA) [int[]]@(0x26FD, 0x26FD) [int[]]@(0x2705, 0x2705) [int[]]@(0x270A, 0x270B) [int[]]@(0x2728, 0x2728) [int[]]@(0x274C, 0x274C) [int[]]@(0x274E, 0x274E) [int[]]@(0x2753, 0x2755) [int[]]@(0x2757, 0x2757) [int[]]@(0x2795, 0x2797) [int[]]@(0x27B0, 0x27B0) [int[]]@(0x27BF, 0x27BF) [int[]]@(0x2B1B, 0x2B1C) [int[]]@(0x2B50, 0x2B50) [int[]]@(0x2B55, 0x2B55) [int[]]@(0x2E80, 0x2E99) [int[]]@(0x2E9B, 0x2EF3) [int[]]@(0x2F00, 0x2FD5) [int[]]@(0x2FF0, 0x303E) [int[]]@(0x3041, 0x3096) [int[]]@(0x3099, 0x30FF) [int[]]@(0x3105, 0x312F) [int[]]@(0x3131, 0x318E) [int[]]@(0x3190, 0x31E5) [int[]]@(0x31EF, 0x321E) [int[]]@(0x3220, 0x3247) [int[]]@(0x3250, 0xA48C) [int[]]@(0xA490, 0xA4C6) [int[]]@(0xA960, 0xA97C) [int[]]@(0xAC00, 0xD7A3) [int[]]@(0xF900, 0xFAFF) [int[]]@(0xFE10, 0xFE19) [int[]]@(0xFE30, 0xFE52) [int[]]@(0xFE54, 0xFE66) [int[]]@(0xFE68, 0xFE6B) [int[]]@(0xFF01, 0xFF60) [int[]]@(0xFFE0, 0xFFE6) [int[]]@(0x16FE0, 0x16FE4) [int[]]@(0x16FF0, 0x16FF6) [int[]]@(0x17000, 0x18CD5) [int[]]@(0x18CFF, 0x18D1E) [int[]]@(0x18D80, 0x18DF2) [int[]]@(0x1AFF0, 0x1AFF3) [int[]]@(0x1AFF5, 0x1AFFB) [int[]]@(0x1AFFD, 0x1AFFE) [int[]]@(0x1B000, 0x1B122) [int[]]@(0x1B132, 0x1B132) [int[]]@(0x1B150, 0x1B152) [int[]]@(0x1B155, 0x1B155) [int[]]@(0x1B164, 0x1B167) [int[]]@(0x1B170, 0x1B2FB) [int[]]@(0x1D300, 0x1D356) [int[]]@(0x1D360, 0x1D376) [int[]]@(0x1F004, 0x1F004) [int[]]@(0x1F0CF, 0x1F0CF) [int[]]@(0x1F18E, 0x1F18E) [int[]]@(0x1F191, 0x1F19A) [int[]]@(0x1F200, 0x1F202) [int[]]@(0x1F210, 0x1F23B) [int[]]@(0x1F240, 0x1F248) [int[]]@(0x1F250, 0x1F251) [int[]]@(0x1F260, 0x1F265) [int[]]@(0x1F300, 0x1F320) [int[]]@(0x1F32D, 0x1F335) [int[]]@(0x1F337, 0x1F37C) [int[]]@(0x1F37E, 0x1F393) [int[]]@(0x1F3A0, 0x1F3CA) [int[]]@(0x1F3CF, 0x1F3D3) [int[]]@(0x1F3E0, 0x1F3F0) [int[]]@(0x1F3F4, 0x1F3F4) [int[]]@(0x1F3F8, 0x1F43E) [int[]]@(0x1F440, 0x1F440) [int[]]@(0x1F442, 0x1F4FC) [int[]]@(0x1F4FF, 0x1F53D) [int[]]@(0x1F54B, 0x1F54E) [int[]]@(0x1F550, 0x1F567) [int[]]@(0x1F57A, 0x1F57A) [int[]]@(0x1F595, 0x1F596) [int[]]@(0x1F5A4, 0x1F5A4) [int[]]@(0x1F5FB, 0x1F64F) [int[]]@(0x1F680, 0x1F6C5) [int[]]@(0x1F6CC, 0x1F6CC) [int[]]@(0x1F6D0, 0x1F6D2) [int[]]@(0x1F6D5, 0x1F6D8) [int[]]@(0x1F6DC, 0x1F6DF) [int[]]@(0x1F6EB, 0x1F6EC) [int[]]@(0x1F6F4, 0x1F6FC) [int[]]@(0x1F7E0, 0x1F7EB) [int[]]@(0x1F7F0, 0x1F7F0) [int[]]@(0x1F90C, 0x1F93A) [int[]]@(0x1F93C, 0x1F945) [int[]]@(0x1F947, 0x1F9FF) [int[]]@(0x1FA70, 0x1FA7C) [int[]]@(0x1FA80, 0x1FA8A) [int[]]@(0x1FA8E, 0x1FAC6) [int[]]@(0x1FAC8, 0x1FAC8) [int[]]@(0x1FACD, 0x1FADC) [int[]]@(0x1FADF, 0x1FAEA) [int[]]@(0x1FAEF, 0x1FAF8) [int[]]@(0x20000, 0x2FFFD) [int[]]@(0x30000, 0x3FFFD) ) function Test-TerminalWideCodePoint { param([Parameter(Mandatory)][int]$Value) $low = 0 $high = [int]($script:TerminalWideIntervalBounds.Count / 2) - 1 while ($low -le $high) { $middle = [int][Math]::Floor(($low + $high) / 2) $start = $script:TerminalWideIntervalBounds[$middle * 2] $end = $script:TerminalWideIntervalBounds[($middle * 2) + 1] if ($Value -lt $start) { $high = $middle - 1 } elseif ($Value -gt $end) { $low = $middle + 1 } else { return $true } } return $false } function Get-TerminalRuneWidth { [CmdletBinding()] param([Parameter(Mandatory)][System.Text.Rune]$Rune) $category = [System.Text.Rune]::GetUnicodeCategory($Rune) if ($category -in @( [System.Globalization.UnicodeCategory]::Control, [System.Globalization.UnicodeCategory]::Format, [System.Globalization.UnicodeCategory]::NonSpacingMark, [System.Globalization.UnicodeCategory]::SpacingCombiningMark, [System.Globalization.UnicodeCategory]::EnclosingMark )) { return 0 } if (Test-TerminalWideCodePoint -Value $Rune.Value) { return 2 } return 1 } function Get-TerminalTextElements { [CmdletBinding()] param( [AllowNull()][string]$Text, [ValidateRange(0, [int]::MaxValue)][int]$StartColumn = 0, [switch]$PreserveTabs ) $elements = [System.Collections.Generic.List[object]]::new() $value = $Text ?? '' Assert-TerminalValidUtf16 -Value $value $plainText = Strip-AnsiSequences -Text $value $enumerator = [System.Globalization.StringInfo]::GetTextElementEnumerator($plainText) $column = $StartColumn while ($enumerator.MoveNext()) { $textElement = $enumerator.GetTextElement() if ($textElement -eq "`t") { $width = $script:TerminalTabStopWidth - ($column % $script:TerminalTabStopWidth) if ($PreserveTabs) { $elements.Add([pscustomobject]@{ Text = $textElement; Width = $width }) } else { for ($space = 0; $space -lt $width; $space++) { $elements.Add([pscustomobject]@{ Text = ' '; Width = 1 }) } } $column += $width continue } if ($textElement.Contains("`r") -or $textElement.Contains("`n")) { $elements.Add([pscustomobject]@{ Text = $textElement; Width = 0 }) $column = $StartColumn continue } $width = 0 $regionalIndicatorCount = 0 $hasEmojiVariationSelector = $false for ($offset = 0; $offset -lt $textElement.Length;) { $rune = [System.Text.Rune]::GetRuneAt($textElement, $offset) $width = [Math]::Max($width, (Get-TerminalRuneWidth -Rune $rune)) if ($rune.Value -ge 0x1F1E6 -and $rune.Value -le 0x1F1FF) { $regionalIndicatorCount++ } if ($rune.Value -eq 0xFE0F) { $hasEmojiVariationSelector = $true } $offset += $rune.Utf16SequenceLength } if ($regionalIndicatorCount -ge 2 -or ($hasEmojiVariationSelector -and $width -gt 0)) { $width = [Math]::Max(2, $width) } $elements.Add([pscustomobject]@{ Text = $textElement; Width = $width }) $column += $width } return ,$elements.ToArray() } function Expand-TerminalTabs { [CmdletBinding()] param( [AllowNull()][string]$Text, [ValidateRange(0, [int]::MaxValue)][int]$StartColumn = 0 ) return (@(Get-TerminalTextElements -Text ($Text ?? '') -StartColumn $StartColumn | ForEach-Object { $_.Text }) -join '') } function Measure-TextWidth { [CmdletBinding()] param( [AllowNull()][string]$Text, [ValidateRange(0, [int]::MaxValue)][int]$StartColumn = 0 ) $width = 0 foreach ($element in (Get-TerminalTextElements -Text ($Text ?? '') -StartColumn $StartColumn)) { $width += $element.Width } return $width } function Limit-TextToCellWidth { [CmdletBinding()] param( [AllowNull()][string]$Text, [Parameter(Mandatory)][int]$Width, [ValidateRange(0, [int]::MaxValue)][int]$StartColumn = 0, [switch]$PreserveTabs ) $builder = [System.Text.StringBuilder]::new() $used = 0 foreach ($element in (Get-TerminalTextElements -Text ($Text ?? '') -StartColumn $StartColumn -PreserveTabs:$PreserveTabs)) { if ($used + $element.Width -gt [Math]::Max(0, $Width)) { break } [void]$builder.Append($element.Text) $used += $element.Width } return $builder.ToString() } function Split-TextByCellWidth { [CmdletBinding()] param( [AllowNull()][string]$Text, [Parameter(Mandatory)][int]$Width, [ValidateRange(0, [int]::MaxValue)][int]$StartColumn = 0, [switch]$PreserveTabs ) $width = [Math]::Max(1, $Width) $lines = [System.Collections.Generic.List[string]]::new() $builder = [System.Text.StringBuilder]::new() $used = 0 $elements = Get-TerminalTextElements -Text ($Text ?? '') -StartColumn $StartColumn -PreserveTabs:$PreserveTabs foreach ($element in $elements) { if ($PreserveTabs) { $element = @(Get-TerminalTextElements -Text $element.Text -StartColumn ($StartColumn + $used) -PreserveTabs)[0] } if ($builder.Length -gt 0 -and $used + $element.Width -gt $width) { $lines.Add($builder.ToString()) [void]$builder.Clear() $used = 0 if ($PreserveTabs) { $element = @(Get-TerminalTextElements -Text $element.Text -StartColumn $StartColumn -PreserveTabs)[0] } } [void]$builder.Append($element.Text) $used += $element.Width } if ($builder.Length -gt 0 -or $lines.Count -eq 0) { $lines.Add($builder.ToString()) } return ,$lines.ToArray() } function Pad-TerminalText { [CmdletBinding()] param( [AllowNull()][string]$Text, [Parameter(Mandatory)][int]$Width, [ValidateRange(0, [int]::MaxValue)][int]$StartColumn = 0 ) $value = $Text ?? '' return $value + (' ' * [Math]::Max(0, $Width - (Measure-TextWidth -Text $value -StartColumn $StartColumn))) } |