// written by Daniel Oaks // 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() }