1 /++ 2 A collection of functions used to translate tags in messages into terminal colours. 3 4 See_Also: 5 [kameloso.terminal], 6 [kameloso.terminal.colours], 7 [kameloso.terminal.colours.defs] 8 9 Copyright: [JR](https://github.com/zorael) 10 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 11 12 Authors: 13 [JR](https://github.com/zorael) 14 +/ 15 module kameloso.terminal.colours.tags; 16 17 private: 18 19 import kameloso.logger : LogLevel; 20 import std.traits : isSomeString; 21 import std.typecons : Flag, No, Yes; 22 23 public: 24 25 26 // expandTags 27 /++ 28 String-replaces `<tags>` in a string with the results from calls to `Tint`. 29 Also works with `dstring`s and `wstring`s. 30 31 `<tags>` are the lowercase first letter of all 32 [kameloso.logger.LogLevel|LogLevel]s; `<l>`, `<t>`, `<i>`, `<w>` 33 `<e>`, `<c>` and `<f>`. `<a>` is not included. 34 35 `</>` equals the passed `baseLevel` and is used to terminate colour sequences, 36 returning to a default. 37 38 Lastly, text between a `<h>` and a `</>` are replaced with the results from 39 a call to [kameloso.terminal.colours.colourByHash|colourByHash]. 40 41 This should hopefully make highlighted strings more readable. 42 43 Example: 44 --- 45 enum oldPattern = " 46 %1$sYour private authorisation key is: %2$s%3$s%4$s 47 It should be entered as %2$spass%4$s under %2$s[IRCBot]%4$s. 48 "; 49 immutable oldMessage = oldPattern.format(Tint.log, Tint.info, pass, Tint.off); 50 51 enum newPattern = " 52 <l>Your private authorisation key is: <i>%s</> 53 It should be entered as <i>pass</> under <i>[IRCBot]</> 54 "; 55 immutable newMessage = newPattern 56 .format(pass) 57 .expandTags(LogLevel.off); 58 59 enum patternWithColouredNickname = "No quotes for nickname <h>%s<h>."; 60 61 immutable colouredMessage = patternWithColouredNickname 62 .format(event.sender.nickname) 63 .expandTags(LogLevel.off); 64 --- 65 66 Params: 67 line = A line of text, presumably with `<tags>`. 68 baseLevel = The base [kameloso.logger.LogLevel|LogLevel] to fall back to on `</>` tags. 69 strip = Whether to expand tags or strip them. 70 71 Returns: 72 The passed `line` but with any `<tags>` replaced with ANSI colour sequences. 73 The original string is passed back if there was nothing to replace. 74 +/ 75 auto expandTags(T)(const T line, const LogLevel baseLevel, const Flag!"strip" strip) @safe 76 if (isSomeString!T) 77 { 78 import kameloso.common : logger; 79 import lu.string : contains; 80 import std.array : Appender; 81 import std.range : ElementEncodingType; 82 import std.string : representation; 83 import std.traits : Unqual; 84 85 static import kameloso.common; 86 87 alias E = Unqual!(ElementEncodingType!T); 88 89 if (!line.length || !line.contains('<')) return line; 90 91 // Without marking this as @trusted, we can't have @safe expandTags... 92 static auto indexOf(H, N)(const H haystack, const N rawNeedle) @trusted 93 { 94 import std.string : indexOf; 95 96 static if (is(N : ubyte)) 97 { 98 immutable needle = cast(char)rawNeedle; 99 } 100 else 101 { 102 alias needle = rawNeedle; 103 } 104 105 return (cast(T)haystack).indexOf(needle); 106 } 107 108 Appender!(E[]) sink; 109 bool dirty; 110 bool escaping; 111 112 // Work around the immutability being lost with -dip1000 113 // The alternative is to use .idup, which is not really desireable here 114 immutable asBytes = () @trusted 115 { 116 return cast(immutable)line.representation; 117 }(); 118 119 immutable toReserve = (asBytes.length + 16); 120 121 byteloop: 122 for (size_t i; i<asBytes.length; ++i) 123 { 124 immutable c = asBytes[i]; 125 126 switch (c) 127 { 128 case '\\': 129 if (escaping) 130 { 131 // Always dirty 132 sink.put('\\'); 133 } 134 else 135 { 136 if (!dirty) 137 { 138 sink.reserve(toReserve); 139 sink.put(asBytes[0..i]); 140 dirty = true; 141 } 142 } 143 144 escaping = !escaping; 145 break; 146 147 case '<': 148 if (escaping) 149 { 150 // Always dirty 151 sink.put('<'); 152 escaping = false; 153 } 154 else 155 { 156 immutable ptrdiff_t closingBracketPos = indexOf(asBytes[i..$], '>'); 157 158 if ((closingBracketPos == -1) || (closingBracketPos > 6)) 159 { 160 if (dirty) 161 { 162 sink.put(c); 163 } 164 } 165 else 166 { 167 // Valid; dirties now if not already dirty 168 169 if (asBytes.length < i+2) 170 { 171 // Too close to the end to have a meaningful tag 172 // Break and return 173 174 if (dirty) 175 { 176 // Add rest first 177 sink.put(asBytes[i..$]); 178 } 179 180 break byteloop; 181 } 182 183 if (!dirty) 184 { 185 sink.reserve(toReserve); 186 sink.put(asBytes[0..i]); 187 dirty = true; 188 } 189 190 immutable slice = asBytes[i+1..i+closingBracketPos]; // mutable 191 if (slice.length != 1) break; 192 193 sliceswitch: 194 switch (slice[0]) 195 { 196 197 version(Colours) 198 { 199 case 'l': 200 if (!strip) sink.put(logger.logtint); 201 break; 202 203 case 't': 204 if (!strip) sink.put(logger.tracetint); 205 break; 206 207 case 'i': 208 if (!strip) sink.put(logger.infotint); 209 break; 210 211 case 'w': 212 if (!strip) sink.put(logger.warningtint); 213 break; 214 215 case 'e': 216 if (!strip) sink.put(logger.errortint); 217 break; 218 219 case 'c': 220 if (!strip) sink.put(logger.criticaltint); 221 break; 222 223 case 'f': 224 if (!strip) sink.put(logger.fataltint); 225 break; 226 227 case 'o': 228 if (!strip) sink.put(logger.offtint); 229 break; 230 231 case '/': 232 if (!strip) 233 { 234 with (LogLevel) 235 final switch (baseLevel) 236 { 237 case all: //log 238 //goto case 'l'; 239 sink.put(logger.logtint); 240 break sliceswitch; 241 242 case trace: 243 //goto case 't'; 244 sink.put(logger.tracetint); 245 break sliceswitch; 246 247 case info: 248 //goto case 'i'; 249 sink.put(logger.infotint); 250 break sliceswitch; 251 252 case warning: 253 //goto case 'w'; 254 sink.put(logger.warningtint); 255 break sliceswitch; 256 257 case error: 258 //goto case 'e'; 259 sink.put(logger.errortint); 260 break sliceswitch; 261 262 case critical: 263 //goto case 'c'; 264 sink.put(logger.criticaltint); 265 break sliceswitch; 266 267 case fatal: 268 //goto case 'f'; 269 sink.put(logger.fataltint); 270 break sliceswitch; 271 272 case off: 273 //goto case 'o'; 274 sink.put(logger.offtint); 275 break sliceswitch; 276 } 277 } 278 break; 279 } 280 281 case 'h': 282 i += 3; // advance past "<h>".length 283 immutable closingHashMarkPos = indexOf(asBytes[i..$], "</>"); 284 285 if (closingHashMarkPos == -1) 286 { 287 // Revert advance 288 i -= 3; 289 goto default; 290 } 291 else 292 { 293 immutable word = cast(string)asBytes[i..i+closingHashMarkPos]; 294 295 version(Colours) 296 { 297 if (!strip) 298 { 299 import kameloso.terminal.colours : colourByHash; 300 301 sink.put(colourByHash(word, *kameloso.common.settings)); 302 303 with (LogLevel) 304 final switch (baseLevel) 305 { 306 case all: //log 307 sink.put(logger.logtint); 308 break; 309 310 case trace: 311 sink.put(logger.tracetint); 312 break; 313 314 case info: 315 sink.put(logger.infotint); 316 break; 317 318 case warning: 319 sink.put(logger.warningtint); 320 break; 321 322 case error: 323 sink.put(logger.errortint); 324 break; 325 326 case critical: 327 sink.put(logger.criticaltint); 328 break; 329 330 case fatal: 331 sink.put(logger.fataltint); 332 break; 333 334 case off: 335 sink.put(logger.offtint); 336 break; 337 } 338 } 339 else 340 { 341 sink.put(word); 342 } 343 } 344 else 345 { 346 sink.put(word); 347 } 348 349 // Don't advance the full "<h>".length 3 350 // because the for-loop ++i will advance one ahead 351 i += (closingHashMarkPos+2); 352 continue; // Not break 353 } 354 355 default: 356 // Invalid control character, just ignore 357 break; 358 } 359 360 i += closingBracketPos; 361 } 362 } 363 break; 364 365 default: 366 if (escaping) 367 { 368 escaping = false; 369 } 370 371 if (dirty) 372 { 373 sink.put(c); 374 } 375 break; 376 } 377 } 378 379 return dirty ? sink.data.idup : line; 380 } 381 382 /// 383 unittest 384 { 385 import kameloso.common : logger; 386 import std.conv : text, to; 387 import std.format : format; 388 import std.typecons : Flag, No, Yes; 389 390 { 391 immutable line = "This is a <l>log</> line."; 392 immutable replaced = line.expandTags(LogLevel.off, No.strip); 393 immutable expected = text("This is a ", logger.logtint, "log", logger.offtint, " line."); 394 assert((replaced == expected), replaced); 395 } 396 { 397 import std.conv : wtext; 398 399 immutable line = "This is a <l>log</> line."w; 400 immutable replaced = line.expandTags(LogLevel.off, No.strip); 401 immutable expected = wtext("This is a "w, logger.logtint, "log"w, logger.offtint, " line."w); 402 assert((replaced == expected), replaced.to!string); 403 } 404 { 405 import std.conv : dtext; 406 407 immutable line = "This is a <l>log</> line."d; 408 immutable replaced = line.expandTags(LogLevel.off, No.strip); 409 immutable expected = dtext("This is a "d, logger.logtint, "log"d, logger.offtint, " line."d); 410 assert((replaced == expected), replaced.to!string); 411 } 412 { 413 immutable line = `<i>info</>nothing<c>critical</>nothing\<w>not warning`; 414 immutable replaced = line.expandTags(LogLevel.off, No.strip); 415 immutable expected = text(logger.infotint, "info", logger.offtint, "nothing", 416 logger.criticaltint, "critical", logger.offtint, "nothing<w>not warning"); 417 assert((replaced == expected), replaced); 418 } 419 { 420 immutable line = "This is a line with no tags"; 421 immutable replaced = line.expandTags(LogLevel.off, No.strip); 422 assert(line is replaced); 423 } 424 { 425 immutable emptyLine = string.init; 426 immutable replaced = emptyLine.expandTags(LogLevel.off, No.strip); 427 assert(replaced is emptyLine); 428 } 429 { 430 immutable line = "hello<h>kameloso</>hello"; 431 immutable replaced = line.expandTags(LogLevel.off, Yes.strip); 432 immutable expected = "hellokamelosohello"; 433 assert((replaced == expected), replaced); 434 } 435 { 436 immutable line = "hello<h></>hello"; 437 immutable replaced = line.expandTags(LogLevel.off, Yes.strip); 438 immutable expected = "hellohello"; 439 assert((replaced == expected), replaced); 440 } 441 { 442 immutable line = `hello\<harbl>kameloso<h>hello</>hi`; 443 immutable replaced = line.expandTags(LogLevel.off, Yes.strip); 444 immutable expected = "hello<harbl>kamelosohellohi"; 445 assert((replaced == expected), replaced); 446 } 447 { 448 enum pattern = "Failed to fetch, replay and clear notes for " ~ 449 "<l>%s<e> on <l>%s<e>: <l>%s"; 450 immutable line = pattern.format("nickname", "<no channel>", "error"); 451 immutable replaced = line.expandTags(LogLevel.off, No.strip); 452 immutable expected = "Failed to fetch, replay and clear notes for " ~ 453 logger.logtint ~ "nickname" ~ logger.errortint ~ " on " ~ logger.logtint ~ 454 "<no channel>" ~ logger.errortint ~ ": " ~ logger.logtint ~ "error"; 455 assert((replaced == expected), replaced); 456 } 457 { 458 enum pattern = "Failed to fetch, replay and clear notes for " ~ 459 "<l>%s<e> on <l>%s<e>: <l>%s"; 460 immutable line = pattern.format("nickname", "<no channel>", "error"); 461 immutable replaced = line.expandTags(LogLevel.off, Yes.strip); 462 immutable expected = "Failed to fetch, replay and clear notes for " ~ 463 "nickname on <no channel>: error"; 464 assert((replaced == expected), replaced); 465 } 466 { 467 enum pattern = "Failed to fetch, replay and clear notes for " ~ 468 "<l>%s</> on <l>%s</>: <l>%s"; 469 immutable line = pattern.format("nickname", "<no channel>", "error"); 470 immutable replaced = line.expandTags(LogLevel.error, No.strip); 471 immutable expected = "Failed to fetch, replay and clear notes for " ~ 472 logger.logtint ~ "nickname" ~ logger.errortint ~ " on " ~ logger.logtint ~ 473 "<no channel>" ~ logger.errortint ~ ": " ~ logger.logtint ~ "error"; 474 assert((replaced == expected), replaced); 475 } 476 { 477 enum pattern = "Failed to fetch, replay and clear notes for " ~ 478 "<l>%s</> on <l>%s</>: <l>%s"; 479 immutable line = pattern.format("nickname", "<no channel>", "error"); 480 immutable replaced = line.expandTags(LogLevel.error, Yes.strip); 481 immutable expected = "Failed to fetch, replay and clear notes for " ~ 482 "nickname on <no channel>: error"; 483 assert((replaced == expected), replaced); 484 } 485 { 486 enum origPattern = "Could not apply <i>+%s<l> <i>%s<l> in <i>%s<l> " ~ 487 "because we are not an operator in the channel."; 488 enum newPattern = "Could not apply <i>+%s</> <i>%s</> in <i>%s</> " ~ 489 "because we are not an operator in the channel."; 490 immutable origLine = origPattern.format("o", "nickname", "#channel").expandTags(LogLevel.off, No.strip); 491 immutable newLine = newPattern.format("o", "nickname", "#channel").expandTags(LogLevel.all, No.strip); 492 assert((origLine == newLine), newLine); 493 } 494 495 version(Colours) 496 { 497 import kameloso.terminal.colours : colourByHash; 498 import kameloso.pods : CoreSettings; 499 500 CoreSettings brightSettings; 501 CoreSettings darkSettings; 502 brightSettings.brightTerminal = true; 503 504 { 505 immutable line = "hello<h>kameloso</>hello"; 506 immutable replaced = line.expandTags(LogLevel.off, No.strip); 507 immutable expected = text("hello", colourByHash("kameloso", 508 darkSettings), logger.offtint, "hello"); 509 assert((replaced == expected), replaced); 510 } 511 { 512 immutable line = `hello\<harbl>kameloso<h>hello</>hi`; 513 immutable replaced = line.expandTags(LogLevel.off, No.strip); 514 immutable expected = text("hello<harbl>kameloso", colourByHash("hello", 515 darkSettings), logger.offtint, "hi"); 516 assert((replaced == expected), replaced); 517 } 518 { 519 immutable line = "<l>%%APPDATA%%\\\\kameloso</>."; 520 immutable replaced = line.expandTags(LogLevel.off, No.strip); 521 immutable expected = logger.logtint ~ "%%APPDATA%%\\kameloso" ~ logger.offtint ~ "."; 522 assert((replaced == expected), replaced); 523 } 524 { 525 immutable line = "<l>herp\\</>herp\\\\herp\\\\<l>herp</>"; 526 immutable replaced = line.expandTags(LogLevel.off, No.strip); 527 immutable expected = logger.logtint ~ "herp</>herp\\herp\\" ~ logger.logtint ~ "herp" ~ logger.offtint; 528 assert((replaced == expected), replaced); 529 } 530 { 531 immutable line = "Added <h>hirrsteff</> as a blacklisted user in #garderoben"; 532 immutable replaced = line.expandTags(LogLevel.off, No.strip); 533 immutable expected = "Added " ~ 534 colourByHash("hirrsteff", brightSettings) ~ 535 logger.offtint ~ " as a blacklisted user in #garderoben"; 536 assert((replaced == expected), replaced); 537 } 538 } 539 } 540 541 542 // expandTags 543 /++ 544 String-replaces `<tags>` in a string with the results from calls to 545 [kameloso.logger.KamelosoLogger|KamelosoLogger] `*tint` methods. 546 Also works with `dstring`s and `wstring`s. Overload that does not take a 547 `strip` [std.typecons.Flag|Flag]. 548 549 Params: 550 line = A line of text, presumably with `<tags>`. 551 baseLevel = The base [kameloso.logger.LogLevel|LogLevel] to fall back to on `</>` tags. 552 553 Returns: 554 The passed `line` but with any `<tags>` replaced with ANSI colour sequences. 555 The original string is passed back if there was nothing to replace. 556 +/ 557 auto expandTags(T)(const T line, const LogLevel baseLevel) @safe 558 if (isSomeString!T) 559 { 560 static import kameloso.common; 561 immutable strip = cast(Flag!"strip")kameloso.common.settings.monochrome; 562 return expandTags(line, baseLevel, strip); 563 } 564 565 /// 566 unittest 567 { 568 import kameloso.common : logger; 569 import std.conv : text, to; 570 571 { 572 immutable line = "This is a <l>log</> line."; 573 immutable replaced = line.expandTags(LogLevel.off); 574 immutable expected = text("This is a ", logger.logtint, "log", logger.offtint, " line."); 575 assert((replaced == expected), replaced); 576 } 577 } 578 579 580 // stripTags 581 /++ 582 Removes `<tags>` from a string. 583 584 Example: 585 --- 586 enum pattern = " 587 <l>Your private authorisation key is: <i>%s</> 588 It should be entered as <i>pass</> under <i>[IRCBot]</> 589 "; 590 immutable newMessage = newPattern 591 .format(pass) 592 .stripTags(); 593 594 enum patternWithColouredNickname = "No quotes for nickname <h>%s<h>."; 595 immutable uncolouredMessage = patternWithColouredNickname 596 .format(event.sender.nickname) 597 .stripTags(); 598 --- 599 600 Params: 601 line = A line of text, presumably with `<tags>` to remove. 602 603 Returns: 604 The passed `line` with any `<tags>` removed. 605 The original string is passed back if there was nothing to remove. 606 +/ 607 auto stripTags(T)(const T line) @safe 608 if (isSomeString!T) 609 { 610 return expandTags(line, LogLevel.off, Yes.strip); 611 } 612 613 /// 614 unittest 615 { 616 { 617 immutable line = "This is a <l>log</> line."; 618 immutable replaced = line.stripTags(); 619 immutable expected = "This is a log line."; 620 assert((replaced == expected), replaced); 621 } 622 }