1 /++
2     String manipulation functions complementing the standard library.
3 
4     Example:
5     ---
6     {
7         string line = "Lorem ipsum :sit amet";
8         immutable lorem = line.advancePast(" :");
9         assert(lorem == "Lorem ipsum", lorem);
10         assert(line == "sit amet", line);
11     }
12     {
13         string line = "Lorem ipsum :sit amet";
14         immutable lorem = line.advancePast(':');
15         assert(lorem == "Lorem ipsum ", lorem);
16         assert(line == "sit amet", line);
17     }
18     {
19         string line = "Lorem ipsum sit amet";  // mutable, will be modified by ref
20         string[] words;
21 
22         while (line.length > 0)
23         {
24             immutable word = line.advancePast(" ", inherit: true);
25             words ~= word;
26         }
27 
28         assert(words == [ "Lorem", "ipsum", "sit", "amet" ]);
29     }
30     ---
31 
32     Copyright: [JR](https://github.com/zorael)
33     License: [Boost Software License 1.0](https://www.boost.org/users/license.html)
34 
35     Authors:
36         [JR](https://github.com/zorael)
37  +/
38 module lu..string;
39 
40 private:
41 
42 import std.traits : allSameType;
43 
44 public:
45 
46 @safe:
47 
48 
49 // advancePast
50 /++
51     Given some string, finds the supplied needle token in it, returns the
52     string up to that point, and advances the passed string by ref to after the token.
53 
54     The closest equivalent in Phobos is [std.algorithm.searching.findSplit],
55     which largely serves the same function but doesn't advance the input string.
56 
57     Additionally takes an optional `inherit` bool argument, to toggle
58     whether the return value inherits the passed line (and clearing it) upon no
59     needle match.
60 
61     Example:
62     ---
63     string foobar = "foo bar!";
64     string foo = foobar.advancePast(" ");
65     string bar = foobar.advancePast("!");
66 
67     assert((foo == "foo"), foo);
68     assert((bar == "bar"), bar);
69     assert(!foobar.length);
70 
71     enum line = "abc def ghi";
72     string def = line[4..$].advancePast(" ");  // now with auto ref
73 
74     string foobar2 = "foo bar!";
75     string foo2 = foobar2.advancePast(" ");
76     string bar2 = foobar2.advancePast("?", inherit: true);
77 
78     assert((foo2 == "foo"), foo2);
79     assert((bar2 == "bar!"), bar2);
80     assert(!foobar2.length);
81 
82     string slice2 = "snarfl";
83     string verb2 = slice2.advancePast(" ", inherit: true);
84 
85     assert((verb2 == "snarfl"), verb2);
86     assert(!slice2.length, slice2);
87     ---
88 
89     Params:
90         haystack = Array to walk and advance.
91         needle = Token that delimits what should be returned and to where to advance.
92             May be another array or some individual character.
93         inherit = Optional flag of whether or not the whole string should be
94             returned and the haystack variable cleared on no needle match.
95         callingFile = Optional file name to attach to an exception.
96         callingLine = Optional line number to attach to an exception.
97 
98     Returns:
99         The string `haystack` from the start up to the needle token. The original
100         variable is advanced to after the token.
101 
102     Throws:
103         [AdvanceException] if the needle could not be found in the string.
104  +/
105 auto advancePast(Haystack, Needle)
106     (auto ref return scope Haystack haystack,
107     const scope Needle needle,
108     const bool inherit = false,
109     const string callingFile = __FILE__,
110     const size_t callingLine = __LINE__) @safe
111 in
112 {
113     import std.traits : isArray;
114 
115     static if (isArray!Needle)
116     {
117         if (!needle.length)
118         {
119             enum message = "Tried to `advancePast` with no `needle` given";
120             throw new AdvanceExceptionImpl!(Haystack, Needle)
121                 (message,
122                 haystack.idup,
123                 needle.idup,
124                 callingFile,
125                 callingLine);
126         }
127     }
128 }
129 do
130 {
131     import std.traits : isArray, isMutable, isSomeChar;
132 
133     static if (!isMutable!Haystack)
134     {
135         enum message = "`advancePast` only works on mutable haystacks";
136         static assert(0, message);
137     }
138     else static if (!isArray!Haystack)
139     {
140         enum message = "`advancePast` only works on array-type haystacks";
141         static assert(0, message);
142     }
143     else static if (
144         !isArray!Haystack &&
145         !is(Needle : ElementType!Haystack) &&
146         !is(Needle : ElementEncodingType!Haystack))
147     {
148         enum message = "`advancePast` only works with array- or single-element-type needles";
149         static assert(0, message);
150     }
151 
152     static if (isArray!Needle || isSomeChar!Needle)
153     {
154         import std.string : indexOf;
155         immutable index = haystack.indexOf(needle);
156     }
157     else
158     {
159         import std.algorithm.searching : countUntil;
160         immutable index = haystack.countUntil(needle);
161     }
162 
163     if (index == -1)
164     {
165         if (inherit)
166         {
167             // No needle match; inherit string and clear the original
168             static if (__traits(isRef, haystack)) scope(exit) haystack = null;
169             return haystack;
170         }
171 
172         static if (isArray!Needle)
173         {
174             immutable needleIdup = needle.idup;
175         }
176         else
177         {
178             alias needleIdup = needle;
179         }
180 
181         enum message = "Tried to advance a string past something that wasn't there";
182         throw new AdvanceExceptionImpl!(Haystack, Needle)
183             (message,
184             haystack.idup,
185             needleIdup,
186             callingFile,
187             callingLine);
188     }
189 
190     static if (isArray!Needle)
191     {
192         immutable separatorLength = needle.length;
193     }
194     else
195     {
196         enum separatorLength = 1;
197     }
198 
199     static if (__traits(isRef, haystack)) scope(exit) haystack = haystack[(index+separatorLength)..$];
200     return haystack[0..index];
201 }
202 
203 
204 ///
205 unittest
206 {
207     import std.conv : to;
208     import std.string : indexOf;
209 
210     {
211         string line = "Lorem ipsum :sit amet";
212         immutable lorem = line.advancePast(" :");
213         assert(lorem == "Lorem ipsum", lorem);
214         assert(line == "sit amet", line);
215     }
216     {
217         string line = "Lorem ipsum :sit amet";
218         //immutable lorem = line.advancePast(" :");
219         immutable lorem = line.advancePast(" :");
220         assert(lorem == "Lorem ipsum", lorem);
221         assert(line == "sit amet", line);
222     }
223     {
224         string line = "Lorem ipsum :sit amet";
225         immutable lorem = line.advancePast(':');
226         assert(lorem == "Lorem ipsum ", lorem);
227         assert(line == "sit amet", line);
228     }
229     {
230         string line = "Lorem ipsum :sit amet";
231         immutable lorem = line.advancePast(':');
232         assert(lorem == "Lorem ipsum ", lorem);
233         assert(line == "sit amet", line);
234     }
235     {
236         string line = "Lorem ipsum :sit amet";
237         immutable lorem = line.advancePast(' ');
238         assert(lorem == "Lorem", lorem);
239         assert(line == "ipsum :sit amet", line);
240     }
241     {
242         string line = "Lorem ipsum :sit amet";
243         immutable lorem = line.advancePast(' ');
244         assert(lorem == "Lorem", lorem);
245         assert(line == "ipsum :sit amet", line);
246     }
247     /*{
248         string line = "Lorem ipsum :sit amet";
249         immutable lorem = line.advancePast("");
250         assert(!lorem.length, lorem);
251         assert(line == "Lorem ipsum :sit amet", line);
252     }*/
253     /*{
254         string line = "Lorem ipsum :sit amet";
255         immutable lorem = line.advancePast("");
256         assert(!lorem.length, lorem);
257         assert(line == "Lorem ipsum :sit amet", line);
258     }*/
259     {
260         string line = "Lorem ipsum :sit amet";
261         immutable lorem = line.advancePast("Lorem ipsum");
262         assert(!lorem.length, lorem);
263         assert(line == " :sit amet", line);
264     }
265     {
266         string line = "Lorem ipsum :sit amet";
267         immutable lorem = line.advancePast("Lorem ipsum");
268         assert(!lorem.length, lorem);
269         assert(line == " :sit amet", line);
270     }
271     {
272         string line = "Lorem ipsum :sit amet";
273         immutable dchar dspace = ' ';
274         immutable lorem = line.advancePast(dspace);
275         assert(lorem == "Lorem", lorem);
276         assert(line == "ipsum :sit amet", line);
277     }
278     {
279         dstring dline = "Lorem ipsum :sit amet"d;
280         immutable dspace = " "d;
281         immutable lorem = dline.advancePast(dspace);
282         assert((lorem == "Lorem"d), lorem.to!string);
283         assert((dline == "ipsum :sit amet"d), dline.to!string);
284     }
285     {
286         dstring dline = "Lorem ipsum :sit amet"d;
287         immutable wchar wspace = ' ';
288         immutable lorem = dline.advancePast(wspace);
289         assert((lorem == "Lorem"d), lorem.to!string);
290         assert((dline == "ipsum :sit amet"d), dline.to!string);
291     }
292     {
293         wstring wline = "Lorem ipsum :sit amet"w;
294         immutable wchar wspace = ' ';
295         immutable lorem = wline.advancePast(wspace);
296         assert((lorem == "Lorem"w), lorem.to!string);
297         assert((wline == "ipsum :sit amet"w), wline.to!string);
298     }
299     {
300         wstring wline = "Lorem ipsum :sit amet"w;
301         immutable wspace = " "w;
302         immutable lorem = wline.advancePast(wspace);
303         assert((lorem == "Lorem"w), lorem.to!string);
304         assert((wline == "ipsum :sit amet"w), wline.to!string);
305     }
306     {
307         string user = "foo!bar@asdf.adsf.com";
308         user = user.advancePast('!');
309         assert((user == "foo"), user);
310     }
311     {
312         immutable def = "abc def ghi"[4..$].advancePast(" ");
313         assert((def == "def"), def);
314     }
315     {
316         import std.exception : assertThrown;
317         assertThrown!AdvanceException("abc def ghi"[4..$].advancePast(""));
318     }
319     {
320         string line = "Lorem ipsum";
321         immutable head = line.advancePast(" ");
322         assert((head == "Lorem"), head);
323         assert((line == "ipsum"), line);
324     }
325     {
326         string line = "Lorem";
327         immutable head = line.advancePast(" ", inherit: true);
328         assert((head == "Lorem"), head);
329         assert(!line.length, line);
330     }
331     {
332         string slice = "verb";
333         string verb;
334 
335         if (slice.indexOf(' ') != -1)
336         {
337             verb = slice.advancePast(' ');
338         }
339         else
340         {
341             verb = slice;
342             slice = string.init;
343         }
344 
345         assert((verb == "verb"), verb);
346         assert(!slice.length, slice);
347     }
348     {
349         string slice = "verb";
350         immutable verb = slice.advancePast(' ', inherit: true);
351         assert((verb == "verb"), verb);
352         assert(!slice.length, slice);
353     }
354     {
355         string url = "https://google.com/index.html#fragment-identifier";
356         url = url.advancePast('#', inherit: true);
357         assert((url == "https://google.com/index.html"), url);
358     }
359     {
360         string url = "https://google.com/index.html";
361         url = url.advancePast('#', inherit: true);
362         assert((url == "https://google.com/index.html"), url);
363     }
364     {
365         string line = "Lorem ipsum sit amet";
366         string[] words;
367 
368         while (line.length > 0)
369         {
370             immutable word = line.advancePast(" ", inherit: true);
371             words ~= word;
372         }
373 
374         assert(words == [ "Lorem", "ipsum", "sit", "amet" ]);
375     }
376     {
377         import std.exception : assertThrown;
378         string url = "https://google.com/index.html#fragment-identifier";
379         assertThrown!AdvanceException(url.advancePast("", inherit: true));
380     }
381 }
382 
383 
384 // AdvanceException
385 /++
386     Exception, to be thrown when a call to [advancePast] went wrong.
387 
388     It is a normal [object.Exception|Exception] but with an attached needle and haystack.
389  +/
390 abstract class AdvanceException : Exception
391 {
392     /++
393         Returns a string of the original haystack the call to [advancePast] was operating on.
394      +/
395     string haystack() pure @safe;
396 
397     /++
398         Returns a string of the original needle the call to [advancePast] was operating on.
399      +/
400     string needle() pure @safe;
401 
402     /++
403         Create a new [AdvanceExceptionImpl], without attaching anything.
404      +/
405     this(
406         const string message,
407         const string file = __FILE__,
408         const size_t line = __LINE__,
409         Throwable nextInChain = null) pure nothrow @nogc @safe
410     {
411         super(message, file, line, nextInChain);
412     }
413 }
414 
415 
416 // AdvanceExceptionImpl
417 /++
418     Exception, to be thrown when a call to [advancePast] went wrong.
419 
420     This is the templated implementation, so that we can support more than one
421     kind of needle and haystack combination.
422 
423     It is a normal [object.Exception|Exception] but with an attached needle and haystack.
424 
425     Params:
426         Haystack = Haystack array type.
427         Needle = Needle array or char-like type.
428  +/
429 final class AdvanceExceptionImpl(Haystack, Needle) : AdvanceException
430 {
431 private:
432     import std.conv : to;
433 
434     /++
435         Raw haystack that `haystack` converts to string and returns.
436      +/
437     string _haystack;
438 
439     /++
440         Raw needle that `needle` converts to string and returns.
441      +/
442     string _needle;
443 
444 public:
445     /++
446         Returns a string of the original needle the call to `advancePast` was operating on.
447 
448         Returns:
449             The raw haystack (be it any kind of string), converted to a `string`.
450      +/
451     override string haystack() pure @safe
452     {
453         return _haystack;
454     }
455 
456     /++
457         Returns a string of the original needle the call to `advancePast` was operating on.
458 
459         Returns:
460             The raw needle (be it any kind of string or character), converted to a `string`.
461      +/
462     override string needle() pure @safe
463     {
464         return _needle;
465     }
466 
467     /++
468         Create a new `AdvanceExceptionImpl`, without attaching anything.
469      +/
470     this(
471         const string message,
472         const string file = __FILE__,
473         const size_t line = __LINE__,
474         Throwable nextInChain = null) pure @safe nothrow @nogc
475     {
476         super(message, file, line, nextInChain);
477     }
478 
479     /++
480         Create a new `AdvanceExceptionImpl`, attaching a command.
481      +/
482     this(
483         const string message,
484         const Haystack haystack,
485         const Needle needle,
486         const string file = __FILE__,
487         const size_t line = __LINE__,
488         Throwable nextInChain = null) pure @safe
489     {
490         this._haystack = haystack.to!string;
491         this._needle = needle.to!string;
492         super(message, file, line, nextInChain);
493     }
494 }
495 
496 
497 // plurality
498 /++
499     Selects the correct singular or plural form of a word depending on the
500     numerical count of it.
501 
502     Technically works with any type provided the number is some comparable integral.
503 
504     Example:
505     ---
506     string one = 1.plurality("one", "two");
507     string two = 2.plurality("one", "two");
508     string many = (-2).plurality("one", "many");
509     string many0 = 0.plurality("one", "many");
510 
511     assert((one == "one"), one);
512     assert((two == "two"), two);
513     assert((many == "many"), many);
514     assert((many0 == "many"), many0);
515     ---
516 
517     Params:
518         num = Numerical count.
519         singular = The singular form.
520         plural = The plural form.
521 
522     Returns:
523         The singular if num is `1` or `-1`, otherwise the plural.
524  +/
525 pragma(inline, true)
526 T plurality(Num, T)(
527     const Num num,
528     const return scope T singular,
529     const return scope T plural) pure nothrow @nogc
530 {
531     import std.traits : isIntegral;
532 
533     static if (!isIntegral!Num)
534     {
535         enum message = "`plurality` only works with integral types";
536         static assert(0, message);
537     }
538 
539     return ((num == 1) || (num == -1)) ? singular : plural;
540 }
541 
542 ///
543 unittest
544 {
545     static assert(10.plurality("one","many") == "many");
546     static assert(1.plurality("one", "many") == "one");
547     static assert((-1).plurality("one", "many") == "one");
548     static assert(0.plurality("one", "many") == "many");
549 }
550 
551 
552 // unenclosed
553 /++
554     Removes paired preceding and trailing tokens around a string line.
555     Assumes ASCII.
556 
557     You should not need to use this directly; rather see [unquoted] and
558     [unsinglequoted].
559 
560     Params:
561         token = Token character to strip away.
562         line = String line to remove any enclosing tokens from.
563 
564     Returns:
565         A slice of the passed string line without enclosing tokens.
566  +/
567 private auto unenclosed(char token = '"')(/*const*/ return scope string line) pure nothrow @nogc
568 {
569     enum escaped = "\\" ~ token;
570 
571     if (line.length < 2)
572     {
573         return line;
574     }
575     else if ((line[0] == token) && (line[$-1] == token))
576     {
577         if ((line.length >= 3) && (line[$-2..$] == escaped))
578         {
579             // End quote is escaped
580             return line;
581         }
582 
583         return line[1..$-1].unenclosed!token;
584     }
585     else
586     {
587         return line;
588     }
589 }
590 
591 
592 // unquoted
593 /++
594     Removes paired preceding and trailing double quotes, unquoting a word.
595     Assumes ASCII.
596 
597     Does not decode the string and may thus give weird results on weird inputs.
598 
599     Example:
600     ---
601     string quoted = `"This is a quote"`;
602     string unquotedLine = quoted.unquoted;
603     assert((unquotedLine == "This is a quote"), unquotedLine);
604     ---
605 
606     Params:
607         line = The (potentially) quoted string.
608 
609     Returns:
610         A slice of the `line` argument that excludes the quotes.
611  +/
612 pragma(inline, true)
613 auto unquoted(/*const*/ return scope string line) pure nothrow @nogc
614 {
615     return unenclosed!'"'(line);
616 }
617 
618 ///
619 unittest
620 {
621     assert(`"Lorem ipsum sit amet"`.unquoted == "Lorem ipsum sit amet");
622     assert(`"""""Lorem ipsum sit amet"""""`.unquoted == "Lorem ipsum sit amet");
623     // Unbalanced quotes are left untouched
624     assert(`"Lorem ipsum sit amet`.unquoted == `"Lorem ipsum sit amet`);
625     assert(`"Lorem \"`.unquoted == `"Lorem \"`);
626     assert("\"Lorem \\\"".unquoted == "\"Lorem \\\"");
627     assert(`"\"`.unquoted == `"\"`);
628 }
629 
630 
631 // unsinglequoted
632 /++
633     Removes paired preceding and trailing single quotes around a line.
634     Assumes ASCII.
635 
636     Does not decode the string and may thus give weird results on weird inputs.
637 
638     Example:
639     ---
640     string quoted = `'This is single-quoted'`;
641     string unquotedLine = quoted.unsinglequoted;
642     assert((unquotedLine == "This is single-quoted"), unquotedLine);
643     ---
644 
645     Params:
646         line = The (potentially) single-quoted string.
647 
648     Returns:
649         A slice of the `line` argument that excludes the single-quotes.
650  +/
651 pragma(inline, true)
652 auto unsinglequoted(/*const*/ return scope string line) pure nothrow @nogc
653 {
654     return unenclosed!'\''(line);
655 }
656 
657 ///
658 unittest
659 {
660     assert(`'Lorem ipsum sit amet'`.unsinglequoted == "Lorem ipsum sit amet");
661     assert(`''''Lorem ipsum sit amet''''`.unsinglequoted == "Lorem ipsum sit amet");
662     // Unbalanced quotes are left untouched
663     assert(`'Lorem ipsum sit amet`.unsinglequoted == `'Lorem ipsum sit amet`);
664     assert(`'Lorem \'`.unsinglequoted == `'Lorem \'`);
665     assert("'Lorem \\'".unsinglequoted == "'Lorem \\'");
666     assert(`'`.unsinglequoted == `'`);
667 }
668 
669 
670 // stripSuffix
671 /++
672     Strips the supplied string from the end of a string.
673 
674     Example:
675     ---
676     string suffixed = "Kameloso";
677     string stripped = suffixed.stripSuffix("oso");
678     assert((stripped == "Kamel"), stripped);
679     ---
680 
681     Params:
682         line = Original line to strip the suffix from.
683         suffix = Suffix string to strip.
684 
685     Returns:
686         `line` with `suffix` sliced off the end.
687  +/
688 auto stripSuffix(
689     /*const*/ return scope string line,
690     const scope string suffix) pure nothrow @nogc
691 {
692     if (line.length < suffix.length) return line;
693     return (line[($-suffix.length)..$] == suffix) ? line[0..($-suffix.length)] : line;
694 }
695 
696 ///
697 unittest
698 {
699     immutable line = "harblsnarbl";
700     assert(line.stripSuffix("snarbl") == "harbl");
701     assert(line.stripSuffix("") == "harblsnarbl");
702     assert(line.stripSuffix("INVALID") == "harblsnarbl");
703     assert(!line.stripSuffix("harblsnarbl").length);
704 }
705 
706 
707 // tabs
708 /++
709     Returns a range of *spaces* equal to that of `num` tabs (\t).
710 
711     Use [std.conv.to] or [std.conv.text] or similar to flatten to a string.
712 
713     Example:
714     ---
715     string indentation = 2.tabs.text;
716     assert((indentation == "        "), `"` ~  indentation ~ `"`);
717     string smallIndent = 1.tabs!2.text;
718     assert((smallIndent == "  "), `"` ~  smallIndent ~ `"`);
719     ---
720 
721     Params:
722         spaces = How many spaces make up a tab.
723         num = How many tabs we want.
724 
725     Returns:
726         A range of whitespace equalling (`num` * `spaces`) spaces.
727  +/
728 auto tabs(uint spaces = 4)(const int num) pure nothrow @nogc
729 in ((num >= 0), "Negative number of tabs passed to `tabs`")
730 {
731     import std.range : repeat, takeExactly;
732     import std.algorithm.iteration : joiner;
733     import std.array : array;
734 
735     enum char[spaces] tab = ' '.repeat.takeExactly(spaces).array;
736     return tab[].repeat.takeExactly(num).joiner;
737 }
738 
739 ///
740 @system
741 unittest
742 {
743     import std.array : Appender;
744     import std.conv : to;
745     import std.exception : assertThrown;
746     import std.format : formattedWrite;
747     import std.algorithm.comparison : equal;
748     import core.exception : AssertError;
749 
750     auto one = 1.tabs!4;
751     auto two = 2.tabs!3;
752     auto three = 3.tabs!2;
753     auto zero = 0.tabs;
754 
755     assert(one.equal("    "), one.to!string);
756     assert(two.equal("      "), two.to!string);
757     assert(three.equal("      "), three.to!string);
758     assert(zero.equal(string.init), zero.to!string);
759 
760     assertThrown!AssertError((-1).tabs);
761 
762     Appender!(char[]) sink;
763     sink.formattedWrite("%sHello world", 2.tabs!2);
764     assert((sink.data == "    Hello world"), sink.data);
765 }
766 
767 
768 // indentInto
769 /++
770     Indents lines in a string into an output range sink with the supplied number of tabs.
771 
772     Params:
773         spaces = How many spaces in an indenting tab.
774         wallOfText = String to indent the individual lines of.
775         sink = Output range to fill with the indented lines.
776         numTabs = Optional amount of tabs to indent with, default 1.
777         skip = How many lines to skip indenting.
778  +/
779 void indentInto(uint spaces = 4, Sink)
780     (const string wallOfText,
781     auto ref Sink sink,
782     const uint numTabs = 1,
783     const uint skip = 0)
784 {
785     import std.algorithm.iteration : splitter;
786     import std.range : enumerate;
787     import std.range : isOutputRange;
788 
789     static if (!isOutputRange!(Sink, char[]))
790     {
791         enum message = "`indentInto` only works with output ranges of `char[]`";
792         static assert(0, message);
793     }
794 
795     if (numTabs == 0)
796     {
797         sink.put(wallOfText);
798         return;
799     }
800 
801     // Must be mutable to work with formattedWrite. That or .to!string
802     auto indent = numTabs.tabs!spaces;
803 
804     foreach (immutable i, immutable line; wallOfText.splitter("\n").enumerate)
805     {
806         if (i > 0) sink.put("\n");
807 
808         if (!line.length)
809         {
810             sink.put("\n");
811             continue;
812         }
813 
814         if (skip > i)
815         {
816             sink.put(line);
817         }
818         else
819         {
820             // Cannot just put(indent), put(line) because indent is a joiner Result
821             import std.format : formattedWrite;
822             sink.formattedWrite("%s%s", indent, line);
823         }
824     }
825 }
826 
827 ///
828 unittest
829 {
830     import std.array : Appender;
831 
832     Appender!(char[]) sink;
833 
834     immutable string_ =
835 "Lorem ipsum
836 sit amet
837 I don't remember
838 any more offhand
839 so shrug";
840 
841     string_.indentInto(sink);
842     assert((sink.data ==
843 "    Lorem ipsum
844     sit amet
845     I don't remember
846     any more offhand
847     so shrug"), '\n' ~ sink.data);
848 
849     sink.clear();
850     string_.indentInto!3(sink, 2);
851     assert((sink.data ==
852 "      Lorem ipsum
853       sit amet
854       I don't remember
855       any more offhand
856       so shrug"), '\n' ~ sink.data);
857 
858     sink.clear();
859     string_.indentInto(sink, 0);
860     assert((sink.data ==
861 "Lorem ipsum
862 sit amet
863 I don't remember
864 any more offhand
865 so shrug"), '\n' ~ sink.data);
866 }
867 
868 
869 // indent
870 /++
871     Indents lines in a string with the supplied number of tabs. Returns a newly
872     allocated string.
873 
874     Params:
875         spaces = How many spaces make up a tab.
876         wallOfText = String to indent the lines of.
877         numTabs = Amount of tabs to indent with, default 1.
878         skip = How many lines to skip indenting.
879 
880     Returns:
881         A string with all the lines of the original string indented.
882  +/
883 string indent(uint spaces = 4)
884     (const string wallOfText,
885     const uint numTabs = 1,
886     const uint skip = 0) pure
887 {
888     import std.array : Appender;
889 
890     Appender!(char[]) sink;
891     sink.reserve(wallOfText.length + 10*spaces*numTabs);  // Extra room for 10 indents
892 
893     wallOfText.indentInto!spaces(sink, numTabs, skip);
894     return sink.data;
895 }
896 
897 ///
898 unittest
899 {
900     immutable string_ =
901 "Lorem ipsum
902 sit amet
903 I don't remember
904 any more offhand
905 so shrug";
906 
907     immutable indentedOne = string_.indent;
908     assert((indentedOne ==
909 "    Lorem ipsum
910     sit amet
911     I don't remember
912     any more offhand
913     so shrug"), '\n' ~ indentedOne);
914 
915     immutable indentedTwo = string_.indent(2);
916     assert((indentedTwo ==
917 "        Lorem ipsum
918         sit amet
919         I don't remember
920         any more offhand
921         so shrug"), '\n' ~ indentedTwo);
922 
923     immutable indentedZero = string_.indent(0);
924     assert((indentedZero ==
925 "Lorem ipsum
926 sit amet
927 I don't remember
928 any more offhand
929 so shrug"), '\n' ~ indentedZero);
930 
931     immutable indentedSkipTwo = string_.indent(1, 2);
932     assert((indentedSkipTwo ==
933 "Lorem ipsum
934 sit amet
935     I don't remember
936     any more offhand
937     so shrug"), '\n' ~ indentedSkipTwo);
938 }
939 
940 
941 // strippedRight
942 /++
943     Returns a slice of the passed string with any trailing whitespace and/or
944     linebreaks sliced off. Overload that implicitly strips `" \n\r\t"`.
945 
946     Duplicates [std.string.stripRight], which we can no longer trust not to
947     assert on unexpected input.
948 
949     Params:
950         line = Line to strip the right side of.
951 
952     Returns:
953         The passed line without any trailing whitespace or linebreaks.
954  +/
955 auto strippedRight(/*const*/ return scope string line) pure nothrow @nogc
956 {
957     if (!line.length) return line;
958     return strippedRight(line, " \n\r\t");
959 }
960 
961 ///
962 unittest
963 {
964     static if (!is(typeof("blah".strippedRight) == string))
965     {
966         enum message = "`lu.string.strippedRight` should return a mutable string";
967         static assert(0, message);
968     }
969 
970     {
971         immutable trailing = "abc  ";
972         immutable stripped = trailing.strippedRight;
973         assert((stripped == "abc"), stripped);
974     }
975     {
976         immutable trailing = "  ";
977         immutable stripped = trailing.strippedRight;
978         assert((stripped == ""), stripped);
979     }
980     {
981         immutable empty = "";
982         immutable stripped = empty.strippedRight;
983         assert((stripped == ""), stripped);
984     }
985     {
986         immutable noTrailing = "abc";
987         immutable stripped = noTrailing.strippedRight;
988         assert((stripped == "abc"), stripped);
989     }
990     {
991         immutable linebreak = "abc\r\n  \r\n";
992         immutable stripped = linebreak.strippedRight;
993         assert((stripped == "abc"), stripped);
994     }
995 }
996 
997 
998 // strippedRight
999 /++
1000     Returns a slice of the passed string with any trailing passed characters.
1001     Implementation template capable of handling both individual characters and
1002     string of tokens to strip.
1003 
1004     Duplicates [std.string.stripRight], which we can no longer trust not to
1005     assert on unexpected input.
1006 
1007     Params:
1008         line = Line to strip the right side of.
1009         chaff = Character or string of characters to strip away.
1010 
1011     Returns:
1012         The passed line without any trailing passed characters.
1013  +/
1014 auto strippedRight(Line, Chaff)
1015     (/*const*/ return scope Line line,
1016     const scope Chaff chaff) pure nothrow @nogc
1017 {
1018     import std.traits : isArray;
1019     import std.range : ElementEncodingType, ElementType;
1020 
1021     static if (!isArray!Line)
1022     {
1023         enum message = "`strippedRight` only works on strings and arrays";
1024         static assert(0, message);
1025     }
1026     else static if (
1027         !is(Chaff : Line) &&
1028         !is(Chaff : ElementType!Line) &&
1029         !is(Chaff : ElementEncodingType!Line))
1030     {
1031         enum message = "`strippedRight` only works with array- or single-element-type chaff";
1032         static assert(0, message);
1033     }
1034 
1035     if (!line.length) return line;
1036 
1037     static if (isArray!Chaff)
1038     {
1039         if (!chaff.length) return line;
1040     }
1041 
1042     size_t pos = line.length;
1043 
1044     loop:
1045     while (pos > 0)
1046     {
1047         static if (isArray!Chaff)
1048         {
1049             import std.string : representation;
1050 
1051             immutable currentChar = line[pos-1];
1052 
1053             foreach (immutable c; chaff.representation)
1054             {
1055                 if (currentChar == c)
1056                 {
1057                     // Found a char we should strip
1058                     --pos;
1059                     continue loop;
1060                 }
1061             }
1062         }
1063         else
1064         {
1065             if (line[pos-1] == chaff)
1066             {
1067                 --pos;
1068                 continue loop;
1069             }
1070         }
1071 
1072         break loop;
1073     }
1074 
1075     return line[0..pos];
1076 }
1077 
1078 ///
1079 unittest
1080 {
1081     {
1082         immutable trailing = "abc,";
1083         immutable stripped = trailing.strippedRight(',');
1084         assert((stripped == "abc"), stripped);
1085     }
1086     {
1087         immutable trailing = "abc!!!";
1088         immutable stripped = trailing.strippedRight('!');
1089         assert((stripped == "abc"), stripped);
1090     }
1091     {
1092         immutable trailing = "abc";
1093         immutable stripped = trailing.strippedRight(' ');
1094         assert((stripped == "abc"), stripped);
1095     }
1096     {
1097         immutable trailing = "";
1098         immutable stripped = trailing.strippedRight(' ');
1099         assert(!stripped.length, stripped);
1100     }
1101     {
1102         immutable trailing = "abc,!.-";
1103         immutable stripped = trailing.strippedRight("-.!,");
1104         assert((stripped == "abc"), stripped);
1105     }
1106     {
1107         immutable trailing = "abc!!!";
1108         immutable stripped = trailing.strippedRight("!");
1109         assert((stripped == "abc"), stripped);
1110     }
1111     {
1112         immutable trailing = "abc";
1113         immutable stripped = trailing.strippedRight(" ABC");
1114         assert((stripped == "abc"), stripped);
1115     }
1116     {
1117         immutable trailing = "";
1118         immutable stripped = trailing.strippedRight(" ");
1119         assert(!stripped.length, stripped);
1120     }
1121 }
1122 
1123 
1124 // strippedLeft
1125 /++
1126     Returns a slice of the passed string with any preceding whitespace and/or
1127     linebreaks sliced off. Overload that implicitly strips `" \n\r\t"`.
1128 
1129     Duplicates [std.string.stripLeft], which we can no longer trust not to
1130     assert on unexpected input.
1131 
1132     Params:
1133         line = Line to strip the left side of.
1134 
1135     Returns:
1136         The passed line without any preceding whitespace or linebreaks.
1137  +/
1138 auto strippedLeft(/*const*/ return scope string line) pure nothrow @nogc
1139 {
1140     if (!line.length) return line;
1141     return strippedLeft(line, " \n\r\t");
1142 }
1143 
1144 ///
1145 unittest
1146 {
1147     static if (!is(typeof("blah".strippedLeft) == string))
1148     {
1149         enum message = "`lu.string.strippedLeft` should return a mutable string";
1150         static assert(0, message);
1151     }
1152 
1153     {
1154         immutable preceded = "   abc";
1155         immutable stripped = preceded.strippedLeft;
1156         assert((stripped == "abc"), stripped);
1157     }
1158     {
1159         immutable preceded = "   ";
1160         immutable stripped = preceded.strippedLeft;
1161         assert((stripped == ""), stripped);
1162     }
1163     {
1164         immutable empty = "";
1165         immutable stripped = empty.strippedLeft;
1166         assert((stripped == ""), stripped);
1167     }
1168     {
1169         immutable noPreceded = "abc";
1170         immutable stripped = noPreceded.strippedLeft;
1171         assert((stripped == noPreceded), stripped);
1172     }
1173     {
1174         immutable linebreak  = "\r\n\r\n  abc";
1175         immutable stripped = linebreak.strippedLeft;
1176         assert((stripped == "abc"), stripped);
1177     }
1178 }
1179 
1180 
1181 // strippedLeft
1182 /++
1183     Returns a slice of the passed string with any preceding passed characters
1184     sliced off. Implementation capable of handling both individual characters
1185     and strings of tokens to strip.
1186 
1187     Duplicates [std.string.stripLeft], which we can no longer trust not to
1188     assert on unexpected input.
1189 
1190     Params:
1191         line = Line to strip the left side of.
1192         chaff = Character or string of characters to strip away.
1193 
1194     Returns:
1195         The passed line without any preceding passed characters.
1196  +/
1197 auto strippedLeft(Line, Chaff)
1198     (/*const*/ return scope Line line,
1199     const scope Chaff chaff) pure nothrow @nogc
1200 {
1201     import std.traits : isArray;
1202     import std.range : ElementEncodingType, ElementType;
1203 
1204     static if (!isArray!Line)
1205     {
1206         enum message = "`strippedLeft` only works on strings and arrays";
1207         static assert(0, message);
1208     }
1209     else static if (
1210         !is(Chaff : Line) &&
1211         !is(Chaff : ElementType!Line) &&
1212         !is(Chaff : ElementEncodingType!Line))
1213     {
1214         enum message = "`strippedLeft` only works with array- or single-element-type chaff";
1215         static assert(0, message);
1216     }
1217 
1218     if (!line.length) return line;
1219 
1220     static if (isArray!Chaff)
1221     {
1222         if (!chaff.length) return line;
1223     }
1224 
1225     size_t pos;
1226 
1227     loop:
1228     while (pos < line.length)
1229     {
1230         static if (isArray!Chaff)
1231         {
1232             import std.string : representation;
1233 
1234             immutable currentChar = line[pos];
1235 
1236             foreach (immutable c; chaff.representation)
1237             {
1238                 if (currentChar == c)
1239                 {
1240                     // Found a char we should strip
1241                     ++pos;
1242                     continue loop;
1243                 }
1244             }
1245         }
1246         else
1247         {
1248             if (line[pos] == chaff)
1249             {
1250                 ++pos;
1251                 continue loop;
1252             }
1253         }
1254 
1255         break loop;
1256     }
1257 
1258     return line[pos..$];
1259 }
1260 
1261 ///
1262 unittest
1263 {
1264     {
1265         immutable trailing = ",abc";
1266         immutable stripped = trailing.strippedLeft(',');
1267         assert((stripped == "abc"), stripped);
1268     }
1269     {
1270         immutable trailing = "!!!abc";
1271         immutable stripped = trailing.strippedLeft('!');
1272         assert((stripped == "abc"), stripped);
1273     }
1274     {
1275         immutable trailing = "abc";
1276         immutable stripped = trailing.strippedLeft(' ');
1277         assert((stripped == "abc"), stripped);
1278     }
1279     {
1280         immutable trailing = "";
1281         immutable stripped = trailing.strippedLeft(' ');
1282         assert(!stripped.length, stripped);
1283     }
1284     {
1285         immutable trailing = ",abc";
1286         immutable stripped = trailing.strippedLeft(",");
1287         assert((stripped == "abc"), stripped);
1288     }
1289     {
1290         immutable trailing = "!!!abc";
1291         immutable stripped = trailing.strippedLeft(",1!");
1292         assert((stripped == "abc"), stripped);
1293     }
1294     {
1295         immutable trailing = "abc";
1296         immutable stripped = trailing.strippedLeft(" ");
1297         assert((stripped == "abc"), stripped);
1298     }
1299     {
1300         immutable trailing = "";
1301         immutable stripped = trailing.strippedLeft(" ");
1302         assert(!stripped.length, stripped);
1303     }
1304 }
1305 
1306 
1307 // stripped
1308 /++
1309     Returns a slice of the passed string with any preceding or trailing
1310     whitespace or linebreaks sliced off both ends. Overload that implicitly
1311     strips `" \n\r\t"`.
1312 
1313     It merely calls both [strippedLeft] and [strippedRight]. As such it
1314     duplicates [std.string.strip], which we can no longer trust not to assert
1315     on unexpected input.
1316 
1317     Params:
1318         line = Line to strip both the right and left side of.
1319 
1320     Returns:
1321         The passed line, stripped of surrounding whitespace.
1322  +/
1323 auto stripped(/*const*/ return scope string line) pure nothrow @nogc
1324 {
1325     return line.strippedLeft.strippedRight;
1326 }
1327 
1328 ///
1329 unittest
1330 {
1331     static if (!is(typeof("blah".stripped) == string))
1332     {
1333         enum message = "`lu.string.stripped` should return a mutable string";
1334         static assert(0, message);
1335     }
1336 
1337     {
1338         immutable line = "   abc   ";
1339         immutable stripped_ = line.stripped;
1340         assert((stripped_ == "abc"), stripped_);
1341     }
1342     {
1343         immutable line = "   ";
1344         immutable stripped_ = line.stripped;
1345         assert((stripped_ == ""), stripped_);
1346     }
1347     {
1348         immutable line = "";
1349         immutable stripped_ = line.stripped;
1350         assert((stripped_ == ""), stripped_);
1351     }
1352     {
1353         immutable line = "abc";
1354         immutable stripped_ = line.stripped;
1355         assert((stripped_ == "abc"), stripped_);
1356     }
1357     {
1358         immutable line = " \r\n  abc\r\n\r\n";
1359         immutable stripped_ = line.stripped;
1360         assert((stripped_ == "abc"), stripped_);
1361     }
1362 }
1363 
1364 
1365 // stripped
1366 /++
1367     Returns a slice of the passed string with any preceding or trailing
1368     passed characters sliced off. Implementation template capable of handling both
1369     individual characters and strings of tokens to strip.
1370 
1371     It merely calls both [strippedLeft] and [strippedRight]. As such it
1372     duplicates [std.string.strip], which we can no longer trust not to assert
1373     on unexpected input.
1374 
1375     Params:
1376         line = Line to strip both the right and left side of.
1377         chaff = Character or string of characters to strip away.
1378 
1379     Returns:
1380         The passed line, stripped of surrounding passed characters.
1381  +/
1382 auto stripped(Line, Chaff)
1383     (/*const*/ return scope Line line,
1384     const scope Chaff chaff) pure nothrow @nogc
1385 {
1386     import std.traits : isArray;
1387     import std.range : ElementEncodingType, ElementType;
1388 
1389     static if (!isArray!Line)
1390     {
1391         enum message = "`stripped` only works on strings and arrays";
1392         static assert(0, message);
1393     }
1394     else static if (
1395         !is(Chaff : Line) &&
1396         !is(Chaff : ElementType!Line) &&
1397         !is(Chaff : ElementEncodingType!Line))
1398     {
1399         enum message = "`stripped` only works with array- or single-element-type chaff";
1400         static assert(0, message);
1401     }
1402 
1403     return line.strippedLeft(chaff).strippedRight(chaff);
1404 }
1405 
1406 ///
1407 unittest
1408 {
1409     {
1410         immutable line = "   abc   ";
1411         immutable stripped_ = line.stripped(' ');
1412         assert((stripped_ == "abc"), stripped_);
1413     }
1414     {
1415         immutable line = "!!!";
1416         immutable stripped_ = line.stripped('!');
1417         assert((stripped_ == ""), stripped_);
1418     }
1419     {
1420         immutable line = "";
1421         immutable stripped_ = line.stripped('_');
1422         assert((stripped_ == ""), stripped_);
1423     }
1424     {
1425         immutable line = "abc";
1426         immutable stripped_ = line.stripped('\t');
1427         assert((stripped_ == "abc"), stripped_);
1428     }
1429     {
1430         immutable line = " \r\n  abc\r\n\r\n  ";
1431         immutable stripped_ = line.stripped(' ');
1432         assert((stripped_ == "\r\n  abc\r\n\r\n"), stripped_);
1433     }
1434     {
1435         immutable line = "   abc   ";
1436         immutable stripped_ = line.stripped(" \t");
1437         assert((stripped_ == "abc"), stripped_);
1438     }
1439     {
1440         immutable line = "!,!!";
1441         immutable stripped_ = line.stripped("!,");
1442         assert((stripped_ == ""), stripped_);
1443     }
1444     {
1445         immutable line = "";
1446         immutable stripped_ = line.stripped("_");
1447         assert((stripped_ == ""), stripped_);
1448     }
1449     {
1450         immutable line = "abc";
1451         immutable stripped_ = line.stripped("\t\r\n");
1452         assert((stripped_ == "abc"), stripped_);
1453     }
1454     {
1455         immutable line = " \r\n  abc\r\n\r\n  ";
1456         immutable stripped_ = line.stripped(" _");
1457         assert((stripped_ == "\r\n  abc\r\n\r\n"), stripped_);
1458     }
1459 }
1460 
1461 
1462 // encode64
1463 /++
1464     Base64-encodes a string.
1465 
1466     Merely wraps [std.base64.Base64.encode|Base64.encode] and
1467     [std.string.representation] into one function that will work with strings.
1468 
1469     Params:
1470         line = String line to encode.
1471 
1472     Returns:
1473         An encoded Base64 string.
1474 
1475     See_Also:
1476         - https://en.wikipedia.org/wiki/Base64
1477  +/
1478 string encode64(const string line) pure nothrow
1479 {
1480     import std.base64 : Base64;
1481     import std.string : representation;
1482 
1483     return Base64.encode(line.representation);
1484 }
1485 
1486 ///
1487 unittest
1488 {
1489     {
1490         immutable password = "harbl snarbl 12345";
1491         immutable encoded = encode64(password);
1492         assert((encoded == "aGFyYmwgc25hcmJsIDEyMzQ1"), encoded);
1493     }
1494     {
1495         immutable string password;
1496         immutable encoded = encode64(password);
1497         assert(!encoded.length, encoded);
1498     }
1499 }
1500 
1501 
1502 // decode64
1503 /++
1504     Base64-decodes a string.
1505 
1506     Merely wraps [std.base64.Base64.decode|Base64.decode] and
1507     [std.string.representation] into one function that will work with strings.
1508 
1509     Params:
1510         encoded = Encoded string to decode.
1511 
1512     Returns:
1513         A decoded normal string.
1514 
1515     See_Also:
1516         - https://en.wikipedia.org/wiki/Base64
1517  +/
1518 string decode64(const string encoded) pure
1519 {
1520     import std.base64 : Base64;
1521     return (cast(char[])Base64.decode(encoded)).idup;
1522 }
1523 
1524 ///
1525 unittest
1526 {
1527     {
1528         immutable password = "base64:aGFyYmwgc25hcmJsIDEyMzQ1";
1529         immutable decoded = decode64(password[7..$]);
1530         assert((decoded == "harbl snarbl 12345"), decoded);
1531     }
1532     {
1533         immutable password = "base64:";
1534         immutable decoded = decode64(password[7..$]);
1535         assert(!decoded.length, decoded);
1536     }
1537 }
1538 
1539 
1540 // splitLineAtPosition
1541 /++
1542     Splits a string with on boundary as delimited by a supplied separator, into
1543     one or more more lines not longer than the passed maximum length.
1544 
1545     If a line cannot be split due to the line being too short or the separator
1546     not occurring in the text, it is added to the returned array as-is and no
1547     more splitting is done.
1548 
1549     Example:
1550     ---
1551     string line = "I am a fish in a sort of long sentence~";
1552     enum maxLineLength = 20;
1553     auto splitLines = line.splitLineAtPosition(' ', maxLineLength);
1554 
1555     assert(splitLines[0] == "I am a fish in a");
1556     assert(splitLines[1] == "sort of a long");
1557     assert(splitLines[2] == "sentence~");
1558     ---
1559 
1560     Params:
1561         line = String line to split.
1562         separator = Separator character with which to split the `line`.
1563         maxLength = Maximum length of the separated lines.
1564 
1565     Returns:
1566         A `T[]` array with lines split out of the passed `line`.
1567  +/
1568 auto splitLineAtPosition(Line, Separator)
1569     (const Line line,
1570     const Separator separator,
1571     const size_t maxLength) pure //nothrow
1572 in
1573 {
1574     static if (is(Separator : Line))
1575     {
1576         enum message = "Tried to `splitLineAtPosition` but no " ~
1577             "`separator` was supplied";
1578         assert(separator.length, message);
1579     }
1580 }
1581 do
1582 {
1583     import std.traits : isArray;
1584     import std.range : ElementEncodingType, ElementType;
1585 
1586     static if (!isArray!Line)
1587     {
1588         enum message = "`splitLineAtPosition` only works on strings and arrays";
1589         static assert(0, message);
1590     }
1591     else static if (
1592         !is(Separator : Line) &&
1593         !is(Separator : ElementType!Line) &&
1594         !is(Separator : ElementEncodingType!Line))
1595     {
1596         enum message = "`splitLineAtPosition` only works on strings and arrays of characters";
1597         static assert(0, message);
1598     }
1599 
1600     string[] lines;
1601     if (!line.length) return lines;
1602 
1603     string slice = line;  // mutable
1604     lines.reserve(cast(int)(line.length / maxLength) + 1);
1605 
1606     whileloop:
1607     while(true)
1608     {
1609         import std.algorithm.comparison : min;
1610 
1611         for (size_t i = min(maxLength, slice.length); i > 0; --i)
1612         {
1613             if (slice[i-1] == separator)
1614             {
1615                 lines ~= slice[0..i-1];
1616                 slice = slice[i..$];
1617                 continue whileloop;
1618             }
1619         }
1620         break;
1621     }
1622 
1623     if (slice.length)
1624     {
1625         // Remnant
1626 
1627         if (lines.length)
1628         {
1629             lines[$-1] ~= separator ~ slice;
1630         }
1631         else
1632         {
1633             // Max line was too short to fit anything. Returning whole line
1634             lines ~= slice;
1635         }
1636     }
1637 
1638     return lines;
1639 }
1640 
1641 ///
1642 unittest
1643 {
1644     import std.conv : text;
1645 
1646     {
1647         immutable prelude = "PRIVMSG #garderoben :";
1648         immutable maxLength = 250 - prelude.length;
1649 
1650         immutable rawLine = "Lorem ipsum dolor sit amet, ea has velit noluisse, " ~
1651             "eos eius appetere constituto no, ad quas natum eos. Perpetua " ~
1652             "electram mnesarchum usu ne, mei vero dolorem no. Ea quando scripta " ~
1653             "quo, minim legendos ut vel. Ut usu graece equidem posidonium. Ius " ~
1654             "denique ponderum verterem no, quo te mentitum officiis referrentur. " ~
1655             "Sed an dolor iriure vocibus. " ~
1656             "Lorem ipsum dolor sit amet, ea has velit noluisse, " ~
1657             "eos eius appetere constituto no, ad quas natum eos. Perpetua " ~
1658             "electram mnesarchum usu ne, mei vero dolorem no. Ea quando scripta " ~
1659             "quo, minim legendos ut vel. Ut usu graece equidem posidonium. Ius " ~
1660             "denique ponderum verterem no, quo te mentitum officiis referrentur. " ~
1661             "Sed an dolor iriure vocibus. ssssssssssssssssssssssssssssssssssss" ~
1662             "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~
1663             "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~
1664             "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~
1665             "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~
1666             "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~
1667             "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~
1668             "ssssssssssssssssssssssssssssssssssssssssssssssssssssssss";
1669         const splitLines = rawLine.splitLineAtPosition(' ', maxLength);
1670         assert((splitLines.length == 4), splitLines.length.text);
1671     }
1672     {
1673         immutable prelude = "PRIVMSG #garderoben :";
1674         immutable maxLength = 250 - prelude.length;
1675 
1676         immutable rawLine = "ssssssssssssssssssssssssssssssssssss" ~
1677             "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~
1678             "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~
1679             "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~
1680             "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~
1681             "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~
1682             "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" ~
1683             "ssssssssssssssssssssssssssssssssssssssssssssssssssssssss";
1684         const splitLines = rawLine.splitLineAtPosition(' ', maxLength);
1685         assert((splitLines.length == 1), splitLines.length.text);
1686         assert(splitLines[0] == rawLine);
1687     }
1688 }
1689 
1690 
1691 // escapeControlCharacters
1692 /++
1693     Replaces the control characters '\n', '\t', '\r' and '\0' with the escaped
1694     "\\n", "\\t", "\\r" and "\\0". Does not allocate a new string if there
1695     was nothing to escape.
1696 
1697     Params:
1698         line = String line to escape characters in.
1699 
1700     Returns:
1701         A new string with control characters escaped, or the original one unchanged.
1702  +/
1703 string escapeControlCharacters(/*const*/ return scope string line) pure nothrow
1704 {
1705     import std.array : Appender;
1706     import std.string : representation;
1707 
1708     if (!line.length) return line;
1709 
1710     Appender!(char[]) sink;
1711     size_t lastEnd;
1712     bool reserved;
1713 
1714     immutable asBytes = line.representation;
1715 
1716     void commitUpTo(const size_t i)
1717     {
1718         if (!reserved)
1719         {
1720             sink.reserve(asBytes.length + 16);  // guesstimate
1721             reserved = true;
1722         }
1723         sink.put(asBytes[lastEnd..i]);
1724     }
1725 
1726     for (size_t i; i<asBytes.length; ++i)
1727     {
1728         import std.algorithm.comparison : among;
1729 
1730         if (asBytes[i].among!('\n', '\t', '\r', '\0'))
1731         {
1732             commitUpTo(i);
1733             lastEnd = i+1;
1734 
1735             switch (asBytes[i])
1736             {
1737             case '\n': sink.put(`\n`); break;
1738             case '\t': sink.put(`\t`); break;
1739             case '\r': sink.put(`\r`); break;
1740             case '\0': sink.put(`\0`); break;
1741             default: break;
1742             }
1743         }
1744     }
1745 
1746     if (!sink.data.length) return line;
1747 
1748     sink.put(asBytes[lastEnd..$]);
1749     return sink.data;
1750 }
1751 
1752 ///
1753 unittest
1754 {
1755     {
1756         immutable line = "abc\ndef";
1757         immutable expected = "abc\\ndef";
1758         immutable actual = escapeControlCharacters(line);
1759         assert((actual == expected), actual);
1760     }
1761     {
1762         immutable line = "\n\t\r\0";
1763         immutable expected = "\\n\\t\\r\\0";
1764         immutable actual = escapeControlCharacters(line);
1765         assert((actual == expected), actual);
1766     }
1767     {
1768         immutable line = "";
1769         immutable expected = "";
1770         immutable actual = escapeControlCharacters(line);
1771         assert((actual == expected), actual);
1772         assert(actual is line);  // No string allocated
1773     }
1774     {
1775         immutable line = "nothing to escape";
1776         immutable expected = "nothing to escape";
1777         immutable actual = escapeControlCharacters(line);
1778         assert((actual == expected), actual);
1779         assert(actual is line);  // No string allocated
1780     }
1781 }
1782 
1783 
1784 // removeControlCharacters
1785 /++
1786     Removes the control characters `'\n'`, `'\t'`, `'\r'` and `'\0'` from a string.
1787     Does not allocate a new string if there was nothing to remove.
1788 
1789     Params:
1790         line = String line to "remove" characters from.
1791 
1792     Returns:
1793         A new string with control characters removed, or the original one unchanged.
1794  +/
1795 string removeControlCharacters(/*const*/ return scope string line) pure nothrow
1796 {
1797     import std.array : Appender;
1798     import std.string : representation;
1799 
1800     if (!line.length) return line;
1801 
1802     Appender!(char[]) sink;
1803     size_t lastEnd;
1804     bool reserved;
1805 
1806     immutable asBytes = line.representation;
1807 
1808     void commitUpTo(const size_t i)
1809     {
1810         if (!reserved)
1811         {
1812             sink.reserve(asBytes.length);
1813             reserved = true;
1814         }
1815         sink.put(asBytes[lastEnd..i]);
1816     }
1817 
1818     for (size_t i; i<asBytes.length; ++i)
1819     {
1820         import std.algorithm.comparison : among;
1821 
1822         if (asBytes[i].among!('\n', '\t', '\r', '\0'))
1823         {
1824             commitUpTo(i);
1825             lastEnd = i+1;
1826         }
1827     }
1828 
1829     if (lastEnd == 0) return line;
1830 
1831     sink.put(asBytes[lastEnd..$]);
1832     return sink.data;
1833 }
1834 
1835 ///
1836 unittest
1837 {
1838     {
1839         immutable line = "abc\ndef";
1840         immutable expected = "abcdef";
1841         immutable actual = removeControlCharacters(line);
1842         assert((actual == expected), actual);
1843     }
1844     {
1845         immutable line = "\n\t\r\0";
1846         immutable expected = "";
1847         immutable actual = removeControlCharacters(line);
1848         assert((actual == expected), actual);
1849     }
1850     {
1851         immutable line = "";
1852         immutable expected = "";
1853         immutable actual = removeControlCharacters(line);
1854         assert((actual == expected), actual);
1855     }
1856     {
1857         immutable line = "nothing to escape";
1858         immutable expected = "nothing to escape";
1859         immutable actual = removeControlCharacters(line);
1860         assert((actual == expected), actual);
1861         assert(line is actual);  // No new string was allocated
1862     }
1863 }
1864 
1865 
1866 // SplitResults
1867 /++
1868     The result of a call to [splitInto].
1869  +/
1870 enum SplitResults
1871 {
1872     /++
1873         The number of arguments passed the number of separated words in the input string.
1874      +/
1875     match,
1876 
1877     /++
1878         The input string did not have enough words to match the passed arguments.
1879      +/
1880     underrun,
1881 
1882     /++
1883         The input string had too many words and could not fit into the passed arguments.
1884      +/
1885     overrun,
1886 }
1887 
1888 
1889 // splitInto
1890 /++
1891     Splits a string by a passed separator and assign the delimited words to the
1892     passed strings by ref.
1893 
1894     Note: Does *not* take quoted substrings into consideration.
1895 
1896     Params:
1897         separator = What token to separate the input string into words with.
1898         slice = Input string of words separated by `separator`.
1899         strings = Variadic list of strings to assign the split words in `slice`.
1900 
1901     Returns:
1902         A [SplitResults] with the results of the split attempt.
1903  +/
1904 auto splitInto(string separator = " ", Strings...)
1905     (auto ref string slice,
1906     scope ref Strings strings)
1907 if (Strings.length && is(Strings[0] == string) && allSameType!Strings)
1908 {
1909     if (!slice.length)
1910     {
1911         return Strings.length ? SplitResults.underrun : SplitResults.match;
1912     }
1913 
1914     foreach (immutable i, ref thisString; strings)
1915     {
1916         import std.string : indexOf;
1917 
1918         ptrdiff_t pos = slice.indexOf(separator);  // mutable
1919 
1920         if ((pos == 0) && (separator.length < slice.length))
1921         {
1922             while (slice[0..separator.length] == separator)
1923             {
1924                 slice = slice[separator.length..$];
1925             }
1926 
1927             pos = slice.indexOf(separator);
1928         }
1929 
1930         if (pos == -1)
1931         {
1932             thisString = slice;
1933             static if (__traits(isRef, slice)) slice = string.init;
1934             return (i+1 == Strings.length) ? SplitResults.match : SplitResults.underrun;
1935         }
1936 
1937         thisString = slice[0..pos];
1938         slice = slice[pos+separator.length..$];
1939     }
1940 
1941     return SplitResults.overrun;
1942 }
1943 
1944 ///
1945 unittest
1946 {
1947     import lu.conv : Enum;
1948 
1949     {
1950         string line = "abc def ghi";
1951         string abc, def, ghi;
1952         immutable results = line.splitInto(abc, def, ghi);
1953 
1954         assert((abc == "abc"), abc);
1955         assert((def == "def"), def);
1956         assert((ghi == "ghi"), ghi);
1957         assert(!line.length, line);
1958         assert((results == SplitResults.match), Enum!SplitResults.toString(results));
1959     }
1960     {
1961         string line = "abc            def                                 ghi";
1962         string abc, def, ghi;
1963         immutable results = line.splitInto(abc, def, ghi);
1964 
1965         assert((abc == "abc"), abc);
1966         assert((def == "def"), def);
1967         assert((ghi == "ghi"), ghi);
1968         assert(!line.length, line);
1969         assert((results == SplitResults.match), Enum!SplitResults.toString(results));
1970     }
1971     {
1972         string line = "abc_def ghi";
1973         string abc, def, ghi;
1974         immutable results = line.splitInto!"_"(abc, def, ghi);
1975 
1976         assert((abc == "abc"), abc);
1977         assert((def == "def ghi"), def);
1978         assert(!ghi.length, ghi);
1979         assert(!line.length, line);
1980         assert((results == SplitResults.underrun), Enum!SplitResults.toString(results));
1981     }
1982     {
1983         string line = "abc def ghi";
1984         string abc, def;
1985         immutable results = line.splitInto(abc, def);
1986 
1987         assert((abc == "abc"), abc);
1988         assert((def == "def"), def);
1989         assert((line == "ghi"), line);
1990         assert((results == SplitResults.overrun), Enum!SplitResults.toString(results));
1991     }
1992     {
1993         string line = "abc///def";
1994         string abc, def;
1995         immutable results = line.splitInto!"//"(abc, def);
1996 
1997         assert((abc == "abc"), abc);
1998         assert((def == "/def"), def);
1999         assert(!line.length, line);
2000         assert((results == SplitResults.match), Enum!SplitResults.toString(results));
2001     }
2002     {
2003         string line = "abc 123 def I am a fish";
2004         string abc, a123, def;
2005         immutable results = line.splitInto(abc, a123, def);
2006 
2007         assert((abc == "abc"), abc);
2008         assert((a123 == "123"), a123);
2009         assert((def == "def"), def);
2010         assert((line == "I am a fish"), line);
2011         assert((results == SplitResults.overrun), Enum!SplitResults.toString(results));
2012     }
2013     {
2014         string line;
2015         string abc, def;
2016         immutable results = line.splitInto(abc, def);
2017         assert((results == SplitResults.underrun), Enum!SplitResults.toString(results));
2018     }
2019 }
2020 
2021 
2022 // splitInto
2023 /++
2024     Splits a string by a passed separator and assign the delimited words to the
2025     passed strings by ref. Overload that stores overflow strings into a passed array.
2026 
2027     Note: *Does* take quoted substrings into consideration.
2028 
2029     Params:
2030         separator = What token to separate the input string into words with.
2031         slice = Input string of words separated by `separator`.
2032         strings = Variadic list of strings to assign the split words in `slice`.
2033         overflow = Overflow array.
2034 
2035     Returns:
2036         A [SplitResults] with the results of the split attempt.
2037  +/
2038 auto splitInto(string separator = " ", Strings...)
2039     (const string slice,
2040     ref Strings strings,
2041     out string[] overflow)
2042 if (!Strings.length || (is(Strings[0] == string) && allSameType!Strings))
2043 {
2044     if (!slice.length)
2045     {
2046         return Strings.length ? SplitResults.underrun : SplitResults.match;
2047     }
2048 
2049     auto chunks = splitWithQuotes!separator(slice);
2050 
2051     foreach (immutable i, ref thisString; strings)
2052     {
2053         if (chunks.length > i)
2054         {
2055             thisString = chunks[i];
2056         }
2057     }
2058 
2059     if (strings.length < chunks.length)
2060     {
2061         overflow = chunks[strings.length..$];
2062         return SplitResults.overrun;
2063     }
2064     else if (strings.length == chunks.length)
2065     {
2066         return SplitResults.match;
2067     }
2068     else /*if (strings.length > chunks.length)*/
2069     {
2070         return SplitResults.underrun;
2071     }
2072 }
2073 
2074 ///
2075 unittest
2076 {
2077     import lu.conv : Enum;
2078     import std.conv : text;
2079 
2080     {
2081         string line = "abc def ghi";
2082         string abc, def, ghi;
2083         string[] overflow;
2084         immutable results = line.splitInto(abc, def, ghi, overflow);
2085 
2086         assert((abc == "abc"), abc);
2087         assert((def == "def"), def);
2088         assert((ghi == "ghi"), ghi);
2089         assert(!overflow.length, overflow.text);
2090         assert((results == SplitResults.match), Enum!SplitResults.toString(results));
2091     }
2092     {
2093         string line = "abc##def##ghi";
2094         string abc, def, ghi;
2095         string[] overflow;
2096         immutable results = line.splitInto!"##"(abc, def, ghi, overflow);
2097 
2098         assert((abc == "abc"), abc);
2099         assert((def == "def"), def);
2100         assert((ghi == "ghi"), ghi);
2101         assert(!overflow.length, overflow.text);
2102         assert((results == SplitResults.match), Enum!SplitResults.toString(results));
2103     }
2104     {
2105         string line = "abc  def  ghi";
2106         string abc, def, ghi;
2107         string[] overflow;
2108         immutable results = line.splitInto(abc, def, ghi, overflow);
2109 
2110         assert((abc == "abc"), abc);
2111         assert((def == "def"), def);
2112         assert((ghi == "ghi"), ghi);
2113         assert(!overflow.length, overflow.text);
2114         assert((results == SplitResults.match), Enum!SplitResults.toString(results));
2115     }
2116     {
2117         string line = "abc_def ghi";
2118         string abc, def, ghi;
2119         string[] overflow;
2120         immutable results = line.splitInto!"_"(abc, def, ghi, overflow);
2121 
2122         assert((abc == "abc"), abc);
2123         assert((def == "def ghi"), def);
2124         assert(!ghi.length, ghi);
2125         assert(!overflow.length, overflow.text);
2126         assert((results == SplitResults.underrun), Enum!SplitResults.toString(results));
2127     }
2128     {
2129         string line = "abc def ghi";
2130         string abc, def;
2131         string[] overflow;
2132         immutable results = line.splitInto(abc, def, overflow);
2133 
2134         assert((abc == "abc"), abc);
2135         assert((def == "def"), def);
2136         assert((overflow == [ "ghi" ]), overflow.text);
2137         assert((results == SplitResults.overrun), Enum!SplitResults.toString(results));
2138     }
2139     {
2140         string line = "abc///def";
2141         string abc, def;
2142         string[] overflow;
2143         immutable results = line.splitInto!"//"(abc, def, overflow);
2144 
2145         assert((abc == "abc"), abc);
2146         assert((def == "/def"), def);
2147         assert(!overflow.length, overflow.text);
2148         assert((results == SplitResults.match), Enum!SplitResults.toString(results));
2149     }
2150     {
2151         string line = "abc 123 def I am a fish";
2152         string abc, a123, def;
2153         string[] overflow;
2154         immutable results = line.splitInto(abc, a123, def, overflow);
2155 
2156         assert((abc == "abc"), abc);
2157         assert((a123 == "123"), a123);
2158         assert((def == "def"), def);
2159         assert((overflow == [ "I", "am", "a", "fish" ]), overflow.text);
2160         assert((results == SplitResults.overrun), Enum!SplitResults.toString(results));
2161     }
2162     {
2163         string line = `abc 123 def "I am a fish"`;
2164         string abc, a123, def;
2165         string[] overflow;
2166         immutable results = line.splitInto(abc, a123, def, overflow);
2167 
2168         assert((abc == "abc"), abc);
2169         assert((a123 == "123"), a123);
2170         assert((def == "def"), def);
2171         assert((overflow == [ "I am a fish" ]), overflow.text);
2172         assert((results == SplitResults.overrun), Enum!SplitResults.toString(results));
2173     }
2174     {
2175         string line;
2176         string abc, def;
2177         string[] overflow;
2178         immutable results = line.splitInto(abc, def, overflow);
2179         assert((results == SplitResults.underrun), Enum!SplitResults.toString(results));
2180     }
2181     {
2182         string line = "abchonkelonkhonkelodef";
2183         string abc, def;
2184         string[] overflow;
2185         immutable results = line.splitInto!"honkelonk"(abc, def, overflow);
2186 
2187         assert((abc == "abc"), abc);
2188         assert((def == "honkelodef"), def);
2189         assert(!overflow.length, overflow.text);
2190         assert((results == SplitResults.match), Enum!SplitResults.toString(results));
2191     }
2192     {
2193         string line = "honkelonkhonkelodef";
2194         string abc, def;
2195         string[] overflow;
2196         immutable results = line.splitInto!"honkelonk"(abc, def, overflow);
2197 
2198         assert((abc == "honkelodef"), abc);
2199         assert((def == string.init), def);
2200         assert(!overflow.length, overflow.text);
2201         assert((results == SplitResults.underrun), Enum!SplitResults.toString(results));
2202     }
2203     {
2204         string line = "###########hirrsteff#snabel";
2205         string abc, def;
2206         string[] overflow;
2207         immutable results = line.splitInto!"#"(abc, def, overflow);
2208 
2209         assert((abc == "hirrsteff"), abc);
2210         assert((def == "snabel"), def);
2211         assert(!overflow.length, overflow.text);
2212         assert((results == SplitResults.match), Enum!SplitResults.toString(results));
2213     }
2214     {
2215         string line = "abc def ghi";
2216         string[] overflow;
2217         immutable results = line.splitInto(overflow);
2218         immutable expectedOverflow = [ "abc", "def", "ghi" ];
2219 
2220         assert((overflow == expectedOverflow), overflow.text);
2221         assert((results == SplitResults.overrun), Enum!SplitResults.toString(results));
2222     }
2223 }
2224 
2225 
2226 // splitWithQuotes
2227 /++
2228     Splits a string into an array of strings by whitespace, but honours quotes.
2229 
2230     Intended to be used with ASCII strings; may or may not work with more
2231     elaborate UTF-8 strings.
2232 
2233     Example:
2234     ---
2235     string s = `title "this is my title" author "john doe"`;
2236     immutable splitUp = splitWithQuotes(s);
2237     assert(splitUp == [ "title", "this is my title", "author", "john doe" ]);
2238     ---
2239 
2240     Params:
2241         separator = Separator string. May be more than one character.
2242         line = Input string.
2243 
2244     Returns:
2245         A `string[]` composed of the input string split up into substrings,
2246         delimited by whitespace. Quoted sections are treated as one substring.
2247  +/
2248 auto splitWithQuotes(string separator = " ")(const string line)
2249 {
2250     import std.array : Appender;
2251     import std.string : representation;
2252 
2253     static if (!separator.length)
2254     {
2255         enum message = "`splitWithQuotes` only works with non-empty separators";
2256         static assert(0, message);
2257     }
2258 
2259     if (!line.length) return null;
2260 
2261     Appender!(string[]) sink;
2262     sink.reserve(8);  // guesstimate
2263 
2264     size_t start;
2265     bool betweenQuotes;
2266     bool escaping;
2267     bool escapedAQuote;
2268     bool escapedABackslash;
2269 
2270     string replaceEscaped(const string line)
2271     {
2272         import std.array : replace;
2273 
2274         string slice = line;  // mutable
2275         if (escapedABackslash) slice = slice.replace(`\\`, "\1\1");
2276         if (escapedAQuote) slice = slice.replace(`\"`, `"`);
2277         if (escapedABackslash) slice = slice.replace("\1\1", `\`);
2278         return slice;
2279     }
2280 
2281     immutable asUbytes = line.representation;
2282     size_t separatorStep;
2283 
2284     for (size_t i; i < asUbytes.length; ++i)
2285     {
2286         immutable c = asUbytes[i];
2287 
2288         if (escaping)
2289         {
2290             if (c == '\\')
2291             {
2292                 escapedABackslash = true;
2293             }
2294             else if (c == '"')
2295             {
2296                 escapedAQuote = true;
2297             }
2298 
2299             escaping = false;
2300         }
2301         else if (separatorStep >= separator.length)
2302         {
2303             separatorStep = 0;
2304         }
2305         else if (!betweenQuotes && (c == separator[separatorStep]))
2306         {
2307             static if (separator.length > 1)
2308             {
2309                 if (i == 0)
2310                 {
2311                     ++separatorStep;
2312                     continue;
2313                 }
2314                 else if (++separatorStep >= separator.length)
2315                 {
2316                     // Full separator
2317                     immutable end = i-separator.length+1;
2318                     if (start != end) sink.put(line[start..end]);
2319                     start = i+1;
2320                 }
2321             }
2322             else
2323             {
2324                 // Full separator
2325                 if (start != i) sink.put(line[start..i]);
2326                 start = i+1;
2327             }
2328         }
2329         else if (c == '\\')
2330         {
2331             escaping = true;
2332         }
2333         else if (c == '"')
2334         {
2335             if (betweenQuotes)
2336             {
2337                 if (escapedAQuote || escapedABackslash)
2338                 {
2339                     sink.put(replaceEscaped(line[start+1..i]));
2340                     escapedAQuote = false;
2341                     escapedABackslash = false;
2342                 }
2343                 else if (i > start+1)
2344                 {
2345                     sink.put(line[start+1..i]);
2346                 }
2347 
2348                 betweenQuotes = false;
2349                 start = i+1;
2350             }
2351             else if (i > start+1)
2352             {
2353                 sink.put(line[start+1..i]);
2354                 betweenQuotes = true;
2355                 start = i+1;
2356             }
2357             else
2358             {
2359                 betweenQuotes = true;
2360             }
2361         }
2362     }
2363 
2364     if (line.length > start+1)
2365     {
2366         if (betweenQuotes)
2367         {
2368             if (escapedAQuote || escapedABackslash)
2369             {
2370                 sink.put(replaceEscaped(line[start+1..$]));
2371             }
2372             else
2373             {
2374                 sink.put(line[start+1..$]);
2375             }
2376         }
2377         else
2378         {
2379             sink.put(line[start..$]);
2380         }
2381     }
2382 
2383     return sink.data;
2384 }
2385 
2386 ///
2387 unittest
2388 {
2389     import std.conv : text;
2390 
2391     {
2392         enum input = `title "this is my title" author "john doe"`;
2393         immutable splitUp = splitWithQuotes(input);
2394         immutable expected =
2395         [
2396             "title",
2397             "this is my title",
2398             "author",
2399             "john doe"
2400         ];
2401         assert(splitUp == expected, splitUp.text);
2402     }
2403     {
2404         enum input = `string without quotes`;
2405         immutable splitUp = splitWithQuotes(input);
2406         immutable expected =
2407         [
2408             "string",
2409             "without",
2410             "quotes",
2411         ];
2412         assert(splitUp == expected, splitUp.text);
2413     }
2414     {
2415         enum input = string.init;
2416         immutable splitUp = splitWithQuotes(input);
2417         immutable expected = (string[]).init;
2418         assert(splitUp == expected, splitUp.text);
2419     }
2420     {
2421         enum input = `title "this is \"my\" title" author "john\\" doe`;
2422         immutable splitUp = splitWithQuotes(input);
2423         immutable expected =
2424         [
2425             "title",
2426             `this is "my" title`,
2427             "author",
2428             `john\`,
2429             "doe"
2430         ];
2431         assert(splitUp == expected, splitUp.text);
2432     }
2433     {
2434         enum input = `title "this is \"my\" title" author "john\\\" doe`;
2435         immutable splitUp = splitWithQuotes(input);
2436         immutable expected =
2437         [
2438             "title",
2439             `this is "my" title`,
2440             "author",
2441             `john\" doe`
2442         ];
2443         assert(splitUp == expected, splitUp.text);
2444     }
2445     {
2446         enum input = `this has "unbalanced quotes`;
2447         immutable splitUp = splitWithQuotes(input);
2448         immutable expected =
2449         [
2450             "this",
2451             "has",
2452             "unbalanced quotes"
2453         ];
2454         assert(splitUp == expected, splitUp.text);
2455     }
2456     {
2457         enum input = `""`;
2458         immutable splitUp = splitWithQuotes(input);
2459         immutable expected = (string[]).init;
2460         assert(splitUp == expected, splitUp.text);
2461     }
2462     {
2463         enum input = `"`;
2464         immutable splitUp = splitWithQuotes(input);
2465         immutable expected = (string[]).init;
2466         assert(splitUp == expected, splitUp.text);
2467     }
2468     {
2469         enum input = `"""""""""""`;
2470         immutable splitUp = splitWithQuotes(input);
2471         immutable expected = (string[]).init;
2472         assert(splitUp == expected, splitUp.text);
2473     }
2474 }
2475 
2476 
2477 // replaceFromAA
2478 /++
2479     Replaces space-separated tokens (that begin with a token character) in a
2480     string with values from a supplied associative array.
2481 
2482     The AA values are of some type of function or delegate returning strings.
2483 
2484     Example:
2485     ---
2486     const @safe string delegate()[string] aa =
2487     [
2488         "$foo"  : () => "bar",
2489         "$baz"  : () => "quux"
2490         "$now"  : () => Clock.currTime.toISOExtString(),
2491         "$rng"  : () => uniform(0, 100).to!string,
2492         "$hirr" : () => 10.to!string,
2493     ];
2494 
2495     immutable line = "first $foo second $baz third $hirr end";
2496     enum expected = "first bar second quux third 10 end";
2497     immutable actual = line.replaceFromAA(aa);
2498     assert((actual == expected), actual);
2499     ---
2500 
2501     Params:
2502         tokenCharacter = What character to use to denote tokens, defaults to '`$`'
2503             but may be any `char`.
2504         line = String to replace tokens in.
2505         aa = Associative array of token keys and replacement callable values.
2506 
2507     Returns:
2508         A new string with occurrences of any passed tokens replaced, or the
2509         original string as-is if there were no changes made.
2510  +/
2511 auto replaceFromAA(char tokenCharacter = '$', Fn)
2512     (const string line,
2513     const Fn[string] aa)
2514 {
2515     import std.array : Appender;
2516     import std.string : indexOf;
2517     import std.traits : isSomeFunction;
2518 
2519     static if (!isSomeFunction!Fn)
2520     {
2521         enum message = "`replaceFromAA` only works with functions and delegates";
2522         static assert(0, message);
2523     }
2524 
2525     Appender!(char[]) sink;
2526     sink.reserve(line.length + 32);  // guesstimate
2527     size_t previousEnd;
2528 
2529     for (size_t i = 0; i < line.length; ++i)
2530     {
2531         if (line[i] == tokenCharacter)
2532         {
2533             immutable spacePos = line.indexOf(' ', i);
2534             immutable end = (spacePos == -1) ? line.length : spacePos;
2535             immutable key = line[i..end];
2536 
2537             if (const replacement = key in aa)
2538             {
2539                 sink.put(line[previousEnd..i]);
2540                 sink.put((*replacement)());
2541                 i += key.length;
2542                 previousEnd = i;
2543             }
2544         }
2545     }
2546 
2547     if (previousEnd == 0) return line;
2548 
2549     sink.put(line[previousEnd..$]);
2550     return sink.data.idup;
2551 }
2552 
2553 ///
2554 unittest
2555 {
2556     static auto echo(const string what) { return what; }
2557 
2558     immutable hello = "hello";
2559 
2560     @safe string delegate()[string] aa =
2561     [
2562         "$foo" : () => hello,
2563         "$bar" : () => echo("I was one"),
2564         "$baz" : () => "BAZ",
2565     ];
2566 
2567     enum line = "I thought what I'd $foo was, I'd pretend $bar of those deaf-$baz";
2568     enum expected = "I thought what I'd hello was, I'd pretend I was one of those deaf-BAZ";
2569     immutable actual = line.replaceFromAA(aa);
2570     assert((actual == expected), actual);
2571 }