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