ergo/vendor/github.com/goshuirc/irc-go/ircfmt/ircfmt.go

331 lines
8.0 KiB
Go

// written by Daniel Oaks <daniel@danieloaks.net>
// released under the ISC license
package ircfmt
import (
"strings"
)
const (
// raw bytes and strings to do replacing with
bold string = "\x02"
colour string = "\x03"
monospace string = "\x11"
reverseColour string = "\x16"
italic string = "\x1d"
strikethrough string = "\x1e"
underline string = "\x1f"
reset string = "\x0f"
runecolour rune = '\x03'
runebold rune = '\x02'
runemonospace rune = '\x11'
runereverseColour rune = '\x16'
runeitalic rune = '\x1d'
runestrikethrough rune = '\x1e'
runereset rune = '\x0f'
runeunderline rune = '\x1f'
// valid characters in a colour code character, for speed
colours1 string = "0123456789"
)
var (
// valtoescape replaces most of IRC characters with our escapes.
valtoescape = strings.NewReplacer("$", "$$", colour, "$c", reverseColour, "$v", bold, "$b", italic, "$i", strikethrough, "$s", underline, "$u", monospace, "$m", reset, "$r")
// valToStrip replaces most of the IRC characters with nothing
valToStrip = strings.NewReplacer(colour, "$c", reverseColour, "", bold, "", italic, "", strikethrough, "", underline, "", monospace, "", reset, "")
// escapetoval contains most of our escapes and how they map to real IRC characters.
// intentionally skips colour, since that's handled elsewhere.
escapetoval = map[rune]string{
'$': "$",
'b': bold,
'i': italic,
'v': reverseColour,
's': strikethrough,
'u': underline,
'm': monospace,
'r': reset,
}
// valid colour codes
numtocolour = map[string]string{
"99": "default",
"15": "light grey",
"14": "grey",
"13": "pink",
"12": "light blue",
"11": "light cyan",
"10": "cyan",
"09": "light green",
"08": "yellow",
"07": "orange",
"06": "magenta",
"05": "brown",
"04": "red",
"03": "green",
"02": "blue",
"01": "black",
"00": "white",
"9": "light green",
"8": "yellow",
"7": "orange",
"6": "magenta",
"5": "brown",
"4": "red",
"3": "green",
"2": "blue",
"1": "black",
"0": "white",
}
// full and truncated colour codes
colourcodesFull = map[string]string{
"white": "00",
"black": "01",
"blue": "02",
"green": "03",
"red": "04",
"brown": "05",
"magenta": "06",
"orange": "07",
"yellow": "08",
"light green": "09",
"cyan": "10",
"light cyan": "11",
"light blue": "12",
"pink": "13",
"grey": "14",
"light grey": "15",
"default": "99",
}
colourcodesTruncated = map[string]string{
"white": "0",
"black": "1",
"blue": "2",
"green": "3",
"red": "4",
"brown": "5",
"magenta": "6",
"orange": "7",
"yellow": "8",
"light green": "9",
"cyan": "10",
"light cyan": "11",
"light blue": "12",
"pink": "13",
"grey": "14",
"light grey": "15",
"default": "99",
}
)
// Escape takes a raw IRC string and returns it with our escapes.
//
// IE, it turns this: "This is a \x02cool\x02, \x034red\x0f message!"
// into: "This is a $bcool$b, $c[red]red$r message!"
func Escape(in string) string {
// replace all our usual escapes
in = valtoescape.Replace(in)
inRunes := []rune(in)
//var out string
out := strings.Builder{}
for 0 < len(inRunes) {
if 1 < len(inRunes) && inRunes[0] == '$' && inRunes[1] == 'c' {
// handle colours
out.WriteString("$c")
inRunes = inRunes[2:] // strip colour code chars
if len(inRunes) < 1 || !strings.Contains(colours1, string(inRunes[0])) {
out.WriteString("[]")
continue
}
var foreBuffer, backBuffer string
foreBuffer += string(inRunes[0])
inRunes = inRunes[1:]
if 0 < len(inRunes) && strings.Contains(colours1, string(inRunes[0])) {
foreBuffer += string(inRunes[0])
inRunes = inRunes[1:]
}
if 1 < len(inRunes) && inRunes[0] == ',' && strings.Contains(colours1, string(inRunes[1])) {
backBuffer += string(inRunes[1])
inRunes = inRunes[2:]
if 0 < len(inRunes) && strings.Contains(colours1, string(inRunes[0])) {
backBuffer += string(inRunes[0])
inRunes = inRunes[1:]
}
}
foreName, exists := numtocolour[foreBuffer]
if !exists {
foreName = foreBuffer
}
backName, exists := numtocolour[backBuffer]
if !exists {
backName = backBuffer
}
out.WriteRune('[')
out.WriteString(foreName)
if backName != "" {
out.WriteRune(',')
out.WriteString(backName)
}
out.WriteRune(']')
} else {
// special case for $$c
if len(inRunes) > 2 && inRunes[0] == '$' && inRunes[1] == '$' && inRunes[2] == 'c' {
out.WriteRune(inRunes[0])
out.WriteRune(inRunes[1])
out.WriteRune(inRunes[2])
inRunes = inRunes[3:]
} else {
out.WriteRune(inRunes[0])
inRunes = inRunes[1:]
}
}
}
return out.String()
}
// Strip takes a raw IRC string and removes it with all formatting codes removed
// IE, it turns this: "This is a \x02cool\x02, \x034red\x0f message!"
// into: "This is a cool, red message!"
func Strip(in string) string {
out := strings.Builder{}
runes := []rune(in)
if out.Len() < len(runes) { // Reduce allocations where needed
out.Grow(len(in) - out.Len())
}
for len(runes) > 0 {
switch runes[0] {
case runebold, runemonospace, runereverseColour, runeitalic, runestrikethrough, runeunderline, runereset:
runes = runes[1:]
case runecolour:
runes = removeColour(runes)
default:
out.WriteRune(runes[0])
runes = runes[1:]
}
}
return out.String()
}
func removeNumber(runes []rune) []rune {
if len(runes) > 0 && runes[0] >= '0' && runes[0] <= '9' {
runes = runes[1:]
}
return runes
}
func removeColour(runes []rune) []rune {
if runes[0] != runecolour {
return runes
}
runes = runes[1:]
runes = removeNumber(runes)
runes = removeNumber(runes)
if len(runes) > 1 && runes[0] == ',' && runes[1] >= '0' && runes[1] <= '9' {
runes = runes[2:]
} else {
return runes // Nothing else because we dont have a comma
}
runes = removeNumber(runes)
return runes
}
// Unescape takes our escaped string and returns a raw IRC string.
//
// IE, it turns this: "This is a $bcool$b, $c[red]red$r message!"
// into this: "This is a \x02cool\x02, \x034red\x0f message!"
func Unescape(in string) string {
out := strings.Builder{}
remaining := []rune(in)
for 0 < len(remaining) {
char := remaining[0]
remaining = remaining[1:]
if char == '$' && 0 < len(remaining) {
char = remaining[0]
remaining = remaining[1:]
val, exists := escapetoval[char]
if exists {
out.WriteString(val)
} else if char == 'c' {
out.WriteString(colour)
if len(remaining) < 2 || remaining[0] != '[' {
continue
}
// get colour names
var coloursBuffer string
remaining = remaining[1:]
for remaining[0] != ']' {
coloursBuffer += string(remaining[0])
remaining = remaining[1:]
}
remaining = remaining[1:] // strip final ']'
colours := strings.Split(coloursBuffer, ",")
var foreColour, backColour string
foreColour = colours[0]
if 1 < len(colours) {
backColour = colours[1]
}
// decide whether we can use truncated colour codes
canUseTruncated := len(remaining) < 1 || !strings.Contains(colours1, string(remaining[0]))
// turn colour names into real codes
var foreColourCode, backColourCode string
var exists bool
if backColour != "" || canUseTruncated {
foreColourCode, exists = colourcodesTruncated[foreColour]
} else {
foreColourCode, exists = colourcodesFull[foreColour]
}
if exists {
foreColour = foreColourCode
}
if backColour != "" {
if canUseTruncated {
backColourCode, exists = colourcodesTruncated[backColour]
} else {
backColourCode, exists = colourcodesFull[backColour]
}
if exists {
backColour = backColourCode
}
}
// output colour codes
out.WriteString(foreColour)
if backColour != "" {
out.WriteRune(',')
out.WriteString(backColour)
}
} else {
// unknown char
out.WriteRune(char)
}
} else {
out.WriteRune(char)
}
}
return out.String()
}