1package list
2
3import (
4 "image"
5 "strings"
6
7 "charm.land/lipgloss/v2"
8 uv "github.com/charmbracelet/ultraviolet"
9)
10
11// DefaultHighlighter is the default highlighter function that applies inverse style.
12var DefaultHighlighter Highlighter = func(x, y int, c *uv.Cell) *uv.Cell {
13 if c == nil {
14 return c
15 }
16 c.Style.Attrs |= uv.AttrReverse
17 return c
18}
19
20// Highlighter represents a function that defines how to highlight text.
21type Highlighter func(x, y int, c *uv.Cell) *uv.Cell
22
23// HighlightContent returns the content with highlighted regions based on the specified parameters.
24func HighlightContent(content string, area image.Rectangle, startLine, startCol, endLine, endCol int) string {
25 var sb strings.Builder
26 pos := image.Pt(-1, -1)
27 HighlightBuffer(content, area, startLine, startCol, endLine, endCol, func(x, y int, c *uv.Cell) *uv.Cell {
28 pos.X = x
29 if pos.Y == -1 {
30 pos.Y = y
31 } else if y > pos.Y {
32 sb.WriteString(strings.Repeat("\n", y-pos.Y))
33 pos.Y = y
34 }
35 sb.WriteString(c.Content)
36 return c
37 })
38 if sb.Len() > 0 {
39 sb.WriteString("\n")
40 }
41 return sb.String()
42}
43
44// Highlight highlights a region of text within the given content and region.
45func Highlight(content string, area image.Rectangle, startLine, startCol, endLine, endCol int, highlighter Highlighter) string {
46 buf := HighlightBuffer(content, area, startLine, startCol, endLine, endCol, highlighter)
47 if buf == nil {
48 return content
49 }
50 return buf.Render()
51}
52
53// HighlightBuffer highlights a region of text within the given content and
54// region, returning a [uv.ScreenBuffer].
55func HighlightBuffer(content string, area image.Rectangle, startLine, startCol, endLine, endCol int, highlighter Highlighter) *uv.ScreenBuffer {
56 if startLine < 0 || startCol < 0 {
57 return nil
58 }
59
60 if highlighter == nil {
61 highlighter = DefaultHighlighter
62 }
63
64 width, height := area.Dx(), area.Dy()
65 buf := uv.NewScreenBuffer(width, height)
66 styled := uv.NewStyledString(content)
67 styled.Draw(&buf, area)
68
69 // Treat -1 as "end of content"
70 if endLine < 0 {
71 endLine = height - 1
72 }
73 if endCol < 0 {
74 endCol = width
75 }
76
77 for y := startLine; y <= endLine && y < height; y++ {
78 if y >= buf.Height() {
79 break
80 }
81
82 line := buf.Line(y)
83
84 // Determine column range for this line
85 colStart := 0
86 if y == startLine {
87 colStart = min(startCol, len(line))
88 }
89
90 colEnd := len(line)
91 if y == endLine {
92 colEnd = min(endCol, len(line))
93 }
94
95 // Track last non-empty position as we go
96 lastContentX := -1
97
98 // Single pass: check content and track last non-empty position
99 for x := colStart; x < colEnd; x++ {
100 cell := line.At(x)
101 if cell == nil {
102 continue
103 }
104
105 // Update last content position if non-empty
106 if cell.Content != "" && cell.Content != " " {
107 lastContentX = x
108 }
109 }
110
111 // Only apply highlight up to last content position
112 highlightEnd := colEnd
113 if lastContentX >= 0 {
114 highlightEnd = lastContentX + 1
115 } else if lastContentX == -1 {
116 highlightEnd = colStart // No content on this line
117 }
118
119 // Apply highlight style only to cells with content
120 for x := colStart; x < highlightEnd; x++ {
121 if !image.Pt(x, y).In(area) {
122 continue
123 }
124 cell := line.At(x)
125 if cell != nil {
126 line.Set(x, highlighter(x, y, cell))
127 }
128 }
129 }
130
131 return &buf
132}
133
134// ToHighlighter converts a [lipgloss.Style] to a [Highlighter].
135func ToHighlighter(lgStyle lipgloss.Style) Highlighter {
136 return func(_ int, _ int, c *uv.Cell) *uv.Cell {
137 if c != nil {
138 c.Style = ToStyle(lgStyle)
139 }
140 return c
141 }
142}
143
144// ToStyle converts an inline [lipgloss.Style] to a [uv.Style].
145func ToStyle(lgStyle lipgloss.Style) uv.Style {
146 var uvStyle uv.Style
147
148 // Colors are already color.Color
149 uvStyle.Fg = lgStyle.GetForeground()
150 uvStyle.Bg = lgStyle.GetBackground()
151
152 // Build attributes using bitwise OR
153 var attrs uint8
154
155 if lgStyle.GetBold() {
156 attrs |= uv.AttrBold
157 }
158
159 if lgStyle.GetItalic() {
160 attrs |= uv.AttrItalic
161 }
162
163 if lgStyle.GetUnderline() {
164 uvStyle.Underline = uv.UnderlineSingle
165 }
166
167 if lgStyle.GetStrikethrough() {
168 attrs |= uv.AttrStrikethrough
169 }
170
171 if lgStyle.GetFaint() {
172 attrs |= uv.AttrFaint
173 }
174
175 if lgStyle.GetBlink() {
176 attrs |= uv.AttrBlink
177 }
178
179 if lgStyle.GetReverse() {
180 attrs |= uv.AttrReverse
181 }
182
183 uvStyle.Attrs = attrs
184
185 return uvStyle
186}
187
188// AdjustArea adjusts the given area rectangle by subtracting margins, borders,
189// and padding from the style.
190func AdjustArea(area image.Rectangle, style lipgloss.Style) image.Rectangle {
191 topMargin, rightMargin, bottomMargin, leftMargin := style.GetMargin()
192 topBorder, rightBorder, bottomBorder, leftBorder := style.GetBorderTopSize(),
193 style.GetBorderRightSize(),
194 style.GetBorderBottomSize(),
195 style.GetBorderLeftSize()
196 topPadding, rightPadding, bottomPadding, leftPadding := style.GetPadding()
197
198 return image.Rectangle{
199 Min: image.Point{
200 X: area.Min.X + leftMargin + leftBorder + leftPadding,
201 Y: area.Min.Y + topMargin + topBorder + topPadding,
202 },
203 Max: image.Point{
204 X: area.Max.X - (rightMargin + rightBorder + rightPadding),
205 Y: area.Max.Y - (bottomMargin + bottomBorder + bottomPadding),
206 },
207 }
208}