• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jedib0t / go-pretty / 14018453830

23 Mar 2025 11:18AM UTC coverage: 99.872% (-0.1%) from 100.0%
14018453830

Pull #364

github

houdini91
OSC 8 hiperlink support

Signed-off-by: houdini91 <mdstrauss91@gmail.com>
Pull Request #364: OSC 8 hiperlink support

4 of 9 new or added lines in 1 file covered. (44.44%)

3891 of 3896 relevant lines covered (99.87%)

1.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.33
/text/escape_seq_parser.go
1
package text
2

3
import (
4
        "fmt"
5
        "sort"
6
        "strconv"
7
        "strings"
8
)
9

10
// Constants
11
const (
12
        EscapeReset        = EscapeResetCSI
13
        EscapeResetCSI     = EscapeStartCSI + "0" + EscapeStopCSI
14
        EscapeResetOSI     = EscapeStartOSI + "0" + EscapeStopOSI
15
        EscapeStart        = EscapeStartCSI
16
        EscapeStartCSI     = "\x1b["
17
        EscapeStartOSI     = "\x1b]"
18
        EscapeStartRune    = rune(27) // \x1b
19
        EscapeStartRuneCSI = '['      // [
20
        EscapeStartRuneOSI = ']'      // ]
21
        EscapeStop         = EscapeStopCSI
22
        EscapeStopCSI      = "m"
23
        EscapeStopOSI      = "\\"
24
        EscapeStopRune     = EscapeStopRuneCSI
25
        EscapeStopRuneCSI  = 'm'
26
        EscapeStopRuneOSI  = '\\'
27
)
28

29
// Deprecated Constants
30
const (
31
        CSIStartRune = EscapeStartRuneCSI
32
        CSIStopRune  = EscapeStopRuneCSI
33
        OSIStartRune = EscapeStartRuneOSI
34
        OSIStopRune  = EscapeStopRuneOSI
35
)
36

37
type escSeqKind int
38

39
const (
40
        escSeqKindUnknown escSeqKind = iota
41
        escSeqKindCSI
42
        escSeqKindOSI
43
)
44

45
type escSeqParser struct {
46
        codes map[int]bool
47

48
        // consume specific
49
        inEscSeq   bool
50
        escSeqKind escSeqKind
51
        escapeSeq  string
52
}
53

54
func (s *escSeqParser) Codes() []int {
1✔
55
        codes := make([]int, 0)
1✔
56
        for code, val := range s.codes {
2✔
57
                if val {
2✔
58
                        codes = append(codes, code)
1✔
59
                }
1✔
60
        }
61
        sort.Ints(codes)
1✔
62
        return codes
1✔
63
}
64

65
func (s *escSeqParser) Consume(char rune) {
1✔
66
        if !s.inEscSeq && char == EscapeStartRune {
2✔
67
                s.inEscSeq = true
1✔
68
                s.escSeqKind = escSeqKindUnknown
1✔
69
                s.escapeSeq = ""
1✔
70
        } else if s.inEscSeq && s.escSeqKind == escSeqKindUnknown {
3✔
71
                if char == EscapeStartRuneCSI {
2✔
72
                        s.escSeqKind = escSeqKindCSI
1✔
73
                } else if char == EscapeStartRuneOSI {
3✔
74
                        s.escSeqKind = escSeqKindOSI
1✔
75
                }
1✔
76
        }
77

78
        if s.inEscSeq {
2✔
79
                s.escapeSeq += string(char)
1✔
80

1✔
81
                // --- FIX for OSC 8 hyperlinks (e.g. \x1b]8;;url\x07label\x1b]8;;\x07)
1✔
82
                if s.escSeqKind == escSeqKindOSI &&
1✔
83
                        strings.HasPrefix(s.escapeSeq, escapeStartConcealOSI) &&
1✔
84
                        char == '\a' { // BEL
1✔
NEW
85

×
NEW
86
                        s.ParseSeq(s.escapeSeq, s.escSeqKind)
×
NEW
87
                        s.Reset()
×
NEW
88
                        return
×
NEW
89
                }
×
90

91
                if s.isEscapeStopRune(char) {
2✔
92
                        s.ParseSeq(s.escapeSeq, s.escSeqKind)
1✔
93
                        s.Reset()
1✔
94
                }
1✔
95
        }
96
}
97

98
func (s *escSeqParser) InSequence() bool {
1✔
99
        return s.inEscSeq
1✔
100
}
1✔
101

102
func (s *escSeqParser) IsOpen() bool {
1✔
103
        return len(s.codes) > 0
1✔
104
}
1✔
105

106
func (s *escSeqParser) Reset() {
1✔
107
        s.inEscSeq = false
1✔
108
        s.escSeqKind = escSeqKindUnknown
1✔
109
        s.escapeSeq = ""
1✔
110
}
1✔
111

112
const (
113
        escCodeResetAll        = 0
114
        escCodeResetIntensity  = 22
115
        escCodeResetItalic     = 23
116
        escCodeResetUnderline  = 24
117
        escCodeResetBlink      = 25
118
        escCodeResetReverse    = 27
119
        escCodeResetCrossedOut = 29
120
        escCodeBold            = 1
121
        escCodeDim             = 2
122
        escCodeItalic          = 3
123
        escCodeUnderline       = 4
124
        escCodeBlinkSlow       = 5
125
        escCodeBlinkRapid      = 6
126
        escCodeReverse         = 7
127
        escCodeConceal         = 8
128
        escCodeCrossedOut      = 9
129
)
130

131
func (s *escSeqParser) ParseSeq(seq string, seqKind escSeqKind) {
1✔
132
        if s.codes == nil {
2✔
133
                s.codes = make(map[int]bool)
1✔
134
        }
1✔
135

136
        if seqKind == escSeqKindOSI {
2✔
137
                seq = strings.Replace(seq, EscapeStartOSI, "", 1)
1✔
138
                seq = strings.Replace(seq, EscapeStopOSI, "", 1)
1✔
139
        } else { // escSeqKindCSI
2✔
140
                seq = strings.Replace(seq, EscapeStartCSI, "", 1)
1✔
141
                seq = strings.Replace(seq, EscapeStopCSI, "", 1)
1✔
142
        }
1✔
143

144
        codes := strings.Split(seq, ";")
1✔
145
        for _, code := range codes {
2✔
146
                code = strings.TrimSpace(code)
1✔
147
                if codeNum, err := strconv.Atoi(code); err == nil {
2✔
148
                        switch codeNum {
1✔
149
                        case escCodeResetAll:
1✔
150
                                s.codes = make(map[int]bool) // clear everything
1✔
151
                        case escCodeResetIntensity:
1✔
152
                                delete(s.codes, escCodeBold)
1✔
153
                                delete(s.codes, escCodeDim)
1✔
154
                        case escCodeResetItalic:
1✔
155
                                delete(s.codes, escCodeItalic)
1✔
156
                        case escCodeResetUnderline:
1✔
157
                                delete(s.codes, escCodeUnderline)
1✔
158
                        case escCodeResetBlink:
1✔
159
                                delete(s.codes, escCodeBlinkSlow)
1✔
160
                                delete(s.codes, escCodeBlinkRapid)
1✔
161
                        case escCodeResetReverse:
1✔
162
                                delete(s.codes, escCodeReverse)
1✔
163
                        case escCodeResetCrossedOut:
1✔
164
                                delete(s.codes, escCodeCrossedOut)
1✔
165
                        default:
1✔
166
                                s.codes[codeNum] = true
1✔
167
                        }
168
                }
169
        }
170
}
171

172
func (s *escSeqParser) ParseString(str string) string {
1✔
173
        s.escapeSeq, s.inEscSeq, s.escSeqKind = "", false, escSeqKindUnknown
1✔
174
        for _, char := range str {
2✔
175
                s.Consume(char)
1✔
176
        }
1✔
177
        return s.Sequence()
1✔
178
}
179

180
func (s *escSeqParser) Sequence() string {
1✔
181
        out := strings.Builder{}
1✔
182
        if s.IsOpen() {
2✔
183
                out.WriteString(EscapeStart)
1✔
184
                for idx, code := range s.Codes() {
2✔
185
                        if idx > 0 {
2✔
186
                                out.WriteRune(';')
1✔
187
                        }
1✔
188
                        out.WriteString(fmt.Sprint(code))
1✔
189
                }
190
                out.WriteString(EscapeStop)
1✔
191
        }
192

193
        return out.String()
1✔
194
}
195

196
const (
197
        escapeStartConcealOSI = "\x1b]8;"
198
        escapeStopConcealOSI  = "\x1b\\"
199
)
200

201
func (s *escSeqParser) isEscapeStopRune(char rune) bool {
1✔
202
        if strings.HasPrefix(s.escapeSeq, escapeStartConcealOSI) {
2✔
203
                if strings.HasSuffix(s.escapeSeq, escapeStopConcealOSI) {
2✔
204
                        return true
1✔
205
                }
1✔
206
        } else if (s.escSeqKind == escSeqKindCSI && char == EscapeStopRuneCSI) ||
1✔
207
                (s.escSeqKind == escSeqKindOSI && char == EscapeStopRuneOSI) {
2✔
208
                return true
1✔
209
        }
1✔
210
        return false
1✔
211
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc