mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-11 14:39:31 +01:00
331 lines
8.0 KiB
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()
|
||
|
}
|