|
@@ -5,6 +5,7 @@ package ircfmt
|
5
|
5
|
|
6
|
6
|
import (
|
7
|
7
|
"regexp"
|
|
8
|
+ "strconv"
|
8
|
9
|
"strings"
|
9
|
10
|
)
|
10
|
11
|
|
|
@@ -19,24 +20,126 @@ const (
|
19
|
20
|
underline string = "\x1f"
|
20
|
21
|
reset string = "\x0f"
|
21
|
22
|
|
22
|
|
- runecolour rune = '\x03'
|
23
|
|
- runebold rune = '\x02'
|
24
|
|
- runemonospace rune = '\x11'
|
25
|
|
- runereverseColour rune = '\x16'
|
26
|
|
- runeitalic rune = '\x1d'
|
27
|
|
- runestrikethrough rune = '\x1e'
|
28
|
|
- runereset rune = '\x0f'
|
29
|
|
- runeunderline rune = '\x1f'
|
30
|
|
-
|
31
|
|
- // valid characters in a colour code character, for speed
|
32
|
|
- colours1 string = "0123456789"
|
|
23
|
+ metacharacters = (bold + colour + monospace + reverseColour + italic + strikethrough + underline + reset)
|
33
|
24
|
)
|
34
|
25
|
|
|
26
|
+// ColorCode is a normalized representation of an IRC color code,
|
|
27
|
+// as per this de facto specification: https://modern.ircdocs.horse/formatting.html#color
|
|
28
|
+// The zero value of the type represents a default or unset color,
|
|
29
|
+// whereas ColorCode{true, 0} represents the color white.
|
|
30
|
+type ColorCode struct {
|
|
31
|
+ IsSet bool
|
|
32
|
+ Value uint8
|
|
33
|
+}
|
|
34
|
+
|
|
35
|
+// ParseColor converts a string representation of an IRC color code, e.g. "04",
|
|
36
|
+// into a normalized ColorCode, e.g. ColorCode{true, 4}.
|
|
37
|
+func ParseColor(str string) (color ColorCode) {
|
|
38
|
+ // "99 - Default Foreground/Background - Not universally supported."
|
|
39
|
+ // normalize 99 to ColorCode{} meaning "unset":
|
|
40
|
+ if code, err := strconv.ParseUint(str, 10, 8); err == nil && code < 99 {
|
|
41
|
+ color.IsSet = true
|
|
42
|
+ color.Value = uint8(code)
|
|
43
|
+ }
|
|
44
|
+ return
|
|
45
|
+}
|
|
46
|
+
|
|
47
|
+// FormattedSubstring represents a section of an IRC message with associated
|
|
48
|
+// formatting data.
|
|
49
|
+type FormattedSubstring struct {
|
|
50
|
+ Content string
|
|
51
|
+ ForegroundColor ColorCode
|
|
52
|
+ BackgroundColor ColorCode
|
|
53
|
+ Bold bool
|
|
54
|
+ Monospace bool
|
|
55
|
+ Strikethrough bool
|
|
56
|
+ Underline bool
|
|
57
|
+ Italic bool
|
|
58
|
+ ReverseColor bool
|
|
59
|
+}
|
|
60
|
+
|
|
61
|
+// IsFormatted returns whether the section has any formatting flags switched on.
|
|
62
|
+func (f *FormattedSubstring) IsFormatted() bool {
|
|
63
|
+ // could rely on value receiver but if this is to be a public API,
|
|
64
|
+ // let's make it a pointer receiver
|
|
65
|
+ g := *f
|
|
66
|
+ g.Content = ""
|
|
67
|
+ return g != FormattedSubstring{}
|
|
68
|
+}
|
|
69
|
+
|
|
70
|
+var (
|
|
71
|
+ // "If there are two ASCII digits available where a <COLOR> is allowed,
|
|
72
|
+ // then two characters MUST always be read for it and displayed as described below."
|
|
73
|
+ // we rely on greedy matching to implement this for both forms:
|
|
74
|
+ // (\x03)00,01
|
|
75
|
+ colorForeBackRe = regexp.MustCompile(`^([0-9]{1,2}),([0-9]{1,2})`)
|
|
76
|
+ // (\x03)00
|
|
77
|
+ colorForeRe = regexp.MustCompile(`^([0-9]{1,2})`)
|
|
78
|
+)
|
|
79
|
+
|
|
80
|
+// Split takes an IRC message (typically a PRIVMSG or NOTICE final parameter)
|
|
81
|
+// containing IRC formatting control codes, and splits it into substrings with
|
|
82
|
+// associated formatting information.
|
|
83
|
+func Split(raw string) (result []FormattedSubstring) {
|
|
84
|
+ var chunk FormattedSubstring
|
|
85
|
+ for {
|
|
86
|
+ // skip to the next metacharacter, or the end of the string
|
|
87
|
+ if idx := strings.IndexAny(raw, metacharacters); idx != 0 {
|
|
88
|
+ if idx == -1 {
|
|
89
|
+ idx = len(raw)
|
|
90
|
+ }
|
|
91
|
+ chunk.Content = raw[:idx]
|
|
92
|
+ if len(chunk.Content) != 0 {
|
|
93
|
+ result = append(result, chunk)
|
|
94
|
+ }
|
|
95
|
+ raw = raw[idx:]
|
|
96
|
+ }
|
|
97
|
+
|
|
98
|
+ if len(raw) == 0 {
|
|
99
|
+ return
|
|
100
|
+ }
|
|
101
|
+
|
|
102
|
+ // we're at a metacharacter. by default, all previous formatting carries over
|
|
103
|
+ metacharacter := raw[0]
|
|
104
|
+ raw = raw[1:]
|
|
105
|
+ switch metacharacter {
|
|
106
|
+ case bold[0]:
|
|
107
|
+ chunk.Bold = !chunk.Bold
|
|
108
|
+ case monospace[0]:
|
|
109
|
+ chunk.Monospace = !chunk.Monospace
|
|
110
|
+ case strikethrough[0]:
|
|
111
|
+ chunk.Strikethrough = !chunk.Strikethrough
|
|
112
|
+ case underline[0]:
|
|
113
|
+ chunk.Underline = !chunk.Underline
|
|
114
|
+ case italic[0]:
|
|
115
|
+ chunk.Italic = !chunk.Italic
|
|
116
|
+ case reverseColour[0]:
|
|
117
|
+ chunk.ReverseColor = !chunk.ReverseColor
|
|
118
|
+ case reset[0]:
|
|
119
|
+ chunk = FormattedSubstring{}
|
|
120
|
+ case colour[0]:
|
|
121
|
+ // preferentially match the "\x0399,01" form, then "\x0399";
|
|
122
|
+ // if neither of those matches, then it's a reset
|
|
123
|
+ if matches := colorForeBackRe.FindStringSubmatch(raw); len(matches) != 0 {
|
|
124
|
+ chunk.ForegroundColor = ParseColor(matches[1])
|
|
125
|
+ chunk.BackgroundColor = ParseColor(matches[2])
|
|
126
|
+ raw = raw[len(matches[0]):]
|
|
127
|
+ } else if matches := colorForeRe.FindStringSubmatch(raw); len(matches) != 0 {
|
|
128
|
+ chunk.ForegroundColor = ParseColor(matches[1])
|
|
129
|
+ raw = raw[len(matches[0]):]
|
|
130
|
+ } else {
|
|
131
|
+ chunk.ForegroundColor = ColorCode{}
|
|
132
|
+ chunk.BackgroundColor = ColorCode{}
|
|
133
|
+ }
|
|
134
|
+ default:
|
|
135
|
+ // should be impossible, but just ignore it
|
|
136
|
+ }
|
|
137
|
+ }
|
|
138
|
+}
|
|
139
|
+
|
35
|
140
|
var (
|
36
|
141
|
// valtoescape replaces most of IRC characters with our escapes.
|
37
|
142
|
valtoescape = strings.NewReplacer("$", "$$", colour, "$c", reverseColour, "$v", bold, "$b", italic, "$i", strikethrough, "$s", underline, "$u", monospace, "$m", reset, "$r")
|
38
|
|
- // valToStrip replaces most of the IRC characters with nothing
|
39
|
|
- valToStrip = strings.NewReplacer(colour, "$c", reverseColour, "", bold, "", italic, "", strikethrough, "", underline, "", monospace, "", reset, "")
|
40
|
143
|
|
41
|
144
|
// escapetoval contains most of our escapes and how they map to real IRC characters.
|
42
|
145
|
// intentionally skips colour, since that's handled elsewhere.
|
|
@@ -98,7 +201,9 @@ var (
|
98
|
201
|
"light blue": "12",
|
99
|
202
|
"pink": "13",
|
100
|
203
|
"grey": "14",
|
|
204
|
+ "gray": "14",
|
101
|
205
|
"light grey": "15",
|
|
206
|
+ "light gray": "15",
|
102
|
207
|
"default": "99",
|
103
|
208
|
}
|
104
|
209
|
|
|
@@ -123,7 +228,7 @@ func Escape(in string) string {
|
123
|
228
|
out.WriteString("$c")
|
124
|
229
|
inRunes = inRunes[2:] // strip colour code chars
|
125
|
230
|
|
126
|
|
- if len(inRunes) < 1 || !strings.Contains(colours1, string(inRunes[0])) {
|
|
231
|
+ if len(inRunes) < 1 || !isDigit(inRunes[0]) {
|
127
|
232
|
out.WriteString("[]")
|
128
|
233
|
continue
|
129
|
234
|
}
|
|
@@ -131,14 +236,14 @@ func Escape(in string) string {
|
131
|
236
|
var foreBuffer, backBuffer string
|
132
|
237
|
foreBuffer += string(inRunes[0])
|
133
|
238
|
inRunes = inRunes[1:]
|
134
|
|
- if 0 < len(inRunes) && strings.Contains(colours1, string(inRunes[0])) {
|
|
239
|
+ if 0 < len(inRunes) && isDigit(inRunes[0]) {
|
135
|
240
|
foreBuffer += string(inRunes[0])
|
136
|
241
|
inRunes = inRunes[1:]
|
137
|
242
|
}
|
138
|
|
- if 1 < len(inRunes) && inRunes[0] == ',' && strings.Contains(colours1, string(inRunes[1])) {
|
|
243
|
+ if 1 < len(inRunes) && inRunes[0] == ',' && isDigit(inRunes[1]) {
|
139
|
244
|
backBuffer += string(inRunes[1])
|
140
|
245
|
inRunes = inRunes[2:]
|
141
|
|
- if 0 < len(inRunes) && strings.Contains(colours1, string(inRunes[0])) {
|
|
246
|
+ if 0 < len(inRunes) && isDigit(inRunes[1]) {
|
142
|
247
|
backBuffer += string(inRunes[0])
|
143
|
248
|
inRunes = inRunes[1:]
|
144
|
249
|
}
|
|
@@ -178,52 +283,27 @@ func Escape(in string) string {
|
178
|
283
|
return out.String()
|
179
|
284
|
}
|
180
|
285
|
|
|
286
|
+func isDigit(r rune) bool {
|
|
287
|
+ return '0' <= r && r <= '9' // don't use unicode.IsDigit, it includes non-ASCII numerals
|
|
288
|
+}
|
|
289
|
+
|
181
|
290
|
// Strip takes a raw IRC string and removes it with all formatting codes removed
|
182
|
291
|
// IE, it turns this: "This is a \x02cool\x02, \x034red\x0f message!"
|
183
|
292
|
// into: "This is a cool, red message!"
|
184
|
293
|
func Strip(in string) string {
|
185
|
|
- out := strings.Builder{}
|
186
|
|
- runes := []rune(in)
|
187
|
|
- if out.Len() < len(runes) { // Reduce allocations where needed
|
188
|
|
- out.Grow(len(in) - out.Len())
|
189
|
|
- }
|
190
|
|
- for len(runes) > 0 {
|
191
|
|
- switch runes[0] {
|
192
|
|
- case runebold, runemonospace, runereverseColour, runeitalic, runestrikethrough, runeunderline, runereset:
|
193
|
|
- runes = runes[1:]
|
194
|
|
- case runecolour:
|
195
|
|
- runes = removeColour(runes)
|
196
|
|
- default:
|
197
|
|
- out.WriteRune(runes[0])
|
198
|
|
- runes = runes[1:]
|
199
|
|
- }
|
200
|
|
- }
|
201
|
|
- return out.String()
|
202
|
|
-}
|
203
|
|
-
|
204
|
|
-func removeNumber(runes []rune) []rune {
|
205
|
|
- if len(runes) > 0 && runes[0] >= '0' && runes[0] <= '9' {
|
206
|
|
- runes = runes[1:]
|
207
|
|
- }
|
208
|
|
- return runes
|
209
|
|
-}
|
210
|
|
-
|
211
|
|
-func removeColour(runes []rune) []rune {
|
212
|
|
- if runes[0] != runecolour {
|
213
|
|
- return runes
|
214
|
|
- }
|
215
|
|
-
|
216
|
|
- runes = runes[1:]
|
217
|
|
- runes = removeNumber(runes)
|
218
|
|
- runes = removeNumber(runes)
|
219
|
|
-
|
220
|
|
- if len(runes) > 1 && runes[0] == ',' && runes[1] >= '0' && runes[1] <= '9' {
|
221
|
|
- runes = runes[2:]
|
|
294
|
+ splitChunks := Split(in)
|
|
295
|
+ if len(splitChunks) == 0 {
|
|
296
|
+ return ""
|
|
297
|
+ } else if len(splitChunks) == 1 {
|
|
298
|
+ return splitChunks[0].Content
|
222
|
299
|
} else {
|
223
|
|
- return runes // Nothing else because we dont have a comma
|
|
300
|
+ var buf strings.Builder
|
|
301
|
+ buf.Grow(len(in))
|
|
302
|
+ for _, chunk := range splitChunks {
|
|
303
|
+ buf.WriteString(chunk.Content)
|
|
304
|
+ }
|
|
305
|
+ return buf.String()
|
224
|
306
|
}
|
225
|
|
- runes = removeNumber(runes)
|
226
|
|
- return runes
|
227
|
307
|
}
|
228
|
308
|
|
229
|
309
|
// resolve "light blue" to "12", "12" to "12", "asdf" to "", etc.
|