Skip to content

Commit

Permalink
fix: Align lines with emojis correctly
Browse files Browse the repository at this point in the history
Alignment issues occur when some lines have certain emojis in them. This
is caused by incorrectly calculating the width of a string.

For a string with emojis in them, it is important to understand that
the string width is different to the rune width. Many functions within
Lipgloss base the string length on the printable rune width which is
incorrect. The correct method requires finding the string width.

The reflow package has a printable rune width function. While the ANSI
sequences need to be removed, we do not want the rune width as this
function returns.

This change adds a new function to get the printable string width. The
code was taken from reflow and modified for use in Lipgloss. Since the
printable rune width function in reflow works correctly, it was more
reasonable to add the function to Lipgloss.
  • Loading branch information
mikelorant committed Dec 11, 2022
1 parent 04a7a77 commit 6d10502
Show file tree
Hide file tree
Showing 7 changed files with 40 additions and 21 deletions.
3 changes: 1 addition & 2 deletions align.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package lipgloss
import (
"strings"

"github.com/muesli/reflow/ansi"
"github.com/muesli/termenv"
)

Expand All @@ -15,7 +14,7 @@ func alignTextHorizontal(str string, pos Position, width int, style *termenv.Sty
var b strings.Builder

for i, l := range lines {
lineWidth := ansi.PrintableRuneWidth(l)
lineWidth := PrintableStringWidth(l)

shortAmount := widestLine - lineWidth // difference from the widest line
shortAmount += max(0, width-(shortAmount+lineWidth)) // difference from the total width, if set
Expand Down
7 changes: 3 additions & 4 deletions borders.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"strings"

"github.com/mattn/go-runewidth"
"github.com/muesli/reflow/ansi"
"github.com/muesli/termenv"
)

Expand Down Expand Up @@ -354,8 +353,8 @@ func renderHorizontalEdge(left, middle, right string, width int) string {
middle = " "
}

leftWidth := ansi.PrintableRuneWidth(left)
rightWidth := ansi.PrintableRuneWidth(right)
leftWidth := PrintableStringWidth(left)
rightWidth := PrintableStringWidth(right)

runes := []rune(middle)
j := 0
Expand All @@ -368,7 +367,7 @@ func renderHorizontalEdge(left, middle, right string, width int) string {
if j >= len(runes) {
j = 0
}
i += ansi.PrintableRuneWidth(string(runes[j]))
i += PrintableStringWidth(string(runes[j]))
}
out.WriteString(right)

Expand Down
32 changes: 30 additions & 2 deletions get.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package lipgloss
import (
"strings"

"github.com/muesli/reflow/ansi"
"github.com/mattn/go-runewidth"
)

const marker = '\x1B'

// GetBold returns the style's bold value. If no value is set false is returned.
func (s Style) GetBold() bool {
return s.getAsBool(boldKey, false)
Expand Down Expand Up @@ -462,11 +464,37 @@ func getLines(s string) (lines []string, widest int) {
lines = strings.Split(s, "\n")

for _, l := range lines {
w := ansi.PrintableRuneWidth(l)
w := PrintableStringWidth(l)
if widest < w {
widest = w
}
}

return lines, widest
}

// PrintableStringWidth returns the width of the given string.
func PrintableStringWidth(s string) int {
var ansi bool
var sr []rune

for _, c := range s {
if c == marker {
// ANSI escape sequence
ansi = true
} else if ansi {
if isTerminator(c) {
// ANSI sequence terminated
ansi = false
}
} else {
sr = append(sr, c)
}
}

return runewidth.StringWidth(string(sr))
}

func isTerminator(c rune) bool {
return (c >= 0x40 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a)
}
6 changes: 2 additions & 4 deletions join.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package lipgloss
import (
"math"
"strings"

"github.com/muesli/reflow/ansi"
)

// JoinHorizontal is a utility function for horizontally joining two
Expand Down Expand Up @@ -85,7 +83,7 @@ func JoinHorizontal(pos Position, strs ...string) string {
b.WriteString(block[i])

// Also make lines the same length
b.WriteString(strings.Repeat(" ", maxWidths[j]-ansi.PrintableRuneWidth(block[i])))
b.WriteString(strings.Repeat(" ", maxWidths[j]-PrintableStringWidth(block[i])))
}
if i < len(blocks[0])-1 {
b.WriteRune('\n')
Expand Down Expand Up @@ -137,7 +135,7 @@ func JoinVertical(pos Position, strs ...string) string {
var b strings.Builder
for i, block := range blocks {
for j, line := range block {
w := maxWidth - ansi.PrintableRuneWidth(line)
w := maxWidth - PrintableStringWidth(line)

switch pos {
case Left:
Expand Down
4 changes: 1 addition & 3 deletions position.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package lipgloss
import (
"math"
"strings"

"github.com/muesli/reflow/ansi"
)

// Position represents a position along a horizontal or vertical axis. It's in
Expand Down Expand Up @@ -56,7 +54,7 @@ func PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOpti
var b strings.Builder
for i, l := range lines {
// Is this line shorter than the longest line?
short := max(0, contentWidth-ansi.PrintableRuneWidth(l))
short := max(0, contentWidth-PrintableStringWidth(l))

switch pos {
case Left:
Expand Down
4 changes: 1 addition & 3 deletions size.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package lipgloss

import (
"strings"

"github.com/muesli/reflow/ansi"
)

// Width returns the cell width of characters in the string. ANSI sequences are
Expand All @@ -14,7 +12,7 @@ import (
// will give you accurate results.
func Width(str string) (width int) {
for _, l := range strings.Split(str, "\n") {
w := ansi.PrintableRuneWidth(l)
w := PrintableStringWidth(l)
if w > width {
width = w
}
Expand Down
5 changes: 2 additions & 3 deletions whitespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package lipgloss
import (
"strings"

"github.com/muesli/reflow/ansi"
"github.com/muesli/termenv"
)

Expand All @@ -30,12 +29,12 @@ func (w whitespace) render(width int) string {
if j >= len(r) {
j = 0
}
i += ansi.PrintableRuneWidth(string(r[j]))
i += PrintableStringWidth(string(r[j]))
}

// Fill any extra gaps white spaces. This might be necessary if any runes
// are more than one cell wide, which could leave a one-rune gap.
short := width - ansi.PrintableRuneWidth(b.String())
short := width - PrintableStringWidth(b.String())
if short > 0 {
b.WriteString(strings.Repeat(" ", short))
}
Expand Down

0 comments on commit 6d10502

Please sign in to comment.