summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/lrstanley/girc/format.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/lrstanley/girc/format.go')
-rw-r--r--vendor/github.com/lrstanley/girc/format.go216
1 files changed, 195 insertions, 21 deletions
diff --git a/vendor/github.com/lrstanley/girc/format.go b/vendor/github.com/lrstanley/girc/format.go
index 85e3e387..3b9d60af 100644
--- a/vendor/github.com/lrstanley/girc/format.go
+++ b/vendor/github.com/lrstanley/girc/format.go
@@ -7,13 +7,21 @@ package girc
import (
"bytes"
"fmt"
+ "net/url"
"regexp"
"strings"
+ "unicode/utf8"
)
const (
- fmtOpenChar = '{'
- fmtCloseChar = '}'
+ fmtOpenChar = '{'
+ fmtCloseChar = '}'
+ maxWordSplitLength = 30
+)
+
+var (
+ reCode = regexp.MustCompile(`(\x02|\x1d|\x0f|\x03|\x16|\x1f|\x01)`)
+ reColor = regexp.MustCompile(`\x03([019]?\d(,[019]?\d)?)`)
)
var fmtColors = map[string]int{
@@ -66,9 +74,9 @@ var fmtCodes = map[string]string{
//
// For example:
//
-// client.Message("#channel", Fmt("{red}{b}Hello {red,blue}World{c}"))
+// client.Message("#channel", Fmt("{red}{b}Hello {red,blue}World{c}"))
func Fmt(text string) string {
- var last = -1
+ last := -1
for i := 0; i < len(text); i++ {
if text[i] == fmtOpenChar {
last = i
@@ -136,16 +144,12 @@ func TrimFmt(text string) string {
return text
}
-// This is really the only fastest way of doing this (marginally better than
-// actually trying to parse it manually.)
-var reStripColor = regexp.MustCompile(`\x03([019]?\d(,[019]?\d)?)?`)
-
// StripRaw tries to strip all ASCII format codes that are used for IRC.
// Primarily, foreground/background colors, and other control bytes like
// reset, bold, italic, reverse, etc. This also is done in a specific way
// in order to ensure no truncation of other non-irc formatting.
func StripRaw(text string) string {
- text = reStripColor.ReplaceAllString(text, "")
+ text = reColor.ReplaceAllString(text, "")
for _, code := range fmtCodes {
text = strings.ReplaceAll(text, code, "")
@@ -164,12 +168,12 @@ func StripRaw(text string) string {
// all ASCII printable chars. This function will NOT do that for
// compatibility reasons.
//
-// channel = ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring
-// [ ":" chanstring ]
-// chanstring = 0x01-0x07 / 0x08-0x09 / 0x0B-0x0C / 0x0E-0x1F / 0x21-0x2B
-// chanstring = / 0x2D-0x39 / 0x3B-0xFF
-// ; any octet except NUL, BELL, CR, LF, " ", "," and ":"
-// channelid = 5( 0x41-0x5A / digit ) ; 5( A-Z / 0-9 )
+// channel = ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring
+// [ ":" chanstring ]
+// chanstring = 0x01-0x07 / 0x08-0x09 / 0x0B-0x0C / 0x0E-0x1F / 0x21-0x2B
+// chanstring = / 0x2D-0x39 / 0x3B-0xFF
+// ; any octet except NUL, BELL, CR, LF, " ", "," and ":"
+// channelid = 5( 0x41-0x5A / digit ) ; 5( A-Z / 0-9 )
func IsValidChannel(channel string) bool {
if len(channel) <= 1 || len(channel) > 50 {
return false
@@ -214,10 +218,10 @@ func IsValidChannel(channel string) bool {
// IsValidNick validates an IRC nickname. Note that this does not validate
// IRC nickname length.
//
-// nickname = ( letter / special ) *8( letter / digit / special / "-" )
-// letter = 0x41-0x5A / 0x61-0x7A
-// digit = 0x30-0x39
-// special = 0x5B-0x60 / 0x7B-0x7D
+// nickname = ( letter / special ) *8( letter / digit / special / "-" )
+// letter = 0x41-0x5A / 0x61-0x7A
+// digit = 0x30-0x39
+// special = 0x5B-0x60 / 0x7B-0x7D
func IsValidNick(nick string) bool {
if nick == "" {
return false
@@ -253,8 +257,9 @@ func IsValidNick(nick string) bool {
// not be supported on all networks. Some limit this to only a single period.
//
// Per RFC:
-// user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
-// ; any octet except NUL, CR, LF, " " and "@"
+//
+// user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
+// ; any octet except NUL, CR, LF, " " and "@"
func IsValidUser(name string) bool {
if name == "" {
return false
@@ -350,3 +355,172 @@ func Glob(input, match string) bool {
// Check suffix last.
return trailingGlob || strings.HasSuffix(input, parts[last])
}
+
+// sliceInsert inserts a string into a slice at a specific index, while trying
+// to avoid as many allocations as possible.
+func sliceInsert(input []string, i int, v ...string) []string {
+ total := len(input) + len(v)
+ if total <= cap(input) {
+ output := input[:total]
+ copy(output[i+len(v):], input[i:])
+ copy(output[i:], v)
+ return output
+ }
+ output := make([]string, total)
+ copy(output, input[:i])
+ copy(output[i:], v)
+ copy(output[i+len(v):], input[i:])
+ return output
+}
+
+// splitMessage is a text splitter that takes into consideration a few things:
+// - Ensuring the returned text is no longer than maxWidth.
+// - Attempting to split at the closest word boundary, while still staying inside
+// of the specific maxWidth.
+// - if there is no good word boundary for longer words (or e.g. links, raw data, etc)
+// that are above maxWordSplitLength characters, split the word into chunks to fit the
+//
+// maximum width.
+func splitMessage(input string, maxWidth int) (output []string) {
+ input = strings.ToValidUTF8(input, "?")
+
+ words := strings.FieldsFunc(strings.TrimSpace(input), func(r rune) bool {
+ switch r { // Same as unicode.IsSpace, but without ctrl/lf.
+ case '\t', '\v', '\f', ' ', 0x85, 0xA0:
+ return true
+ }
+ return false
+ })
+
+ output = []string{""}
+ codes := []string{}
+
+ var lastColor string
+ var match []string
+
+ for i := 0; i < len(words); i++ {
+ j := strings.IndexAny(words[i], "\n\r")
+ if j == -1 {
+ continue
+ }
+
+ word := words[i]
+ words[i] = word[:j]
+
+ words = sliceInsert(words, i+1, "", strings.TrimLeft(word[j:], "\n\r"))
+ }
+
+ for _, word := range words {
+ // Used in place of a single newline.
+ if word == "" {
+ // Last line was already empty or already only had control characters.
+ if output[len(output)-1] == "" || output[len(output)-1] == lastColor+word {
+ continue
+ }
+
+ output = append(output, strings.Join(codes, "")+lastColor+word)
+ continue
+ }
+
+ // Keep track of the last used color codes.
+ match = reColor.FindAllString(word, -1)
+ if len(match) > 0 {
+ lastColor = match[len(match)-1]
+ }
+
+ // Find all sequence codes -- this approach isn't perfect (ideally, a lexer
+ // should be used to track each exact type of code), but it's good enough for
+ // most cases.
+ match = reCode.FindAllString(word, -1)
+ if len(match) > 0 {
+ for _, m := range match {
+ // Reset was used, so clear all codes.
+ if m == fmtCodes["reset"] {
+ lastColor = ""
+ codes = []string{}
+ continue
+ }
+
+ // Check if we already have the code, and if so, remove it (closing).
+ contains := false
+ for i := 0; i < len(codes); i++ {
+ if m == codes[i] {
+ contains = true
+ codes = append(codes[:i], codes[i+1:]...)
+
+ // If it's a closing color code, reset the last used color
+ // as well.
+ if m == fmtCodes["clear"] {
+ lastColor = ""
+ }
+
+ break
+ }
+ }
+
+ // Track the new code, unless it's a color clear but we aren't
+ // tracking a color right now.
+ if !contains && (lastColor == "" || m != fmtCodes["clear"]) {
+ codes = append(codes, m)
+ }
+ }
+ }
+
+ checkappend:
+
+ // Check if we can append, otherwise we must split.
+ if 1+utf8.RuneCountInString(word)+utf8.RuneCountInString(output[len(output)-1]) < maxWidth {
+ if output[len(output)-1] != "" {
+ output[len(output)-1] += " "
+ }
+ output[len(output)-1] += word
+ continue
+ }
+
+ // If the word can fit on a line by itself, check if it's a url. If it is,
+ // put it on it's own line.
+ if utf8.RuneCountInString(word+strings.Join(codes, "")+lastColor) < maxWidth {
+ if _, err := url.Parse(word); err == nil {
+ output = append(output, strings.Join(codes, "")+lastColor+word)
+ continue
+ }
+ }
+
+ // Check to see if we can split by misc symbols, but must be at least a few
+ // characters long to be split by it.
+ if j := strings.IndexAny(word, "-+_=|/~:;,."); j > 3 && 1+utf8.RuneCountInString(word[0:j])+utf8.RuneCountInString(output[len(output)-1]) < maxWidth {
+ if output[len(output)-1] != "" {
+ output[len(output)-1] += " "
+ }
+ output[len(output)-1] += word[0:j]
+ word = word[j+1:]
+ goto checkappend
+ }
+
+ // If the word is longer than is acceptable to just put on the next line,
+ // split it into chunks. Also don't split the word if only a few characters
+ // left of the word would be on the next line.
+ if 1+utf8.RuneCountInString(word) > maxWordSplitLength && maxWidth-utf8.RuneCountInString(output[len(output)-1]) > 5 {
+ left := maxWidth - utf8.RuneCountInString(output[len(output)-1]) - 1 // -1 for the space
+
+ if output[len(output)-1] != "" {
+ output[len(output)-1] += " "
+ }
+ output[len(output)-1] += word[0:left]
+ word = word[left:]
+ goto checkappend
+ }
+
+ left := maxWidth - utf8.RuneCountInString(output[len(output)-1])
+ output[len(output)-1] += word[0:left]
+
+ output = append(output, strings.Join(codes, "")+lastColor)
+ word = word[left:]
+ goto checkappend
+ }
+
+ for i := 0; i < len(output); i++ {
+ output[i] = strings.ToValidUTF8(output[i], "?")
+ }
+ return output
+}