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 }