1 /++ 2 + Module for dealing with IRC formatting. 3 +/ 4 module virc.style; 5 6 import std.range : isOutputRange; 7 8 ///Names for the default colours in mIRC-style control codes. 9 enum MIRCColours { 10 ///RGB(255,255,255) 11 white = 0, 12 ///RGB(0,0,0) 13 black = 1, 14 ///RGB(0,0,127) 15 blue = 2, 16 ///RGB(0,147,0) 17 green = 3, 18 ///RGB(255,0,0) 19 lightRed = 4, 20 ///RGB(127,0,0) 21 brown = 5, 22 ///RGB(156,0,156) 23 purple = 6, 24 ///RGB(252,127,0) 25 orange = 7, 26 ///RGB(255,255,0) 27 yellow = 8, 28 ///RGB(0,252,0) 29 lightGreen = 9, 30 ///RGB(0,147,147) 31 cyan = 10, 32 ///RGB(0,255,255) 33 lightCyan = 11, 34 ///RGB(0,0,252) 35 lightBlue = 12, 36 ///RGB(255,0,255) 37 pink = 13, 38 ///RGB(127,127,127) 39 grey = 14, 40 ///RGB(210,210,210) 41 lightGrey = 15, 42 ///"Default" colour 43 transparent = 99 44 } 45 46 ///Characters that indicate text style changes. 47 enum ControlCharacters { 48 ///Bold text 49 bold = '\x02', 50 ///Underlined text 51 underline = '\x1F', 52 ///Italicized text 53 italic = '\x1D', 54 ///Resets all following text to default style 55 plain = '\x0F', 56 ///Coloured text and/or background. 57 color = '\x03', 58 ///As colour, but extended to 24 bit colour. 59 extendedColor = '\x04', 60 ///Text where the background and foreground colours are reversed. 61 reverse = '\x16', 62 ///Text where every character has the same width 63 monospace = '\x11', 64 ///Text with a line through the middle 65 strikethrough = '\x1E' 66 } 67 68 69 auto colouredText(string fmt = "%s", T)(ulong f, ulong b, T val) { 70 return ColouredText!(fmt, T)(f, b, val); 71 } 72 auto colouredText(string fmt = "%s", T)(ulong f, T val) { 73 return ColouredText!(fmt, T)(f, val); 74 } 75 auto colouredText(string fmt = "%s", T)(RGBA32 f, RGBA32 b, T val) { 76 return ColouredText!(fmt, T)(f.closestMIRCColour, b.closestMIRCColour, val); 77 } 78 auto colouredText(string fmt = "%s", T)(RGBA32 f, T val) { 79 return ColouredText!(fmt, T)(f.closestMIRCColour, val); 80 } 81 struct ColouredText(string fmt, T) { 82 ulong fg; 83 ulong bg; 84 bool hasBG; 85 T thing; 86 87 void toString(T)(T sink) const if (isOutputRange!(T, const(char))) { 88 import std.format : formattedWrite; 89 if (fg == ulong.max) { 90 sink.formattedWrite!fmt(thing); 91 } else if (hasBG) { 92 sink.formattedWrite!(ControlCharacters.color~"%02d,%02d"~fmt~ControlCharacters.color)(fg, bg, thing); 93 } else { 94 sink.formattedWrite!(ControlCharacters.color~"%02d"~fmt~ControlCharacters.color)(fg, thing); 95 } 96 } 97 this(ulong f, ulong b, T str) @safe pure nothrow @nogc { 98 fg = f; 99 bg = b; 100 thing = str; 101 hasBG = true; 102 } 103 this(ulong f, T str) @safe pure nothrow @nogc { 104 fg = f; 105 thing = str; 106 } 107 } 108 /// 109 @safe unittest { 110 import std.conv : text; 111 import std.outbuffer; 112 ColouredText!("%s", ulong)().toString(new OutBuffer); 113 assert(colouredText!"Test %s"(1,2,3).text == "\x0301,02Test 3\x03"); 114 assert(colouredText!"Test %s"(1,3).text == "\x0301Test 3\x03"); 115 } 116 117 private struct StyledText(string fmt, char controlCode, T) { 118 T thing; 119 120 void toString(T)(T sink) const if (isOutputRange!(T, const(char))) { 121 import std.format : formattedWrite; 122 sink.formattedWrite(controlCode~fmt~controlCode, thing); 123 } 124 } 125 /// 126 @safe unittest { 127 import std.outbuffer; 128 StyledText!("%s", ControlCharacters.bold, ulong)().toString(new OutBuffer); 129 } 130 auto underlinedText(string fmt = "%s", T)(T val) { 131 return StyledText!(fmt, ControlCharacters.underline, T)(val); 132 } 133 /// 134 @safe unittest { 135 import std.conv : text; 136 assert(underlinedText!"Test %s"(3).text == "\x1FTest 3\x1F"); 137 } 138 139 auto boldText(string fmt = "%s", T)(T val) { 140 return StyledText!(fmt, ControlCharacters.bold, T)(val); 141 } 142 /// 143 @safe unittest { 144 import std.conv : text; 145 assert(boldText!"Test %s"(3).text == "\x02Test 3\x02"); 146 } 147 148 auto italicText(string fmt = "%s", T)(T val) { 149 return StyledText!(fmt, ControlCharacters.italic, T)(val); 150 } 151 /// 152 @safe unittest { 153 import std.conv : text; 154 assert(italicText!"Test %s"(3).text == "\x1DTest 3\x1D"); 155 } 156 157 158 auto reverseText(string fmt = "%s", T)(T val) { 159 return StyledText!(fmt, ControlCharacters.reverse, T)(val); 160 } 161 /// 162 @safe unittest { 163 import std.conv : text; 164 assert(reverseText!"Test %s"(3).text == "\x16Test 3\x16"); 165 } 166 167 auto monospaceText(string fmt = "%s", T)(T val) { 168 return StyledText!(fmt, ControlCharacters.monospace, T)(val); 169 } 170 /// 171 @safe unittest { 172 import std.conv : text; 173 assert(monospaceText!"Test %s"(3).text == "\x11Test 3\x11"); 174 } 175 176 auto strikethroughText(string fmt = "%s", T)(T val) { 177 return StyledText!(fmt, ControlCharacters.strikethrough, T)(val); 178 } 179 /// 180 @safe unittest { 181 import std.conv : text; 182 assert(strikethroughText!"Test %s"(3).text == "\x1ETest 3\x1E"); 183 } 184 185 struct RGBA32 { 186 ubyte red; 187 ubyte green; 188 ubyte blue; 189 ubyte alpha; 190 float distance(const RGBA32 other) const @safe pure nothrow { 191 import std.math : sqrt; 192 return sqrt(cast(float)((red-other.red)^^2 + (blue-other.blue)^^2 + (green-other.green)^^2)); 193 } 194 auto closestMIRCColour() const @safe pure nothrow { 195 import std.algorithm : minIndex; 196 return mIRCColourDefs[].minIndex!((x,y) => x.distance(this) < y.distance(this)); 197 } 198 auto closestANSIColour() const @safe pure nothrow { 199 return ANSIColours[closestMIRCColour]; 200 } 201 static auto randomColour() @safe { 202 import std.random : uniform; 203 return RGBA32(uniform!(typeof(red))(),uniform!(typeof(green))(),uniform!(typeof(blue))(),0); 204 } 205 auto randomComplementaryColour() const @safe { 206 import std.random : uniform; 207 static immutable gamma = 2.2; 208 const L = 0.2126 * (cast(float)red / ubyte.max) + 209 0.7152 * (cast(float)green / ubyte.max) + 210 0.0722 * (cast(float)blue / ubyte.max); 211 return (L > 0.5) ? 212 RGBA32(cast(ubyte)uniform(0,127),cast(ubyte)uniform(0,127),cast(ubyte)uniform(0,127),0) : 213 RGBA32(cast(ubyte)uniform(127, 255),cast(ubyte)uniform(127, 255),cast(ubyte)uniform(127, 255),0); 214 } 215 } 216 217 /// 218 unittest { 219 assert(RGBA32(0,0,0,0).closestMIRCColour == 1); 220 assert(RGBA32(0,0,1,0).closestMIRCColour == 1); 221 assert(RGBA32(0,0,255,0).closestMIRCColour == 60); 222 } 223 224 immutable RGBA32[100] mIRCColourDefs = [ 225 RGBA32(255,255,255,0), 226 RGBA32(0,0,0,0), 227 RGBA32(0,0,127,0), 228 RGBA32(0,147,0,0), 229 RGBA32(255,0,0,0), 230 RGBA32(127,0,0,0), 231 RGBA32(156,0,156,0), 232 RGBA32(252,127,0,0), 233 RGBA32(255,255,0,0), 234 RGBA32(0,252,0,0), 235 RGBA32(0,147,147,0), 236 RGBA32(0,255,255,0), 237 RGBA32(0,0,252,0), 238 RGBA32(255,0,255,0), 239 RGBA32(127,127,127,0), 240 RGBA32(210,210,210,0), 241 RGBA32(71,0,0,0), 242 RGBA32(71,33,0,0), 243 RGBA32(71,71,0,0), 244 RGBA32(50,71,0,0), 245 RGBA32(0,71,0,0), 246 RGBA32(0,71,44,0), 247 RGBA32(0,71,71,0), 248 RGBA32(0,39,71,0), 249 RGBA32(0,0,71,0), 250 RGBA32(46,0,71,0), 251 RGBA32(71,0,71,0), 252 RGBA32(71,0,42,0), 253 RGBA32(116,0,0,0), 254 RGBA32(116,58,0,0), 255 RGBA32(116,116,0,0), 256 RGBA32(81,116,0,0), 257 RGBA32(0,116,0,0), 258 RGBA32(0,116,73,0), 259 RGBA32(0,116,116,0), 260 RGBA32(0,64,116,0), 261 RGBA32(0,0,116,0), 262 RGBA32(75,0,116,0), 263 RGBA32(116,0,116,0), 264 RGBA32(116,0,69,0), 265 RGBA32(181,0,0,0), 266 RGBA32(181,99,0,0), 267 RGBA32(181,181,0,0), 268 RGBA32(125,181,0,0), 269 RGBA32(0,181,0,0), 270 RGBA32(0,181,113,0), 271 RGBA32(0,181,181,0), 272 RGBA32(0,99,181,0), 273 RGBA32(0,0,181,0), 274 RGBA32(117,0,181,0), 275 RGBA32(181,0,181,0), 276 RGBA32(181,0,107,0), 277 RGBA32(255,0,0,0), 278 RGBA32(255,140,0,0), 279 RGBA32(255,255,0,0), 280 RGBA32(178,255,0,0), 281 RGBA32(0,255,0,0), 282 RGBA32(0,255,160,0), 283 RGBA32(0,255,255,0), 284 RGBA32(0,140,255,0), 285 RGBA32(0,0,255,0), 286 RGBA32(165,0,255,0), 287 RGBA32(255,0,255,0), 288 RGBA32(255,0,152,0), 289 RGBA32(255,89,89,0), 290 RGBA32(255,180,89,0), 291 RGBA32(255,255,113,0), 292 RGBA32(207,255,96,0), 293 RGBA32(111,255,111,0), 294 RGBA32(101,255,201,0), 295 RGBA32(109,255,255,0), 296 RGBA32(89,180,255,0), 297 RGBA32(89,89,255,0), 298 RGBA32(196,89,255,0), 299 RGBA32(255,102,255,0), 300 RGBA32(255,89,188,0), 301 RGBA32(255,156,156,0), 302 RGBA32(255,211,156,0), 303 RGBA32(255,255,156,0), 304 RGBA32(226,255,156,0), 305 RGBA32(156,255,156,0), 306 RGBA32(156,255,219,0), 307 RGBA32(156,255,255,0), 308 RGBA32(156,211,255,0), 309 RGBA32(156,156,255,0), 310 RGBA32(220,156,255,0), 311 RGBA32(255,156,255,0), 312 RGBA32(255,148,211,0), 313 RGBA32(0,0,0,0), 314 RGBA32(19,19,19,0), 315 RGBA32(40,40,40,0), 316 RGBA32(54,54,54,0), 317 RGBA32(77,77,77,0), 318 RGBA32(101,101,101,0), 319 RGBA32(129,129,129,0), 320 RGBA32(159,159,159,0), 321 RGBA32(188,188,188,0), 322 RGBA32(226,226,226,0), 323 RGBA32(255,255,255,0), 324 RGBA32(0,0,0,255) 325 ]; 326 327 immutable ubyte[100] ANSIColours = [ 328 15, 329 0, 330 4, 331 2, 332 9, 333 1, 334 5, 335 202, 336 11, 337 10, 338 6, 339 14, 340 12, 341 13, 342 8, 343 7, 344 52, 345 94, 346 100, 347 58, 348 22, 349 29, 350 23, 351 24, 352 17, 353 54, 354 53, 355 89, 356 88, 357 130, 358 142, 359 64, 360 28, 361 35, 362 30, 363 25, 364 18, 365 91, 366 90, 367 125, 368 124, 369 166, 370 184, 371 106, 372 34, 373 49, 374 37, 375 33, 376 19, 377 129, 378 127, 379 161, 380 196, 381 208, 382 226, 383 154, 384 46, 385 86, 386 51, 387 75, 388 21, 389 171, 390 201, 391 198, 392 203, 393 215, 394 227, 395 191, 396 83, 397 122, 398 87, 399 111, 400 63, 401 177, 402 207, 403 205, 404 217, 405 223, 406 229, 407 193, 408 157, 409 158, 410 159, 411 153, 412 147, 413 183, 414 219, 415 212, 416 16, 417 233, 418 235, 419 237, 420 239, 421 241, 422 244, 423 247, 424 250, 425 254, 426 231, 427 0 428 ]; 429 430 struct ActiveFormatting { 431 bool bold; 432 bool underline; 433 bool strikethrough; 434 bool italic; 435 bool reverse; 436 bool monospace; 437 string colour; 438 } 439 440 ActiveFormatting checkFormatting(string input) @safe pure { 441 ActiveFormatting result; 442 for (size_t offset; offset < input.length; offset++) { 443 switch (input[offset]) { 444 case ControlCharacters.bold: 445 result.bold ^= true; 446 break; 447 case ControlCharacters.underline: 448 result.underline ^= true; 449 break; 450 case ControlCharacters.italic: 451 result.italic ^= true; 452 break; 453 case ControlCharacters.plain: 454 result.bold = false; 455 result.underline = false; 456 result.strikethrough = false; 457 result.reverse = false; 458 result.monospace = false; 459 result.italic = false; 460 result.colour = ""; 461 break; 462 case ControlCharacters.color: 463 bool sawComma; 464 size_t o = offset + 1; 465 size_t seenDigits = 0; 466 for (; o < input.length; o++) { 467 if ((input[o] == ',')) { 468 if ((seenDigits == 0) || sawComma) { 469 break; 470 } 471 sawComma = true; 472 seenDigits = 0; 473 continue; 474 } 475 if ((input[o] < '0') || (input[o] > '9')) { 476 break; 477 } 478 if (++seenDigits > 2) { 479 break; 480 } 481 } 482 if ((o - offset) > 1) { 483 result.colour = input[offset .. o]; 484 } else { 485 result.colour = ""; 486 } 487 offset = o - 1; //account for loop's offset++ 488 break; 489 case ControlCharacters.extendedColor: 490 bool sawComma; 491 size_t o = offset + 1; 492 size_t seenDigits = 0; 493 for (; o < input.length; o++) { 494 if ((input[o] == ',')) { 495 if ((seenDigits == 0) || sawComma) { 496 break; 497 } 498 sawComma = true; 499 seenDigits = 0; 500 continue; 501 } 502 if (!(((input[o] >= '0') && (input[o] <= '9')) || 503 ((input[o] >= 'A') && (input[o] <= 'F')) || 504 ((input[o] >= 'a') && (input[o] <= 'f')))) { 505 break; 506 } 507 if (++seenDigits > 6) { 508 break; 509 } 510 } 511 if ((o - offset) > 1) { 512 result.colour = input[offset .. o]; 513 } else { 514 result.colour = ""; 515 } 516 offset = o - 1; //account for loop's offset++ 517 break; 518 case ControlCharacters.reverse: 519 result.reverse ^= true; 520 break; 521 case ControlCharacters.monospace: 522 result.monospace ^= true; 523 break; 524 case ControlCharacters.strikethrough: 525 result.strikethrough ^= true; 526 break; 527 default: break; 528 } 529 } 530 return result; 531 } 532 533 @safe pure unittest { 534 with(checkFormatting("")) { 535 assert(bold == false); 536 assert(underline == false); 537 assert(strikethrough == false); 538 assert(italic == false); 539 assert(reverse == false); 540 assert(monospace == false); 541 assert(colour == ""); 542 } 543 with(checkFormatting("\x1F")) { 544 assert(bold == false); 545 assert(underline == true); 546 assert(strikethrough == false); 547 assert(italic == false); 548 assert(reverse == false); 549 assert(monospace == false); 550 assert(colour == ""); 551 } 552 with(checkFormatting("\x1F\x1F")) { 553 assert(bold == false); 554 assert(underline == false); 555 assert(strikethrough == false); 556 assert(italic == false); 557 assert(reverse == false); 558 assert(monospace == false); 559 assert(colour == ""); 560 } 561 with(checkFormatting("\x0310")) { 562 assert(bold == false); 563 assert(underline == false); 564 assert(strikethrough == false); 565 assert(italic == false); 566 assert(reverse == false); 567 assert(monospace == false); 568 assert(colour == "\x0310"); 569 } 570 with(checkFormatting("\x0310,12")) { 571 assert(bold == false); 572 assert(underline == false); 573 assert(strikethrough == false); 574 assert(italic == false); 575 assert(reverse == false); 576 assert(monospace == false); 577 assert(colour == "\x0310,12"); 578 } 579 with(checkFormatting("\x0310\x03")) { 580 assert(bold == false); 581 assert(underline == false); 582 assert(strikethrough == false); 583 assert(italic == false); 584 assert(reverse == false); 585 assert(monospace == false); 586 assert(colour == ""); 587 } 588 with(checkFormatting("\x04FFFFFF")) { 589 assert(bold == false); 590 assert(underline == false); 591 assert(strikethrough == false); 592 assert(italic == false); 593 assert(reverse == false); 594 assert(monospace == false); 595 assert(colour == "\x04FFFFFF"); 596 } 597 with(checkFormatting("\x04FFFFFF,000000")) { 598 assert(bold == false); 599 assert(underline == false); 600 assert(strikethrough == false); 601 assert(italic == false); 602 assert(reverse == false); 603 assert(monospace == false); 604 assert(colour == "\x04FFFFFF,000000"); 605 } 606 }