1 /++ 2 Functions related to IRC colouring and formatting; mapping it to ANSI 3 terminal such, stripping it, etc. 4 5 IRC colours are not in the standard per se, but there is a de-facto standard 6 based on the mIRC coluring syntax of `\3fg,bg...\3`, where '\3' is byte 3, 7 `fg` is a foreground colour number (of [IRCColour]) and `bg` is a similar 8 background colour number. 9 10 Example: 11 --- 12 immutable nameInColour = "kameloso".ircColour(IRCColour.red); 13 immutable nameInHashedColour = "kameloso".ircColourByHash(Yes.extendedOutgoingColours); 14 immutable nameInBold = "kameloso".ircBold; 15 --- 16 17 See_Also: 18 [kameloso.terminal.colours] 19 20 Copyright: [JR](https://github.com/zorael) 21 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 22 23 Authors: 24 [JR](https://github.com/zorael) 25 +/ 26 module kameloso.irccolours; 27 28 private: 29 30 import kameloso.terminal.colours.defs : TerminalBackground, TerminalForeground, 31 TerminalFormat, TerminalReset; 32 import dialect.common : IRCControlCharacter; 33 import std.range.primitives : isOutputRange; 34 import std.typecons : Flag, No, Yes; 35 36 37 public: 38 39 @safe: 40 41 /++ 42 Official mIRC colour table. 43 +/ 44 enum IRCColour 45 { 46 unset = -1, /// Unset 47 white = 0, /// White 48 black = 1, /// Black 49 blue = 2, /// Blue 50 green = 3, /// Green 51 red = 4, /// Red 52 brown = 5, /// Brown 53 magenta = 6, /// Magenta 54 orange = 7, /// Orange 55 yellow = 8, /// Yellow 56 lightgreen = 9, /// Light green 57 cyan = 10, /// Cyan 58 lightcyan = 11, /// Light cyan 59 lightblue = 12, /// Light blue 60 pink = 13, /// Pink 61 grey = 14, /// Grey 62 lightgrey = 15, /// Light grey 63 transparent = 99, /// "Transparent" 64 } 65 66 67 // ircANSIColourMap 68 /++ 69 Map of IRC colour values above 16 to ANSI terminal colours, as per ircdocs. 70 71 See_Also: 72 https://modern.ircdocs.horse/formatting.html#colors-16-98. 73 +/ 74 immutable uint[99] ircANSIColourMap = 75 [ 76 0 : TerminalForeground.default_, 77 1 : TerminalForeground.white, // replace with .black on bright terminals 78 2 : TerminalForeground.red, 79 3 : TerminalForeground.green, 80 4 : TerminalForeground.yellow, 81 5 : TerminalForeground.blue, 82 6 : TerminalForeground.magenta, 83 7 : TerminalForeground.cyan, 84 8 : TerminalForeground.lightgrey, 85 9 : TerminalForeground.darkgrey, 86 10 : TerminalForeground.lightred, 87 11 : TerminalForeground.lightgreen, 88 12 : TerminalForeground.lightyellow, 89 13 : TerminalForeground.lightblue, 90 14 : TerminalForeground.lightmagenta, 91 15 : TerminalForeground.lightcyan, 92 16 : 52, 93 17 : 94, 94 18 : 100, 95 19 : 58, 96 20 : 22, 97 21 : 29, 98 22 : 23, 99 23 : 24, 100 24 : 17, 101 25 : 54, 102 26 : 53, 103 27 : 89, 104 28 : 88, 105 29 : 130, 106 30 : 142, 107 31 : 64, 108 32 : 28, 109 33 : 35, 110 34 : 30, 111 35 : 25, 112 36 : 18, 113 37 : 91, 114 38 : 90, 115 39 : 125, 116 40 : 124, 117 41 : 166, 118 42 : 184, 119 43 : 106, 120 44 : 34, 121 45 : 49, 122 46 : 37, 123 47 : 33, 124 48 : 19, 125 49 : 129, 126 50 : 127, 127 51 : 161, 128 52 : 196, 129 53 : 208, 130 54 : 226, 131 55 : 154, 132 56 : 46, 133 57 : 86, 134 58 : 51, 135 59 : 75, 136 60 : 21, 137 61 : 171, 138 62 : 201, 139 63 : 198, 140 64 : 203, 141 65 : 215, 142 66 : 227, 143 67 : 191, 144 68 : 83, 145 69 : 122, 146 70 : 87, 147 71 : 111, 148 72 : 63, 149 73 : 177, 150 74 : 207, 151 75 : 205, 152 76 : 217, 153 77 : 223, 154 78 : 229, 155 79 : 193, 156 80 : 157, 157 81 : 158, 158 82 : 159, 159 83 : 153, 160 84 : 147, 161 85 : 183, 162 86 : 219, 163 87 : 212, 164 88 : 16, 165 89 : 233, 166 90 : 235, 167 91 : 237, 168 92 : 239, 169 93 : 241, 170 94 : 244, 171 95 : 247, 172 96 : 250, 173 97 : 254, 174 98 : 231, 175 ]; 176 177 178 // ircColourInto 179 /++ 180 Colour-codes the passed string with mIRC colouring, foreground and background. 181 Takes an output range sink and writes to it instead of allocating a new string. 182 183 Params: 184 line = Line to tint. 185 sink = Output range sink to fill with the function's output. 186 fg = Foreground [IRCColour] integer. 187 bg = Optional background [IRCColour] integer. 188 +/ 189 void ircColourInto(Sink) 190 (const string line, 191 auto ref Sink sink, 192 const int fg, 193 const int bg = IRCColour.unset) 194 if (isOutputRange!(Sink, char[])) 195 in (line.length, "Tried to apply IRC colours to a string but no string was given") 196 { 197 import lu.conv : toAlphaInto; 198 199 sink.put(cast(char)IRCControlCharacter.colour); 200 (cast(int)fg).toAlphaInto!(2, 2)(sink); // So far the highest colour seems to be 99; two digits 201 202 if (bg != IRCColour.unset) 203 { 204 sink.put(','); 205 (cast(int)bg).toAlphaInto!(2, 2)(sink); 206 } 207 208 sink.put(line); 209 sink.put(cast(char)IRCControlCharacter.colour); 210 } 211 212 /// 213 unittest 214 { 215 import std.array : Appender; 216 217 alias I = IRCControlCharacter; 218 Appender!(char[]) sink; 219 220 "kameloso".ircColourInto(sink, IRCColour.red, IRCColour.white); 221 assert((sink.data == I.colour ~ "04,00kameloso" ~ I.colour), sink.data); 222 sink.clear(); 223 224 "harbl".ircColourInto(sink, IRCColour.green); 225 assert((sink.data == I.colour ~ "03harbl" ~ I.colour), sink.data); 226 } 227 228 229 // ircColour 230 /++ 231 Colour-codes the passed string with mIRC colouring, foreground and background. 232 Direct overload that leverages the output range version to colour an internal 233 [std.array.Appender|Appender], and returns the resulting string. 234 235 Params: 236 line = Line to tint. 237 fg = Foreground [IRCColour] integer. 238 bg = Optional background [IRCColour] integer. 239 240 Returns: 241 The passed line, encased within IRC colour tags. 242 +/ 243 string ircColour( 244 const string line, 245 const int fg, 246 const int bg = IRCColour.unset) pure 247 in (line.length, "Tried to apply IRC colours to a string but no string was given") 248 { 249 import std.array : Appender; 250 251 if (!line.length) return string.init; 252 253 Appender!(char[]) sink; 254 255 sink.reserve(line.length + 7); // Two colour tokens, four colour numbers and a comma 256 line.ircColourInto(sink, fg, bg); 257 return sink.data; 258 } 259 260 /// 261 unittest 262 { 263 alias I = IRCControlCharacter; 264 265 immutable redwhite = "kameloso".ircColour(IRCColour.red, IRCColour.white); 266 assert((redwhite == I.colour ~ "04,00kameloso" ~ I.colour), redwhite); 267 268 immutable green = "harbl".ircColour(IRCColour.green); 269 assert((green == I.colour ~ "03harbl" ~ I.colour), green); 270 } 271 272 273 // ircColour 274 /++ 275 Returns a mIRC colour code for the passed foreground and background colour. 276 Overload that doesn't take a string to tint, only the [IRCColour]s to 277 produce a colour code from. 278 279 Params: 280 fg = Foreground [IRCColour]. 281 bg = Optional background [IRCColour]. 282 283 Returns: 284 An opening IRC colour token with the passed colours. 285 +/ 286 string ircColour(const IRCColour fg, const IRCColour bg = IRCColour.unset) pure 287 { 288 import lu.conv : toAlphaInto; 289 import std.array : Appender; 290 291 Appender!(char[]) sink; 292 sink.reserve(6); 293 294 sink.put(cast(char)IRCControlCharacter.colour); 295 (cast(int)fg).toAlphaInto!(2, 2)(sink); 296 297 if (bg != IRCColour.unset) 298 { 299 sink.put(','); 300 (cast(int)bg).toAlphaInto!(2, 2)(sink); 301 } 302 303 return sink.data; 304 } 305 306 /// 307 unittest 308 { 309 alias I = IRCControlCharacter; 310 311 with (IRCColour) 312 { 313 { 314 immutable line = "abcdefg".ircColour(white); 315 immutable expected = I.colour ~ "00abcdefg" ~ I.colour; 316 assert((line == expected), line); 317 } 318 { 319 immutable line = "abcdefg".ircBold; 320 immutable expected = I.bold ~ "abcdefg" ~ I.bold; 321 assert((line == expected), line); 322 } 323 { 324 immutable line = ircColour(white) ~ "abcdefg" ~ I.reset; 325 immutable expected = I.colour ~ "00abcdefg" ~ I.reset; 326 assert((line == expected), line); 327 } 328 { 329 immutable line = "" ~ I.bold ~ I.underlined ~ ircColour(green) ~ 330 "abcdef" ~ "ghijkl".ircColour(red) ~ I.reset; 331 immutable expected = "" ~ I.bold ~ I.underlined ~ I.colour ~ "03abcdef" ~ 332 I.colour ~ "04ghijkl" ~ I.colour ~ I.reset; 333 assert((line == expected), line); 334 335 immutable expressedDifferently = ircBold(ircUnderlined("abcdef".ircColour(green) ~ 336 "ghijkl".ircColour(red))); 337 immutable expectedDifferently = "" ~ I.bold ~ I.underlined ~ I.colour ~ 338 "03abcdef" ~ I.colour ~ I.colour ~ "04ghijkl" ~ I.colour ~ 339 I.underlined ~ I.bold; 340 assert((expressedDifferently == expectedDifferently), expressedDifferently); 341 } 342 { 343 immutable account = "kameloso"; 344 immutable authorised = "not authorised"; 345 immutable line = "Account " ~ ircBold(account) ~ ": " ~ ircUnderlined(authorised) ~ "!"; 346 immutable expected = "Account " ~ I.bold ~ "kameloso" ~ I.bold ~ ": " ~ 347 I.underlined ~ "not authorised" ~ I.underlined ~ "!"; 348 assert((line == expected), line); 349 } 350 } 351 } 352 353 354 // ircColourByHash 355 /++ 356 Returns the passed string coloured with an IRC colour depending on the hash 357 of the string, making for good "random" (uniformly distributed) nick colours 358 in IRC messages. 359 360 Params: 361 word = String to tint. 362 extendedOutgoingColours = Whether or not to use extended colours (16-98). 363 364 Returns: 365 The passed string encased within IRC colour coding. 366 +/ 367 string ircColourByHash( 368 const string word, 369 const Flag!"extendedOutgoingColours" extendedOutgoingColours) pure 370 in (word.length, "Tried to apply IRC colours by hash to a string but no string was given") 371 { 372 import lu.conv : toAlphaInto; 373 import std.array : Appender; 374 375 if (!word.length) return string.init; 376 377 Appender!(char[]) sink; 378 sink.reserve(word.length + 4); // colour, index, word, colour 379 380 immutable modulo = extendedOutgoingColours ? ircANSIColourMap.length : 16; 381 immutable colourInteger = (hashOf(word) % modulo); 382 383 sink.put(cast(char)IRCControlCharacter.colour); 384 colourInteger.toAlphaInto!(2, 2)(sink); 385 sink.put(word); 386 sink.put(cast(char)IRCControlCharacter.colour); 387 388 return sink.data; 389 } 390 391 /// 392 unittest 393 { 394 alias I = IRCControlCharacter; 395 396 // Colour based on hash 397 398 { 399 immutable actual = "kameloso".ircColourByHash(Yes.extendedOutgoingColours); 400 immutable expected = I.colour ~ "23kameloso" ~ I.colour; 401 assert((actual == expected), actual); 402 } 403 { 404 immutable actual = "kameloso^".ircColourByHash(Yes.extendedOutgoingColours); 405 immutable expected = I.colour ~ "56kameloso^" ~ I.colour; 406 assert((actual == expected), actual); 407 } 408 { 409 immutable actual = "kameloso^11".ircColourByHash(Yes.extendedOutgoingColours); 410 immutable expected = I.colour ~ "91kameloso^11" ~ I.colour; 411 assert((actual == expected), actual); 412 } 413 { 414 immutable actual = "flerrp".ircColourByHash(Yes.extendedOutgoingColours); 415 immutable expected = I.colour ~ "90flerrp" ~ I.colour; 416 assert((actual == expected), actual); 417 } 418 } 419 420 421 // ircBold 422 /++ 423 Returns the passed something wrapped in between IRC bold control characters. 424 425 Params: 426 something = Something [std.conv.to]-convertible to enwrap in bold. 427 428 Returns: 429 The passed something, as a string, in IRC bold. 430 +/ 431 auto ircBold(T)(T something) //pure nothrow 432 { 433 import std.conv : text; 434 435 alias I = IRCControlCharacter; 436 return text(cast(char)I.bold, something, cast(char)I.bold); 437 } 438 439 /// 440 unittest 441 { 442 import std.conv : to; 443 alias I = IRCControlCharacter; 444 445 { 446 immutable line = "kameloso: " ~ ircBold("kameloso"); 447 immutable expected = "kameloso: " ~ I.bold ~ "kameloso" ~ I.bold; 448 assert((line == expected), line); 449 } 450 { 451 immutable number = 1234; 452 immutable line = number.ircBold; 453 immutable expected = I.bold ~ number.to!string ~ I.bold; 454 assert((line == expected), line); 455 } 456 { 457 immutable b = true; 458 immutable line = b.ircBold; 459 immutable expected = I.bold ~ "true" ~ I.bold; 460 assert((line == expected), line); 461 } 462 } 463 464 465 // ircItalics 466 /++ 467 Returns the passed something wrapped in between IRC italics control characters. 468 469 Params: 470 something = Something [std.conv.to]-convertible to enwrap in italics. 471 472 Returns: 473 The passed something, as a string, in IRC italics. 474 +/ 475 auto ircItalics(T)(T something) //pure nothrow 476 { 477 import std.conv : text; 478 479 alias I = IRCControlCharacter; 480 return text(cast(char)I.italics, something, cast(char)I.italics); 481 } 482 483 /// 484 unittest 485 { 486 import std.conv : to; 487 alias I = IRCControlCharacter; 488 489 { 490 immutable line = "kameloso: " ~ ircItalics("kameloso"); 491 immutable expected = "kameloso: " ~ I.italics ~ "kameloso" ~ I.italics; 492 assert((line == expected), line); 493 } 494 { 495 immutable number = 1234; 496 immutable line = number.ircItalics; 497 immutable expected = I.italics ~ number.to!string ~ I.italics; 498 assert((line == expected), line); 499 } 500 { 501 immutable b = true; 502 immutable line = b.ircItalics; 503 immutable expected = I.italics ~ "true" ~ I.italics; 504 assert((line == expected), line); 505 } 506 } 507 508 509 // ircUnderlined 510 /++ 511 Returns the passed something wrapped in between IRC underlined control characters. 512 513 Params: 514 something = Something [std.conv.to]-convertible to enwrap in underlined. 515 516 Returns: 517 The passed something, as a string, in IRC underlined. 518 +/ 519 auto ircUnderlined(T)(T something) //pure nothrow 520 { 521 import std.conv : text; 522 523 alias I = IRCControlCharacter; 524 return text(cast(char)I.underlined, something, cast(char)I.underlined); 525 } 526 527 /// 528 unittest 529 { 530 import std.conv : to; 531 alias I = IRCControlCharacter; 532 533 { 534 immutable line = "kameloso: " ~ ircUnderlined("kameloso"); 535 immutable expected = "kameloso: " ~ I.underlined ~ "kameloso" ~ I.underlined; 536 assert((line == expected), line); 537 } 538 { 539 immutable number = 1234; 540 immutable line = number.ircUnderlined; 541 immutable expected = I.underlined ~ number.to!string ~ I.underlined; 542 assert((line == expected), line); 543 } 544 { 545 immutable b = true; 546 immutable line = b.ircUnderlined; 547 immutable expected = I.underlined ~ "true" ~ I.underlined; 548 assert((line == expected), line); 549 } 550 } 551 552 553 // ircReset 554 /++ 555 Returns an IRC formatting reset token. 556 557 Returns: 558 An IRC colour/formatting reset token. 559 +/ 560 auto ircReset() @nogc pure nothrow 561 { 562 return cast(char)IRCControlCharacter.reset; 563 } 564 565 566 // mapEffects 567 /++ 568 Maps mIRC effect tokens (colour, bold, italics, underlined) to terminal ones. 569 570 Example: 571 --- 572 string mIRCEffectString = "..."; 573 string TerminalFormatString = mapEffects(mIRCEffectString); 574 --- 575 576 Params: 577 origLine = String line to map effects of. 578 fgBase = Optional foreground base code to reset to after end colour tags. 579 bgBase = Optional background base code to reset to after end colour tags. 580 581 Returns: 582 A new string based on `origLine` with mIRC tokens mapped to terminal ones. 583 +/ 584 version(Colours) 585 auto mapEffects( 586 const string origLine, 587 const TerminalForeground fgBase = TerminalForeground.default_, 588 const TerminalBackground bgBase = TerminalBackground.default_) pure nothrow 589 { 590 import lu.string : contains; 591 592 alias I = IRCControlCharacter; 593 alias TF = TerminalFormat; 594 595 if (!origLine.length) return string.init; 596 597 string line = origLine; // mutable 598 599 if (line.contains(I.colour)) 600 { 601 // Colour is mIRC 3 602 line = mapColours(line, fgBase, bgBase); 603 } 604 605 if (line.contains(I.bold)) 606 { 607 // Bold is terminal 1, mIRC 2 608 line = mapEffectsImpl!(No.strip, I.bold, TF.bold)(line); 609 } 610 611 if (line.contains(I.italics)) 612 { 613 // Italics is terminal 3 (not really), mIRC 29 614 line = mapEffectsImpl!(No.strip, I.italics, TF.italics)(line); 615 } 616 617 if (line.contains(I.underlined)) 618 { 619 // Underlined is terminal 4, mIRC 31 620 line = mapEffectsImpl!(No.strip, I.underlined, TF.underlined)(line); 621 } 622 623 return line; 624 } 625 626 /// 627 version(Colours) 628 unittest 629 { 630 import kameloso.terminal : TerminalToken; 631 import lu.conv : toAlpha; 632 633 alias I = IRCControlCharacter; 634 635 enum bBold = TerminalToken.format ~ "[" ~ TerminalFormat.bold.toAlpha ~ "m"; 636 enum bReset = TerminalToken.format ~ "[22m"; 637 //enum bResetAll = TerminalToken.format ~ "[0m"; 638 639 immutable line1 = "ABC"~I.bold~"DEF"~I.bold~"GHI"~I.bold~"JKL"~I.bold~"MNO"; 640 immutable line2 = "ABC"~bBold~"DEF"~bReset~"GHI"~bBold~"JKL"~bReset~"MNO";//~bResetAll; 641 immutable mapped = mapEffects(line1); 642 643 assert((mapped == line2), mapped); 644 } 645 646 647 // stripEffects 648 /++ 649 Removes all form of mIRC formatting (colours, bold, italics, underlined) 650 from a string. 651 652 Params: 653 line = String to strip effects from. 654 655 Returns: 656 A string devoid of effects. 657 +/ 658 auto stripEffects(const string line) pure nothrow 659 { 660 if (!line.length) return line; 661 662 alias I = IRCControlCharacter; 663 664 return line 665 .stripColours 666 .mapEffectsImpl!(Yes.strip, I.bold, TerminalFormat.unset) 667 .mapEffectsImpl!(Yes.strip, I.italics, TerminalFormat.unset) 668 .mapEffectsImpl!(Yes.strip, I.underlined, TerminalFormat.unset); 669 } 670 671 /// 672 unittest 673 { 674 alias I = IRCControlCharacter; 675 676 enum boldCode = "" ~ I.bold; 677 enum italicsCode = "" ~ I.italics; 678 679 { 680 immutable withTags = "This is " ~ boldCode ~ "riddled" ~ boldCode ~ " with " ~ 681 italicsCode ~ "tags" ~ italicsCode; 682 immutable without = stripEffects(withTags); 683 assert((without == "This is riddled with tags"), without); 684 } 685 { 686 immutable withTags = "This line has no tags."; 687 immutable without = stripEffects(withTags); 688 assert((without == withTags), without); 689 } 690 { 691 string withTags; 692 immutable without = stripEffects(withTags); 693 assert(!without.length, without); 694 } 695 } 696 697 698 // mapColours 699 /++ 700 Maps mIRC effect colour tokens to terminal ones. 701 702 Merely calls [mapColoursImpl] with `No.strip`. 703 704 Params: 705 line = String line with IRC colours to translate. 706 fgFallback = Foreground code to reset to after colour-default tokens. 707 bgFallback = Background code to reset to after colour-default tokens. 708 709 Returns: 710 The passed `line`, now with terminal colouring. 711 +/ 712 version(Colours) 713 auto mapColours( 714 const string line, 715 const TerminalForeground fgFallback, 716 const TerminalBackground bgFallback) pure nothrow 717 { 718 if (!line.length) return line; 719 return mapColoursImpl!(No.strip)(line, fgFallback, bgFallback); 720 } 721 722 723 // mapColoursImpl 724 /++ 725 Maps mIRC effect colour tokens to terminal ones, or strip them entirely. 726 Now with less regex. 727 728 Pass `Yes.strip` as `strip` to map colours to nothing, removing colouring. 729 730 This function requires version `Colours` to map colours, but doesn't if 731 just to strip. 732 733 Params: 734 strip = Whether or not to strip colours or to map them. 735 line = String line with IRC colours to translate. 736 fgFallback = Foreground code to reset to after colour-default tokens. 737 bgFallback = Background code to reset to after colour-default tokens. 738 739 Returns: 740 The passed `line`, now with terminal colouring, or completely without. 741 +/ 742 private string mapColoursImpl(Flag!"strip" strip = No.strip) 743 (const string line, 744 const TerminalForeground fgFallback, 745 const TerminalBackground bgFallback) pure nothrow 746 { 747 import lu.conv : toAlphaInto; 748 import std.array : Appender; 749 import std.string : indexOf; 750 751 version(Colours) {} 752 else 753 { 754 static if (!strip) 755 { 756 static assert(0, "Tried to `mapColoursImpl!(No.strip)` outside of version `Colours`"); 757 } 758 } 759 760 static struct Segment 761 { 762 string pre; 763 int fg; 764 int bg; 765 bool hasBackground; 766 bool isReset; 767 } 768 769 string slice = line; // mutable 770 771 ptrdiff_t pos = slice.indexOf(IRCControlCharacter.colour); 772 773 if (pos == -1) return line; // Return line as is, don't allocate a new one 774 775 Segment[] segments; 776 segments.reserve(8); // Guesstimate 777 778 while (pos != -1) 779 { 780 immutable segmentIndex = segments.length; // snapshot 781 segments ~= Segment.init; 782 Segment* segment = &segments[segmentIndex]; 783 784 segment.pre = slice[0..pos]; 785 if (slice.length == pos) break; 786 slice = slice[pos+1..$]; 787 788 if (!slice.length) 789 { 790 segment.isReset = true; 791 break; 792 } 793 794 int c = slice[0] - '0'; 795 796 if ((c >= 0) && (c <= 9)) 797 { 798 int fg1; 799 int fg2; 800 bool hasFg2; 801 802 fg1 = c; 803 if (slice.length < 2) break; 804 slice = slice[1..$]; 805 806 c = slice[0] - '0'; 807 808 if ((c >= 0) && (c <= 9)) 809 { 810 fg2 = c; 811 hasFg2 = true; 812 if (slice.length < 2) break; 813 slice = slice[1..$]; 814 } 815 816 int fg = hasFg2 ? (10*fg1 + fg2) : fg1; 817 818 if (fg > 15) 819 { 820 fg %= 16; 821 } 822 823 segment.fg = fg; 824 825 if (slice[0] == ',') 826 { 827 if (!slice.length) break; 828 slice = slice[1..$]; 829 830 c = slice[0] - '0'; 831 832 if ((c >= 0) && (c <= 9)) 833 { 834 segment.hasBackground = true; 835 836 int bg1; 837 int bg2; 838 bool hasBg2; 839 840 bg1 = c; 841 if (slice.length < 2) break; 842 slice = slice[1..$]; 843 844 c = slice[0] - '0'; 845 846 if ((c >= 0) && (c <= 9)) 847 { 848 bg2 = c; 849 hasBg2 = true; 850 if (!slice.length) break; 851 slice = slice[1..$]; 852 } 853 854 uint bg = hasBg2 ? (10*bg1 + bg2) : bg1; 855 856 if (bg > 15) 857 { 858 bg %= 16; 859 } 860 861 segment.bg = bg; 862 } 863 } 864 } 865 else 866 { 867 segment.isReset = true; 868 } 869 870 pos = slice.indexOf(IRCControlCharacter.colour); 871 } 872 873 immutable tail = slice; 874 875 Appender!(char[]) sink; 876 sink.reserve(line.length + segments.length * 8); 877 878 static if (strip) 879 { 880 foreach (segment; segments) 881 { 882 sink.put(segment.pre); 883 } 884 } 885 else 886 { 887 version(Colours) 888 { 889 alias F = TerminalForeground; 890 alias B = TerminalBackground; 891 892 static immutable TerminalForeground[16] weechatForegroundMap = 893 [ 894 0 : F.white, 895 1 : F.darkgrey, 896 2 : F.blue, 897 3 : F.green, 898 4 : F.lightred, 899 5 : F.red, 900 6 : F.magenta, 901 7 : F.yellow, 902 8 : F.lightyellow, 903 9 : F.lightgreen, 904 10 : F.cyan, 905 11 : F.lightcyan, 906 12 : F.lightblue, 907 13 : F.lightmagenta, 908 14 : F.darkgrey, 909 15 : F.lightgrey, 910 ]; 911 912 static immutable TerminalBackground[16] weechatBackgroundMap = 913 [ 914 0 : B.white, 915 1 : B.black, 916 2 : B.blue, 917 3 : B.green, 918 4 : B.red, 919 5 : B.red, 920 6 : B.magenta, 921 7 : B.yellow, 922 8 : B.yellow, 923 9 : B.green, 924 10 : B.cyan, 925 11 : B.cyan, 926 12 : B.blue, 927 13 : B.magenta, 928 14 : B.black, 929 15 : B.lightgrey, 930 ]; 931 932 bool open; 933 934 foreach (segment; segments) 935 { 936 open = true; 937 sink.put(segment.pre); 938 sink.put("\033["); 939 940 if (segment.isReset) 941 { 942 fgFallback.toAlphaInto(sink); 943 sink.put(';'); 944 bgFallback.toAlphaInto(sink); 945 sink.put('m'); 946 open = false; 947 continue; 948 } 949 950 (cast(uint)weechatForegroundMap[segment.fg]).toAlphaInto(sink); 951 952 if (segment.hasBackground) 953 { 954 sink.put(';'); 955 (cast(uint)weechatBackgroundMap[segment.bg]).toAlphaInto(sink); 956 } 957 958 sink.put("m"); 959 } 960 } 961 else 962 { 963 //static assert(0); 964 } 965 } 966 967 sink.put(tail); 968 969 version(Colours) 970 { 971 static if (!strip) 972 { 973 if (open) 974 { 975 if ((fgFallback == 39) && (bgFallback == 49)) 976 { 977 // Shortcut 978 sink.put("\033[39;49m"); 979 } 980 else 981 { 982 sink.put("\033["); 983 fgFallback.toAlphaInto(sink); 984 sink.put(';'); 985 bgFallback.toAlphaInto(sink); 986 sink.put('m'); 987 } 988 } 989 } 990 } 991 992 return sink.data; 993 } 994 995 /// 996 version(Colours) 997 unittest 998 { 999 alias I = IRCControlCharacter; 1000 alias TF = TerminalForeground; 1001 alias TB = TerminalBackground; 1002 1003 { 1004 immutable line = "This is " ~ I.colour ~ "4all red!" ~ I.colour ~ " while this is not."; 1005 immutable mapped = mapColours(line, TF.default_, TB.default_); 1006 assert((mapped == "This is \033[91mall red!\033[39;49m while this is not."), mapped); 1007 } 1008 { 1009 immutable line = "This time there's" ~ I.colour ~ "6 no ending token, only magenta."; 1010 immutable mapped = mapColours(line, TF.default_, TB.default_); 1011 assert((mapped == "This time there's\033[35m no ending token, only magenta.\033[39;49m"), mapped); 1012 } 1013 { 1014 immutable line = I.colour ~ "1,0You" ~ I.colour ~ "0,4Tube" ~ I.colour ~ " asdf"; 1015 immutable mapped = mapColours(line, TF.default_, TB.default_); 1016 assert((mapped == "\033[90;107mYou\033[97;41mTube\033[39;49m asdf"), mapped); 1017 } 1018 { 1019 immutable line = I.colour ~ "17,0You" ~ I.colour ~ "0,21Tube" ~ I.colour ~ " asdf"; 1020 immutable mapped = mapColours(line, TF.default_, TB.default_); 1021 assert((mapped == "\033[90;107mYou\033[97;41mTube\033[39;49m asdf"), mapped); 1022 } 1023 { 1024 immutable line = I.colour ~ "17,0You" ~ I.colour ~ "0,2" ~ I.colour; 1025 immutable mapped = mapColours(line, TF.default_, TB.default_); 1026 assert((mapped == "\033[90;107mYou\033[97;44m\033[39;49m"), mapped); 1027 } 1028 { 1029 immutable line = I.colour ~ ""; 1030 immutable mapped = mapColours(line, TF.default_, TB.default_); 1031 assert((mapped == "\033[39;49m"), mapped); 1032 } 1033 } 1034 1035 1036 // stripColours 1037 /++ 1038 Removes IRC colouring from a passed string. 1039 1040 Merely calls [mapColours] with a `Yes.strip` template parameter. 1041 1042 Params: 1043 line = String to strip of IRC colour tags. 1044 1045 Returns: 1046 The passed `line`, now stripped of IRC colours. 1047 +/ 1048 auto stripColours(const string line) pure nothrow 1049 { 1050 if (!line.length) return line; 1051 return mapColoursImpl!(Yes.strip)(line, TerminalForeground.default_, TerminalBackground.default_); 1052 } 1053 1054 /// 1055 unittest 1056 { 1057 alias I = IRCControlCharacter; 1058 1059 { 1060 immutable line = "This is " ~ I.colour ~ "4all red!" ~ I.colour ~ " while this is not."; 1061 immutable stripped = line.stripColours(); 1062 assert((stripped == "This is all red! while this is not."), stripped); 1063 } 1064 { 1065 immutable line = "This time there's" ~ I.colour ~ "6 no ending token, only magenta."; 1066 immutable stripped = line.stripColours(); 1067 assert((stripped == "This time there's no ending token, only magenta."), stripped); 1068 } 1069 { 1070 immutable line = "This time there's" ~ I.colour ~ "6 no ending " ~ I.colour ~ 1071 "6token, only " ~ I.colour ~ "magenta."; 1072 immutable stripped = line.stripColours(); 1073 assert((stripped == "This time there's no ending token, only magenta."), stripped); 1074 } 1075 } 1076 1077 1078 // mapEffectsImpl 1079 /++ 1080 Replaces mIRC tokens with terminal effect codes, in an alternating fashion 1081 so as to support repeated effects toggling behaviour. Now with less regex. 1082 1083 It seems to be the case that a token for bold text will trigger bold text up 1084 until the next bold token. If we only naïvely replace all mIRC tokens for 1085 bold text then, we'll get lines that start off bold and continue as such 1086 until the very end. 1087 1088 Instead we iterate all occcurences of the passed `mircToken`, toggling the 1089 effect on and off. 1090 1091 Params: 1092 strip = Whether or not to strip effects or map them. 1093 mircToken = mIRC token for a particular text effect. 1094 terminalFormatCode = Terminal equivalent of the mircToken effect. 1095 line = The mIRC-formatted string to translate. 1096 1097 Returns: 1098 The passed `line`, now with terminal formatting. 1099 +/ 1100 private string mapEffectsImpl(Flag!"strip" strip, IRCControlCharacter mircToken, 1101 TerminalFormat terminalFormatCode) 1102 (const string line) pure 1103 { 1104 import lu.conv : toAlpha; 1105 import std.array : Appender; 1106 import std.string : indexOf; 1107 1108 version(Colours) {} 1109 else 1110 { 1111 static if (!strip) 1112 { 1113 static assert(0, "Tried to call `mapEffectsImpl!(No.strip)` outside of version `Colours`"); 1114 } 1115 } 1116 1117 string slice = line; // mutable 1118 ptrdiff_t pos = slice.indexOf(mircToken); 1119 if (pos == -1) return line; // As is 1120 1121 Appender!(char[]) sink; 1122 1123 static if (!strip) 1124 { 1125 import kameloso.terminal : TerminalToken; 1126 import kameloso.terminal.colours : applyANSI; 1127 1128 enum terminalToken = TerminalToken.format ~ "[" ~ toAlpha(terminalFormatCode) ~ "m"; 1129 sink.reserve(cast(size_t)(line.length * 1.5)); 1130 bool open; 1131 } 1132 else 1133 { 1134 sink.reserve(line.length); 1135 } 1136 1137 while (pos != -1) 1138 { 1139 sink.put(slice[0..pos]); 1140 1141 if (slice.length == pos) 1142 { 1143 // Slice away the end so it isn't added as the tail afterwards 1144 slice = slice[pos..$]; 1145 break; 1146 } 1147 1148 slice = slice[pos+1..$]; 1149 1150 static if (!strip) 1151 { 1152 if (!open) 1153 { 1154 sink.put(terminalToken); 1155 open = true; 1156 } 1157 else 1158 { 1159 static if ((terminalFormatCode == 1) || (terminalFormatCode == 2)) 1160 { 1161 // Both 1 and 2 seem to be reset by 22? 1162 enum tokenstring = TerminalToken.format ~ "[22m"; 1163 sink.put(tokenstring); 1164 } 1165 else static if ((terminalFormatCode >= 3) && (terminalFormatCode <= 5)) 1166 { 1167 enum tokenstring = TerminalToken.format ~ "[2" ~ terminalFormatCode.toAlpha ~ "m"; 1168 sink.put(tokenstring); 1169 } 1170 else 1171 { 1172 //logger.warning("Unknown terminal effect code: ", TerminalFormatCode); 1173 sink.applyANSI(TerminalReset.all); 1174 } 1175 1176 open = false; 1177 } 1178 } 1179 1180 pos = slice.indexOf(mircToken); 1181 } 1182 1183 alias tail = slice; 1184 sink.put(tail); 1185 1186 static if (!strip) 1187 { 1188 if (open) sink.applyANSI(TerminalReset.all); 1189 } 1190 1191 return sink.data; 1192 } 1193 1194 /// 1195 version(Colours) 1196 unittest 1197 { 1198 import kameloso.terminal : TerminalToken; 1199 import lu.conv : toAlpha; 1200 1201 alias I = IRCControlCharacter; 1202 alias TF = TerminalFormat; 1203 1204 enum bBold = TerminalToken.format ~ "[" ~ TF.bold.toAlpha ~ "m"; 1205 enum bReset = TerminalToken.format ~ "[22m"; 1206 1207 { 1208 enum line = "derp " ~ I.bold ~ "herp derp" ~ I.bold ~ "der dper"; 1209 immutable mapped = mapEffectsImpl!(No.strip, I.bold, TF.bold)(line); 1210 assert((mapped == "derp " ~ bBold ~ "herp derp" ~ bReset ~ "der dper"), mapped); 1211 } 1212 } 1213 1214 1215 // expandIRCTags 1216 /++ 1217 Slightly more complicated, but essentially string-replaces `<tags>` in an 1218 outgoing IRC string with correlating formatting using 1219 [dialect.common.IRCControlCharacter|IRCControlCharacter]s in their syntax. 1220 Overload that takes an explicit `strip` [std.typecons.Flag|Flag]. 1221 1222 Params: 1223 line = String line to expand IRC tags of. 1224 extendedOutgoingColours = Whether or not to use extended colours (16-99). 1225 strip = Whether to expand tags or strip them from the input line. 1226 1227 Returns: 1228 The passed `line` but with tags expanded to formatting and colouring. 1229 +/ 1230 T expandIRCTags(T) 1231 (const T line, 1232 const Flag!"extendedOutgoingColours" extendedOutgoingColours, 1233 const Flag!"strip" strip) @system 1234 { 1235 import std.encoding : sanitize; 1236 import std.utf : UTFException; 1237 import core.exception : UnicodeException; 1238 1239 try 1240 { 1241 return expandIRCTagsImpl(line, extendedOutgoingColours, strip); 1242 } 1243 catch (UTFException _) 1244 { 1245 return expandIRCTagsImpl(sanitize(line), extendedOutgoingColours, strip); 1246 } 1247 catch (UnicodeException _) 1248 { 1249 return expandIRCTagsImpl(sanitize(line), extendedOutgoingColours, strip); 1250 } 1251 } 1252 1253 /// 1254 @system unittest 1255 { 1256 import std.typecons : Flag, No, Yes; 1257 1258 // See unittests of other overloads for more No.strip tests 1259 1260 { 1261 immutable line = "hello<b>hello<b>hello"; 1262 immutable expanded = line.expandIRCTags(Yes.extendedOutgoingColours, Yes.strip); 1263 immutable expected = "hellohellohello"; 1264 assert((expanded == expected), expanded); 1265 } 1266 { 1267 immutable line = "hello<99,99<b>hiho</>"; 1268 immutable expanded = line.expandIRCTags(Yes.extendedOutgoingColours, Yes.strip); 1269 immutable expected = "hello<99,99hiho"; 1270 assert((expanded == expected), expanded); 1271 } 1272 { 1273 immutable line = "hello<1>hellohello"; 1274 immutable expanded = line.expandIRCTags(Yes.extendedOutgoingColours, Yes.strip); 1275 immutable expected = "hellohellohello"; 1276 assert((expanded == expected), expanded); 1277 } 1278 { 1279 immutable line = `hello\<h>hello<h>hello<h>hello`; 1280 immutable expanded = line.expandIRCTags(Yes.extendedOutgoingColours, Yes.strip); 1281 immutable expected = "hello<h>hellohellohello"; 1282 assert((expanded == expected), expanded); 1283 } 1284 } 1285 1286 1287 // expandIRCTags 1288 /++ 1289 Slightly more complicated, but essentially string-replaces `<tags>` in an 1290 outgoing IRC string with correlating formatting using 1291 [dialect.common.IRCControlCharacter|IRCControlCharacter]s in their syntax. 1292 Overload that does not take a `strip` [std.typecons.Flag|Flag]. 1293 1294 `<tags>` are the lowercase first letter of all 1295 [dialect.common.IRCControlCharacter|IRCControlCharacter] members; 1296 `<b>` for [dialect.common.IRCControlCharacter.bold|IRCControlCharacter.bold], 1297 `<c>` for [dialect.common.IRCControlCharacter.colour|IRCControlCharacter.colour], 1298 `<i>` for [dialect.common.IRCControlCharacter.italics|IRCControlCharacter.italics], 1299 `<u>` for [dialect.common.IRCControlCharacter.underlined|IRCControlCharacter.underlined], 1300 and the magic `</>` for [dialect.common.IRCControlCharacter.reset|IRCControlCharacter.reset], 1301 1302 An additional `<h>` tag is also introduced, which invokes [ircColourByHash] 1303 on the content between two of them. 1304 1305 If the line is not valid UTF, it is sanitised and the expansion retried. 1306 1307 Example: 1308 --- 1309 // Old 1310 enum pattern = "Quote %s #%s saved."; 1311 immutable message = plugin.state.settings.colouredOutgoing ? 1312 pattern.format(id.ircColourByHash(Yes.extendedOutgoingColours), index.ircBold) : 1313 pattern.format(id, index); 1314 privmsg(plugin.state, event.channel, event.sender.nickname. message); 1315 1316 // New 1317 enum newPattern = "Quote <h>%s<h> #<b>%d<b> saved."; 1318 immutable newMessage = newPattern.format(id, index); 1319 privmsg(plugin.state, event.channel, event.sender.nickname, newMessage); 1320 --- 1321 1322 Params: 1323 line = String line to expand IRC tags of. 1324 1325 Returns: 1326 The passed `line` but with tags expanded to formatting and colouring. 1327 +/ 1328 T expandIRCTags(T)(const T line) @system 1329 { 1330 static import kameloso.common; 1331 1332 debug 1333 { 1334 if (kameloso.common.settings is null) 1335 { 1336 import std.stdio : stdout, writefln; 1337 1338 // We're likely threading and forgot to initialise global settings 1339 kameloso.common.settings = new typeof(*kameloso.common.settings); 1340 1341 writefln("-- Warning: attempted to expand IRC tags by relying on " ~ 1342 "global `kameloso.common.settings`, and it was null"); 1343 stdout.flush(); 1344 } 1345 } 1346 1347 immutable extendedOutgoingColours = 1348 cast(Flag!"extendedOutgoingColours")kameloso.common.settings.extendedOutgoingColours; 1349 immutable strip = cast(Flag!"strip")!kameloso.common.settings.colouredOutgoing; 1350 return expandIRCTags(line, extendedOutgoingColours, strip); 1351 } 1352 1353 /// 1354 @system unittest 1355 { 1356 import dialect.common : I = IRCControlCharacter; 1357 import std.conv : text, to; 1358 import std.format : format; 1359 1360 { 1361 immutable line = "hello"; 1362 immutable expanded = line.expandIRCTags; 1363 assert((expanded is line), expanded); 1364 } 1365 { 1366 immutable line = string.init; 1367 immutable expanded = line.expandIRCTags; 1368 assert(expanded is null); 1369 } 1370 { 1371 immutable line = "hello<b>hello<b>hello"; 1372 immutable expanded = line.expandIRCTags; 1373 immutable expected = "hello" ~ I.bold ~ "hello" ~ I.bold ~ "hello"; 1374 assert((expanded == expected), expanded); 1375 } 1376 { 1377 immutable line = "hello<1>hello<c>hello"; 1378 immutable expanded = line.expandIRCTags; 1379 immutable expected = "hello" ~ I.colour ~ "01hello" ~ I.colour ~ "hello"; 1380 assert((expanded == expected), expanded); 1381 } 1382 { 1383 immutable line = "hello<3,4>hello<c>hello"; 1384 immutable expanded = line.expandIRCTags; 1385 immutable expected = "hello" ~ I.colour ~ "03,04hello" ~ I.colour ~ "hello"; 1386 assert((expanded == expected), expanded); 1387 } 1388 { 1389 immutable line = "hello<99,99<b>hiho</>"; 1390 immutable expanded = line.expandIRCTags; 1391 immutable expected = "hello<99,99" ~ I.bold ~ "hiho" ~ I.reset; 1392 assert((expanded == expected), expanded); 1393 } 1394 { 1395 immutable line = "hello<99,99><b>hiho</>"; 1396 immutable expanded = line.expandIRCTags; 1397 immutable expected = "hello" ~ I.colour ~ "99,99" ~ I.bold ~ "hiho" ~ I.reset; 1398 assert((expanded == expected), expanded); 1399 } 1400 { 1401 immutable line = "hello<99,999><b>hiho</>hey"; 1402 immutable expanded = line.expandIRCTags; 1403 immutable expected = "hello<99,999>" ~ I.bold ~ "hiho" ~ I.reset ~ "hey"; 1404 assert((expanded == expected), expanded); 1405 } 1406 { 1407 immutable line = `hello\<1,2>hiho`; 1408 immutable expanded = line.expandIRCTags; 1409 immutable expected = `hello<1,2>hiho`; 1410 assert((expanded == expected), expanded); 1411 } 1412 { 1413 immutable line = `hello\\<1,2>hiho`; 1414 immutable expanded = line.expandIRCTags; 1415 immutable expected = `hello\` ~ I.colour ~ "01,02hiho"; 1416 assert((expanded == expected), expanded); 1417 } 1418 { 1419 immutable line = "hello<"; 1420 immutable expanded = line.expandIRCTags; 1421 assert((expanded is line), expanded); 1422 } 1423 { 1424 immutable line = "hello<<<<"; 1425 immutable expanded = line.expandIRCTags; 1426 assert((expanded is line), expanded); 1427 } 1428 { 1429 immutable line = "hello<x>hello<z>"; 1430 immutable expanded = line.expandIRCTags; 1431 immutable expected = "hellohello"; 1432 assert((expanded == expected), expanded); 1433 } 1434 { 1435 immutable line = "hello<h>kameloso<h>hello"; 1436 immutable expanded = line.expandIRCTags; 1437 immutable expected = "hello" ~ I.colour ~ "23kameloso" ~ I.colour ~ "hello"; 1438 assert((expanded == expected), expanded); 1439 } 1440 { 1441 immutable line = "hello<h>kameloso"; 1442 immutable expanded = line.expandIRCTags; 1443 immutable expected = "hellokameloso"; 1444 assert((expanded == expected), expanded); 1445 } 1446 { 1447 immutable line = "hello<3,4>hello<c>hello"d; 1448 immutable expanded = line.expandIRCTags; 1449 immutable expected = "hello"d ~ I.colour ~ "03,04hello"d ~ I.colour ~ "hello"d; 1450 assert((expanded == expected), expanded.to!string); 1451 } 1452 /*{ 1453 immutable line = "hello<h>kameloso<h>hello"w; 1454 immutable expanded = line.expandIRCTags; 1455 immutable expected = "hello"w ~ I.colour ~ "01kameloso"w ~ I.colour ~ "hello"w; 1456 assert((expanded == expected), expanded.to!string); 1457 }*/ 1458 { 1459 immutable line = "Quote <h>zorael<h> #<b>5<b> saved."; 1460 immutable expanded = line.expandIRCTags; 1461 enum pattern = "Quote %s #%s saved."; 1462 immutable expected = pattern.format( 1463 "zorael".ircColourByHash(Yes.extendedOutgoingColours), 1464 "5".ircBold); 1465 assert((expanded == expected), expanded); 1466 } 1467 { 1468 immutable line = "Stopwatch stopped after <b>5 seconds<b>."; 1469 immutable expanded = line.expandIRCTags; 1470 enum pattern = "Stopwatch stopped after %s."; 1471 immutable expected = pattern.format("5 seconds".ircBold); 1472 assert((expanded == expected), expanded); 1473 } 1474 { 1475 immutable line = "<h>hirrsteff<h> was already <b>whitelist<b> in #garderoben."; 1476 immutable expanded = line.expandIRCTags; 1477 enum pattern = "%s was already %s in #garderoben."; 1478 immutable expected = pattern.format( 1479 "hirrsteff".ircColourByHash(Yes.extendedOutgoingColours), 1480 "whitelist".ircBold); 1481 assert((expanded == expected), expanded); 1482 } 1483 { 1484 immutable line = `hello\<h>hello<h>hello<h>hello`; 1485 immutable expanded = line.expandIRCTags; 1486 immutable expected = text( 1487 "hello<h>hello", 1488 "hello".ircColourByHash(Yes.extendedOutgoingColours), 1489 "hello"); 1490 assert((expanded == expected), expanded); 1491 } 1492 } 1493 1494 1495 // stripIRCTags 1496 /++ 1497 Removes `<tags>` in an outgoing IRC string where the tags correlate to formatting 1498 using [dialect.common.IRCControlCharacter|IRCControlCharacter]s. 1499 1500 Params: 1501 line = String line to remove IRC tags from. 1502 1503 Returns: 1504 The passed `line` but with tags removed. 1505 +/ 1506 T stripIRCTags(T)(const T line) @system 1507 { 1508 return expandIRCTags(line, No.extendedOutgoingColours, Yes.strip); 1509 } 1510 1511 /// 1512 @system unittest 1513 { 1514 import std.typecons : Flag, No, Yes; 1515 1516 { 1517 immutable line = "hello<b>hello<b>hello"; 1518 immutable expanded = line.stripIRCTags(); 1519 immutable expected = "hellohellohello"; 1520 assert((expanded == expected), expanded); 1521 } 1522 { 1523 immutable line = "hello<99,99<b>hiho</>"; 1524 immutable expanded = line.stripIRCTags(); 1525 immutable expected = "hello<99,99hiho"; 1526 assert((expanded == expected), expanded); 1527 } 1528 { 1529 immutable line = "hello<1>hellohello"; 1530 immutable expanded = line.stripIRCTags(); 1531 immutable expected = "hellohellohello"; 1532 assert((expanded == expected), expanded); 1533 } 1534 { 1535 immutable line = `hello\<h>hello<h>hello<h>hello`; 1536 immutable expanded = line.stripIRCTags(); 1537 immutable expected = "hello<h>hellohellohello"; 1538 assert((expanded == expected), expanded); 1539 } 1540 } 1541 1542 1543 // expandIRCTagsImpl 1544 /++ 1545 Implementation function for [expandIRCTags]. Kept separate so that 1546 [std.utf.UTFException|UTFException] can be neatly caught. 1547 1548 Params: 1549 line = String line to expand IRC tags of. 1550 extendedOutgoingColours = Whether or not to use extended colours (16-99). 1551 strip = Whether to expand tags or strip them from the input line. 1552 1553 Returns: 1554 The passed `line` but with tags expanded to formatting and colouring. 1555 1556 Throws: 1557 [std.string.indexOf] (used internally) throws [std.utf.UTFException|UTFException] 1558 if the starting index of a lookup doesn't represent a well-formed codepoint. 1559 +/ 1560 private T expandIRCTagsImpl(T) 1561 (const T line, 1562 const Flag!"extendedOutgoingColours" extendedOutgoingColours, 1563 const Flag!"strip" strip = No.strip) pure 1564 { 1565 import dialect.common : IRCControlCharacter; 1566 import lu.string : contains; 1567 import std.array : Appender; 1568 import std.range : ElementEncodingType; 1569 import std.string : representation; 1570 import std.traits : Unqual; 1571 1572 alias E = Unqual!(ElementEncodingType!T); 1573 1574 if (!line.length || !line.contains('<')) return line; 1575 1576 Appender!(E[]) sink; 1577 bool dirty; 1578 bool escaping; 1579 1580 immutable asBytes = line.representation; 1581 immutable toReserve = (asBytes.length + 16); 1582 1583 byteloop: 1584 for (size_t i; i<asBytes.length; ++i) 1585 { 1586 immutable c = asBytes[i]; 1587 1588 switch (c) 1589 { 1590 case '\\': 1591 if (escaping) 1592 { 1593 // Always dirty 1594 sink.put('\\'); 1595 } 1596 else 1597 { 1598 if (!dirty) 1599 { 1600 sink.reserve(toReserve); 1601 sink.put(asBytes[0..i]); 1602 dirty = true; 1603 } 1604 } 1605 1606 escaping = !escaping; 1607 break; 1608 1609 case '<': 1610 if (escaping) 1611 { 1612 // Always dirty 1613 sink.put('<'); 1614 escaping = false; 1615 } 1616 else 1617 { 1618 import std.string : indexOf; 1619 1620 immutable ptrdiff_t closingBracketPos = (cast(T)asBytes[i..$]).indexOf('>'); 1621 1622 if ((closingBracketPos == -1) || (closingBracketPos > 6)) 1623 { 1624 if (dirty) 1625 { 1626 sink.put(c); 1627 } 1628 } 1629 else 1630 { 1631 // Valid; dirties now if not already dirty 1632 1633 if (asBytes.length < i+2) 1634 { 1635 // Too close to the end to have a meaningful tag 1636 // Break and return 1637 1638 if (dirty) 1639 { 1640 // Add rest first 1641 sink.put(asBytes[i..$]); 1642 } 1643 1644 break byteloop; 1645 } 1646 1647 if (!dirty) 1648 { 1649 sink.reserve(toReserve); 1650 sink.put(asBytes[0..i]); 1651 dirty = true; 1652 } 1653 1654 immutable slice = asBytes[i+1..i+closingBracketPos]; // mutable 1655 1656 if ((slice[0] >= '0') && (slice[0] <= '9')) 1657 { 1658 if (!strip) 1659 { 1660 static auto getColourChars(S)(S slice) 1661 { 1662 static struct Result 1663 { 1664 immutable S fg; 1665 immutable S bg; 1666 } 1667 1668 immutable commaPos = (cast(T)slice).indexOf(','); 1669 1670 if (commaPos != -1) 1671 { 1672 return Result(slice[0..commaPos], slice[commaPos+1..$]); 1673 } 1674 else 1675 { 1676 return Result(slice); 1677 } 1678 } 1679 1680 immutable colours = getColourChars(slice); 1681 1682 sink.put(cast(char)IRCControlCharacter.colour); 1683 if (colours.fg.length == 1) sink.put('0'); 1684 sink.put(colours.fg); 1685 1686 if (colours.bg.length) 1687 { 1688 sink.put(','); 1689 if (colours.bg.length == 1) sink.put('0'); 1690 sink.put(colours.bg); 1691 } 1692 } 1693 } 1694 else 1695 { 1696 if (slice.length != 1) break; 1697 1698 switch (slice[0]) 1699 { 1700 case 'b': 1701 if (!strip) sink.put(cast(char)IRCControlCharacter.bold); 1702 break; 1703 1704 case 'c': 1705 if (!strip) sink.put(cast(char)IRCControlCharacter.colour); 1706 break; 1707 1708 case 'i': 1709 if (!strip) sink.put(cast(char)IRCControlCharacter.italics); 1710 break; 1711 1712 case 'u': 1713 if (!strip) sink.put(cast(char)IRCControlCharacter.underlined); 1714 break; 1715 1716 case '/': 1717 if (!strip) sink.put(cast(char)IRCControlCharacter.reset); 1718 break; 1719 1720 case 'h': 1721 i += 3; // advance past "<h>".length 1722 immutable closingHashMarkPos = (cast(T)asBytes[i..$]).indexOf("<h>"); 1723 1724 if (closingHashMarkPos == -1) 1725 { 1726 // Revert advance 1727 i -= 3; 1728 goto default; 1729 } 1730 else 1731 { 1732 if (!strip) 1733 { 1734 sink.put(ircColourByHash( 1735 cast(string)asBytes[i..i+closingHashMarkPos], 1736 extendedOutgoingColours)); 1737 } 1738 else 1739 { 1740 sink.put(cast(string)asBytes[i..i+closingHashMarkPos]); 1741 } 1742 1743 // Don't advance the full "<h>".length 3 1744 // because the for-loop ++i will advance one ahead 1745 i += (closingHashMarkPos+2); 1746 continue; // Not break 1747 } 1748 1749 default: 1750 // Invalid control character, just ignore 1751 break; 1752 } 1753 } 1754 1755 i += closingBracketPos; 1756 } 1757 } 1758 break; 1759 1760 default: 1761 if (dirty) 1762 { 1763 sink.put(c); 1764 } 1765 break; 1766 } 1767 } 1768 1769 return dirty ? sink.data.idup : line; 1770 }