1 /++
2     Various functions that do string manipulation.
3 
4     Copyright: [JR](https://github.com/zorael)
5     License: [Boost Software License 1.0](https://www.boost.org/users/license.html)
6 
7     Authors:
8         [JR](https://github.com/zorael)
9  +/
10 module kameloso..string;
11 
12 private:
13 
14 import dialect.defs : IRCClient;
15 import std.typecons : Flag, No, Yes;
16 
17 public:
18 
19 
20 // stripSeparatedPrefix
21 /++
22     Strips a prefix word from a string, optionally also stripping away some
23     non-word characters (currently ":;?! ").
24 
25     This is to make a helper for stripping away bot prefixes, where such may be
26     "kameloso: ".
27 
28     Example:
29     ---
30     string prefixed = "kameloso: sudo MODE +o #channel :user";
31     string command = prefixed.stripSeparatedPrefix("kameloso");
32     assert((command == "sudo MODE +o #channel :user"), command);
33     ---
34 
35     Params:
36         line = String line prefixed with `prefix`, potentially including
37             separating characters.
38         prefix = Prefix to strip.
39         demandSep = Makes it a necessity that `line` is followed
40             by one of the prefix letters ": !?;". If it isn't, the `line` string
41             will be returned as is.
42 
43     Returns:
44         The passed line with the `prefix` sliced away.
45  +/
46 auto stripSeparatedPrefix(
47     const string line,
48     const string prefix,
49     const Flag!"demandSeparatingChars" demandSep = Yes.demandSeparatingChars) pure
50 in (prefix.length, "Tried to strip separated prefix but no prefix was given")
51 {
52     import lu.string : nom, strippedLeft;
53     import std.algorithm.comparison : among;
54     import std.meta : aliasSeqOf;
55 
56     enum separatingChars = ": !?;";  // In reasonable order of likelihood
57 
58     string slice = line.strippedLeft;  // mutable
59 
60     // the onus is on the caller that slice begins with prefix, else this will throw
61     slice.nom!(Yes.decode)(prefix);
62 
63     if (demandSep)
64     {
65         // Return the whole line, a non-match, if there are no separating characters
66         // (at least one of the chars in separatingChars)
67         if (!slice.length || !slice[0].among!(aliasSeqOf!separatingChars)) return line;
68     }
69 
70     while (slice.length && slice[0].among!(aliasSeqOf!separatingChars))
71     {
72         slice = slice[1..$];
73     }
74 
75     return slice.strippedLeft(separatingChars);
76 }
77 
78 ///
79 unittest
80 {
81     immutable lorem = "say: lorem ipsum".stripSeparatedPrefix("say");
82     assert((lorem == "lorem ipsum"), lorem);
83 
84     immutable notehello = "note!!!! zorael hello".stripSeparatedPrefix("note");
85     assert((notehello == "zorael hello"), notehello);
86 
87     immutable sudoquit = "sudo quit :derp".stripSeparatedPrefix("sudo");
88     assert((sudoquit == "quit :derp"), sudoquit);
89 
90     /*immutable eightball = "8ball predicate?".stripSeparatedPrefix("");
91     assert((eightball == "8ball predicate?"), eightball);*/
92 
93     immutable isnotabot = "kamelosois a bot".stripSeparatedPrefix("kameloso");
94     assert((isnotabot == "kamelosois a bot"), isnotabot);
95 
96     immutable isabot = "kamelosois a bot"
97         .stripSeparatedPrefix("kameloso", No.demandSeparatingChars);
98     assert((isabot == "is a bot"), isabot);
99 
100     immutable doubles = "kameloso            is a snek"
101         .stripSeparatedPrefix("kameloso");
102     assert((doubles == "is a snek"), doubles);
103 }
104 
105 
106 // replaceTokens
107 /++
108     Apply some common text replacements. Used on part and quit reasons.
109 
110     Params:
111         line = String to replace tokens in.
112         client = The current [dialect.defs.IRCClient|IRCClient].
113 
114     Returns:
115         A modified string with token occurrences replaced.
116  +/
117 auto replaceTokens(const string line, const IRCClient client) @safe pure nothrow
118 {
119     import kameloso.constants : KamelosoInfo;
120     import std.array : replace;
121 
122     return line
123         .replaceTokens
124         .replace("$nickname", client.nickname);
125 }
126 
127 ///
128 unittest
129 {
130     import kameloso.constants : KamelosoInfo;
131     import std.format : format;
132 
133     IRCClient client;
134     client.nickname = "harbl";
135 
136     {
137         immutable line = "asdf $nickname is kameloso version $version from $source";
138         immutable expected = "asdf %s is kameloso version %s from %s"
139             .format(client.nickname, cast(string)KamelosoInfo.version_,
140                 cast(string)KamelosoInfo.source);
141         immutable actual = line.replaceTokens(client);
142         assert((actual == expected), actual);
143     }
144     {
145         immutable line = "";
146         immutable expected = "";
147         immutable actual = line.replaceTokens(client);
148         assert((actual == expected), actual);
149     }
150     {
151         immutable line = "blerp";
152         immutable expected = "blerp";
153         immutable actual = line.replaceTokens(client);
154         assert((actual == expected), actual);
155     }
156 }
157 
158 
159 // replaceTokens
160 /++
161     Apply some common text replacements. Used on part and quit reasons.
162     Overload that doesn't take an [dialect.defs.IRCClient|IRCClient] and as such can't
163     replace `$nickname`.
164 
165     Params:
166         line = String to replace tokens in.
167 
168     Returns:
169         A modified string with token occurrences replaced.
170  +/
171 auto replaceTokens(const string line) @safe pure nothrow
172 {
173     import kameloso.constants : KamelosoInfo;
174     import std.array : replace;
175 
176     return line
177         .replace("$version", cast(string)KamelosoInfo.version_)
178         .replace("$source", cast(string)KamelosoInfo.source);
179 }
180 
181 
182 // replaceRandom
183 /++
184     Replaces `$random` and `$random(i..n)` tokens in a string with corresponding
185     random values.
186 
187     If given only `$random`, a value between the passed `defaultLowerBound` inclusive
188     to `defaultUpperBound` exclusive is substituted, whereas if a range of
189     `$random(i..n)` is given, a value between `i` inclusive and `n` exclusive is
190     substituted.
191 
192     On syntax errors, or if `n` is not greater than `i`, the original line is
193     silently returned.
194 
195     Params:
196         line = String to replace tokens in.
197         defaultLowerBound = Default lower bound when no range given.
198         defaultUpperBound = Default upper bound when no range given.
199 
200     Returns:
201         A new string with occurences of `$random` and `$random(i..n)` replaced,
202         or the original string if there were no changes made.
203  +/
204 auto replaceRandom(
205     const string line,
206     const long defaultLowerBound = 0,
207     const long defaultUpperBound = 100) @safe
208 {
209     import std.conv : text;
210     import std.random : uniform;
211     import std.string : indexOf;
212 
213     immutable randomPos = line.indexOf("$random");
214 
215     if (randomPos == -1)
216     {
217         // No $random token
218         return line;
219     }
220 
221     if (line.length > randomPos)
222     {
223         immutable openParen = randomPos + "$random".length;
224 
225         if (line.length == openParen)
226         {
227             immutable randomNumber = uniform(defaultLowerBound, defaultUpperBound);
228             return text(line[0..randomPos], randomNumber);
229         }
230         else if (line[openParen] == '(')
231         {
232             immutable dots = line.indexOf("..", openParen);
233 
234             if (dots != -1)
235             {
236                 immutable endParen = line.indexOf(')', dots);
237 
238                 if (endParen != -1)
239                 {
240                     try
241                     {
242                         import std.conv : to;
243 
244                         immutable lowerBound = line[openParen+1..dots].to!long;
245                         immutable upperBound = line[dots+2..endParen].to!long;
246                         immutable randomNumber = uniform(lowerBound, upperBound);
247                         return text(line[0..randomPos], randomNumber, line[endParen+1..$]);
248                     }
249                     catch (Exception _)
250                     {
251                         return line;
252                     }
253                 }
254             }
255         }
256         else if (line[openParen] == ' ')
257         {
258             immutable randomNumber = uniform(defaultLowerBound, defaultUpperBound);
259             return text(line[0..randomPos], randomNumber, line[openParen..$]);
260         }
261     }
262 
263     return line;
264 }
265 
266 ///
267 unittest
268 {
269     import lu.string : nom;
270     import std.conv : to;
271 
272     {
273         enum line = "$random bottles of beer on the wall";
274         string replaced = line.replaceRandom();  // mutable
275         immutable number = replaced.nom(' ').to!int;
276         assert(((number >= 0) && (number < 100)), number.to!string);
277     }
278     {
279         enum line = "$random(100..200) bottles of beer on the wall";
280         string replaced = line.replaceRandom();  // mutable
281         immutable number = replaced.nom(' ').to!int;
282         assert(((number >= 100) && (number < 200)), number.to!string);
283     }
284     {
285         enum line = "$random(-20..-10) bottles of beer on the wall";
286         string replaced = line.replaceRandom();  // mutable
287         immutable number = replaced.nom(' ').to!int;
288         assert(((number >= -20) && (number < -10)), number.to!string);
289     }
290     /*{
291         static if (__VERSION__ > 2089L)
292         {
293             // Fails pre-2.090 with Error: signed integer overflow
294             enum line = "$random(-9223372036854775808..9223372036854775807) bottles of beer on the wall";
295             string replaced = line.replaceRandom();  // mutable
296             immutable number = replaced.nom(' ').to!long;
297             //assert(((number >= cast(long)-9223372036854775808) && (number < 9223372036854775807)), number.to!string);
298         }
299     }*/
300     {
301         // syntax error, no bounds given
302         enum line = "$random() bottles of beer on the wall";
303         immutable replaced = line.replaceRandom();
304         assert((replaced == line), replaced);
305     }
306     {
307         // syntax error, no closing paren
308         enum line = "$random( bottles of beer on the wall";
309         immutable replaced = line.replaceRandom();
310         assert((replaced == line), replaced);
311     }
312     {
313         // syntax error, no upper bound given
314         enum line = "$random(0..) bottles of beer on the wall";
315         immutable replaced = line.replaceRandom();
316         assert((replaced == line), replaced);
317     }
318     {
319         // syntax error, no boudns given
320         enum line = "$random(..) bottles of beer on the wall";
321         immutable replaced = line.replaceRandom();
322         assert((replaced == line), replaced);
323     }
324     {
325         // syntax error, missing closing paren
326         enum line = "$random(0..100 bottles of beer on the wall";
327         immutable replaced = line.replaceRandom();
328         assert((replaced == line), replaced);
329     }
330     {
331         // syntax error, parens include text
332         enum line = "$random(0..100 bottles of beer on the wall)";
333         immutable replaced = line.replaceRandom();
334         assert((replaced == line), replaced);
335     }
336     {
337         // syntax error, i == n
338         enum line = "$random(0..0) bottles of beer on the wall";
339         immutable replaced = line.replaceRandom();
340         assert((replaced == line), replaced);
341     }
342     {
343         // syntax error, i > n
344         enum line = "$random(2..1) bottles of beer on the wall";
345         immutable replaced = line.replaceRandom();
346         assert((replaced == line), replaced);
347     }
348     {
349         // empty string
350         enum line = string.init;
351         string replaced = line.replaceRandom();
352         assert(!replaced.length, replaced);
353     }
354     {
355         // no $random token
356         enum line = "99 bottles of beer on the wall";
357         immutable replaced = line.replaceRandom();
358         assert((replaced == line), replaced);
359     }
360 }
361 
362 
363 // doublyBackslashed
364 /++
365     Returns the supplied string with any backslashes doubled. This is to make
366     paths on Windows display properly.
367 
368     Merely returns the given string on Posix and other non-Windows platforms.
369 
370     Example:
371     ---
372     string path = r"c:\Windows\system32";
373     assert(path.doublyBackslashed == r"c:\\Windows\\system32");
374     ---
375 
376     Params:
377         path = The original path string with only single backslashes.
378 
379     Returns:
380         The passed `path` but doubly backslashed.
381  +/
382 auto doublyBackslashed(const string path)
383 {
384     if (!path.length) return path;
385 
386     version(Windows)
387     {
388         import std.array : replace;
389         import std.string : indexOf;
390 
391         string slice = path.replace('\\', r"\\");
392 
393         while (slice.indexOf(r"\\\\") != -1)
394         {
395             slice = slice.replace(r"\\\\", r"\\");
396         }
397 
398         return slice;
399     }
400     else
401     {
402         return path;
403     }
404 }
405 
406 ///
407 version(Windows)
408 unittest
409 {
410     {
411         enum path = r"c:\windows\system32";
412         enum expected = r"c:\\windows\\system32";
413         immutable actual = path.doublyBackslashed;
414         assert((actual == expected), actual);
415     }
416     {
417         enum path = r"c:\Users\blerp\AppData\Local\kameloso\server\irc.chat.twitch.tv";
418         enum expected = r"c:\\Users\\blerp\\AppData\\Local\\kameloso\\server\\irc.chat.twitch.tv";
419         immutable actual = path.doublyBackslashed;
420         assert((actual == expected), actual);
421     }
422     {
423         enum path = r"c:\\windows\\system32";
424         enum expected = r"c:\\windows\\system32";
425         immutable actual = path.doublyBackslashed;
426         assert((actual == expected), actual);
427     }
428 }