Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. // written by Daniel Oaks <daniel@danieloaks.net>
  2. // released under the ISC license
  3. package ircfmt
  4. import (
  5. "regexp"
  6. "strings"
  7. )
  8. const (
  9. // raw bytes and strings to do replacing with
  10. bold string = "\x02"
  11. colour string = "\x03"
  12. monospace string = "\x11"
  13. reverseColour string = "\x16"
  14. italic string = "\x1d"
  15. strikethrough string = "\x1e"
  16. underline string = "\x1f"
  17. reset string = "\x0f"
  18. runecolour rune = '\x03'
  19. runebold rune = '\x02'
  20. runemonospace rune = '\x11'
  21. runereverseColour rune = '\x16'
  22. runeitalic rune = '\x1d'
  23. runestrikethrough rune = '\x1e'
  24. runereset rune = '\x0f'
  25. runeunderline rune = '\x1f'
  26. // valid characters in a colour code character, for speed
  27. colours1 string = "0123456789"
  28. )
  29. var (
  30. // valtoescape replaces most of IRC characters with our escapes.
  31. valtoescape = strings.NewReplacer("$", "$$", colour, "$c", reverseColour, "$v", bold, "$b", italic, "$i", strikethrough, "$s", underline, "$u", monospace, "$m", reset, "$r")
  32. // valToStrip replaces most of the IRC characters with nothing
  33. valToStrip = strings.NewReplacer(colour, "$c", reverseColour, "", bold, "", italic, "", strikethrough, "", underline, "", monospace, "", reset, "")
  34. // escapetoval contains most of our escapes and how they map to real IRC characters.
  35. // intentionally skips colour, since that's handled elsewhere.
  36. escapetoval = map[rune]string{
  37. '$': "$",
  38. 'b': bold,
  39. 'i': italic,
  40. 'v': reverseColour,
  41. 's': strikethrough,
  42. 'u': underline,
  43. 'm': monospace,
  44. 'r': reset,
  45. }
  46. // valid colour codes
  47. numtocolour = map[string]string{
  48. "99": "default",
  49. "15": "light grey",
  50. "14": "grey",
  51. "13": "pink",
  52. "12": "light blue",
  53. "11": "light cyan",
  54. "10": "cyan",
  55. "09": "light green",
  56. "08": "yellow",
  57. "07": "orange",
  58. "06": "magenta",
  59. "05": "brown",
  60. "04": "red",
  61. "03": "green",
  62. "02": "blue",
  63. "01": "black",
  64. "00": "white",
  65. "9": "light green",
  66. "8": "yellow",
  67. "7": "orange",
  68. "6": "magenta",
  69. "5": "brown",
  70. "4": "red",
  71. "3": "green",
  72. "2": "blue",
  73. "1": "black",
  74. "0": "white",
  75. }
  76. colourcodesTruncated = map[string]string{
  77. "white": "0",
  78. "black": "1",
  79. "blue": "2",
  80. "green": "3",
  81. "red": "4",
  82. "brown": "5",
  83. "magenta": "6",
  84. "orange": "7",
  85. "yellow": "8",
  86. "light green": "9",
  87. "cyan": "10",
  88. "light cyan": "11",
  89. "light blue": "12",
  90. "pink": "13",
  91. "grey": "14",
  92. "light grey": "15",
  93. "default": "99",
  94. }
  95. bracketedExpr = regexp.MustCompile(`^\[.*?\]`)
  96. colourDigits = regexp.MustCompile(`^[0-9]{1,2}$`)
  97. )
  98. // Escape takes a raw IRC string and returns it with our escapes.
  99. //
  100. // IE, it turns this: "This is a \x02cool\x02, \x034red\x0f message!"
  101. // into: "This is a $bcool$b, $c[red]red$r message!"
  102. func Escape(in string) string {
  103. // replace all our usual escapes
  104. in = valtoescape.Replace(in)
  105. inRunes := []rune(in)
  106. //var out string
  107. out := strings.Builder{}
  108. for 0 < len(inRunes) {
  109. if 1 < len(inRunes) && inRunes[0] == '$' && inRunes[1] == 'c' {
  110. // handle colours
  111. out.WriteString("$c")
  112. inRunes = inRunes[2:] // strip colour code chars
  113. if len(inRunes) < 1 || !strings.Contains(colours1, string(inRunes[0])) {
  114. out.WriteString("[]")
  115. continue
  116. }
  117. var foreBuffer, backBuffer string
  118. foreBuffer += string(inRunes[0])
  119. inRunes = inRunes[1:]
  120. if 0 < len(inRunes) && strings.Contains(colours1, string(inRunes[0])) {
  121. foreBuffer += string(inRunes[0])
  122. inRunes = inRunes[1:]
  123. }
  124. if 1 < len(inRunes) && inRunes[0] == ',' && strings.Contains(colours1, string(inRunes[1])) {
  125. backBuffer += string(inRunes[1])
  126. inRunes = inRunes[2:]
  127. if 0 < len(inRunes) && strings.Contains(colours1, string(inRunes[0])) {
  128. backBuffer += string(inRunes[0])
  129. inRunes = inRunes[1:]
  130. }
  131. }
  132. foreName, exists := numtocolour[foreBuffer]
  133. if !exists {
  134. foreName = foreBuffer
  135. }
  136. backName, exists := numtocolour[backBuffer]
  137. if !exists {
  138. backName = backBuffer
  139. }
  140. out.WriteRune('[')
  141. out.WriteString(foreName)
  142. if backName != "" {
  143. out.WriteRune(',')
  144. out.WriteString(backName)
  145. }
  146. out.WriteRune(']')
  147. } else {
  148. // special case for $$c
  149. if len(inRunes) > 2 && inRunes[0] == '$' && inRunes[1] == '$' && inRunes[2] == 'c' {
  150. out.WriteRune(inRunes[0])
  151. out.WriteRune(inRunes[1])
  152. out.WriteRune(inRunes[2])
  153. inRunes = inRunes[3:]
  154. } else {
  155. out.WriteRune(inRunes[0])
  156. inRunes = inRunes[1:]
  157. }
  158. }
  159. }
  160. return out.String()
  161. }
  162. // Strip takes a raw IRC string and removes it with all formatting codes removed
  163. // IE, it turns this: "This is a \x02cool\x02, \x034red\x0f message!"
  164. // into: "This is a cool, red message!"
  165. func Strip(in string) string {
  166. out := strings.Builder{}
  167. runes := []rune(in)
  168. if out.Len() < len(runes) { // Reduce allocations where needed
  169. out.Grow(len(in) - out.Len())
  170. }
  171. for len(runes) > 0 {
  172. switch runes[0] {
  173. case runebold, runemonospace, runereverseColour, runeitalic, runestrikethrough, runeunderline, runereset:
  174. runes = runes[1:]
  175. case runecolour:
  176. runes = removeColour(runes)
  177. default:
  178. out.WriteRune(runes[0])
  179. runes = runes[1:]
  180. }
  181. }
  182. return out.String()
  183. }
  184. func removeNumber(runes []rune) []rune {
  185. if len(runes) > 0 && runes[0] >= '0' && runes[0] <= '9' {
  186. runes = runes[1:]
  187. }
  188. return runes
  189. }
  190. func removeColour(runes []rune) []rune {
  191. if runes[0] != runecolour {
  192. return runes
  193. }
  194. runes = runes[1:]
  195. runes = removeNumber(runes)
  196. runes = removeNumber(runes)
  197. if len(runes) > 1 && runes[0] == ',' && runes[1] >= '0' && runes[1] <= '9' {
  198. runes = runes[2:]
  199. } else {
  200. return runes // Nothing else because we dont have a comma
  201. }
  202. runes = removeNumber(runes)
  203. return runes
  204. }
  205. // resolve "light blue" to "12", "12" to "12", "asdf" to "", etc.
  206. func resolveToColourCode(str string) (result string) {
  207. str = strings.ToLower(strings.TrimSpace(str))
  208. if colourDigits.MatchString(str) {
  209. return str
  210. }
  211. return colourcodesTruncated[str]
  212. }
  213. // resolve "[light blue, black]" to ("13, "1")
  214. func resolveToColourCodes(namedColors string) (foreground, background string) {
  215. // cut off the brackets
  216. namedColors = strings.TrimPrefix(namedColors, "[")
  217. namedColors = strings.TrimSuffix(namedColors, "]")
  218. var foregroundStr, backgroundStr string
  219. commaIdx := strings.IndexByte(namedColors, ',')
  220. if commaIdx != -1 {
  221. foregroundStr = namedColors[:commaIdx]
  222. backgroundStr = namedColors[commaIdx+1:]
  223. } else {
  224. foregroundStr = namedColors
  225. }
  226. return resolveToColourCode(foregroundStr), resolveToColourCode(backgroundStr)
  227. }
  228. // Unescape takes our escaped string and returns a raw IRC string.
  229. //
  230. // IE, it turns this: "This is a $bcool$b, $c[red]red$r message!"
  231. // into this: "This is a \x02cool\x02, \x034red\x0f message!"
  232. func Unescape(in string) string {
  233. var out strings.Builder
  234. remaining := in
  235. for len(remaining) != 0 {
  236. char := remaining[0]
  237. remaining = remaining[1:]
  238. if char != '$' || len(remaining) == 0 {
  239. // not an escape
  240. out.WriteByte(char)
  241. continue
  242. }
  243. // ingest the next character of the escape
  244. char = remaining[0]
  245. remaining = remaining[1:]
  246. if char == 'c' {
  247. out.WriteString(colour)
  248. namedColors := bracketedExpr.FindString(remaining)
  249. if namedColors == "" {
  250. // for a non-bracketed color code, output the following characters directly,
  251. // e.g., `$c1,8` will become `\x031,8`
  252. continue
  253. }
  254. // process bracketed color codes:
  255. remaining = remaining[len(namedColors):]
  256. followedByDigit := len(remaining) != 0 && ('0' <= remaining[0] && remaining[0] <= '9')
  257. foreground, background := resolveToColourCodes(namedColors)
  258. if foreground != "" {
  259. if len(foreground) == 1 && background == "" && followedByDigit {
  260. out.WriteByte('0')
  261. }
  262. out.WriteString(foreground)
  263. if background != "" {
  264. out.WriteByte(',')
  265. if len(background) == 1 && followedByDigit {
  266. out.WriteByte('0')
  267. }
  268. out.WriteString(background)
  269. }
  270. }
  271. } else {
  272. val, exists := escapetoval[rune(char)]
  273. if exists {
  274. out.WriteString(val)
  275. } else {
  276. // invalid escape, use the raw char
  277. out.WriteByte(char)
  278. }
  279. }
  280. }
  281. return out.String()
  282. }