Phi: nicer scroll animation for METAR widget
[fg:fgdata.git] / Nasal / string.nas
1 var iscntrl = func(c) c >= 1 and c <= 31 or c == 127;
2 var isascii = func(c) c >= 0 and c <= 127;
3 var isupper = func(c) c >= `A` and c <= `Z`;
4 var islower = func(c) c >= `a` and c <= `z`;
5 var isdigit = func(c) c >= `0` and c <= `9`;
6 var isblank = func(c) c == ` ` or c == `\t`;
7 var ispunct = func(c) c >= `!` and c <= `/` or c >= `:` and c <= `@`
8                 or c >= `[` and c <= `\`` or c >= `{` and c <= `~`;
9
10 var isxdigit = func(c) isdigit(c) or c >= `a` and c <= `f` or c >= `A` and c <= `F`;
11 var isspace = func(c) c == ` ` or c >= `\t` and c <= `\r`;
12 var isalpha = func(c) isupper(c) or islower(c);
13 var isalnum = func(c) isalpha(c) or isdigit(c);
14 var isgraph = func(c) isalnum(c) or ispunct(c);
15 var isprint = func(c) isgraph(c) or c == ` `;
16
17 var toupper = func(c) islower(c) ? c + `A` - `a` : c;
18 var tolower = func(c) isupper(c) ? c + `a` - `A` : c;
19
20 var isxspace = func(c) isspace(c) or c == `\n`;
21
22
23 ##
24 # trim spaces at the left (lr < 0), at the right (lr > 0), or both (lr = 0)
25 # An optional function argument defines which characters should be trimmed:
26 #
27 #  string.trim(a);                                    # trim spaces
28 #  string.trim(a, 1, string.isdigit);                 # trim digits at the right
29 #  string.trim(a, 0, func(c) c == `\\` or c == `/`);  # trim slashes/backslashes
30 #
31 var trim = func(s, lr = 0, istrim = nil) {
32         if (istrim == nil)
33                 istrim = isspace;
34         var l = 0;
35         if (lr <= 0)
36                 for (; l < size(s); l += 1)
37                         if (!istrim(s[l]))
38                                 break;
39         var r = size(s) - 1;
40         if (lr >= 0)
41                 for (; r >= 0; r -= 1)
42                         if (!istrim(s[r]))
43                                 break;
44         return r < l ? "" : substr(s, l, r - l + 1);
45 }
46
47
48 ##
49 # return string converted to lower case letters
50 #
51 var lc = func(str) {
52         var s = "";
53         for (var i = 0; i < size(str); i += 1)
54                 s ~= chr(tolower(str[i]));
55         return s;
56 }
57
58
59 ##
60 # return string converted to upper case letters
61 #
62 var uc = func(str) {
63         var s = "";
64         for (var i = 0; i < size(str); i += 1)
65                 s ~= chr(toupper(str[i]));
66         return s;
67 }
68
69
70 ##
71 # case insensitive string compare and match functions
72 # (not very efficient -- converting the array to be sorted
73 # first is faster)
74 #
75 var icmp = func(a, b) cmp(lc(a), lc(b));
76 var imatch = func(a, b) match(lc(a), lc(b));
77
78
79
80
81 ##
82 # Functions that are used in the IO security code (io.nas) are defined in a
83 # closure that holds safe copies of system functions. Later manipulation of
84 # append(), pop() etc. doesn't affect them. Of course, any security code
85 # must itself store safe copies of these tamper-proof functions before user
86 # code can redefine them, and the closure() command must be made inaccessible.
87 ##
88
89 var match = nil;
90 var normpath = nil;
91 var join = nil;
92 var replace = nil;
93
94 (func {
95         var append = append;
96         var caller = caller;
97         var pop = pop;
98         var setsize = setsize;
99         var size = size;
100         var split = split;
101         var substr = substr;
102         var subvec = subvec;
103
104
105 ##
106 # check if string <str> matches shell style pattern <patt>
107 #
108 # Rules:
109 # ?   stands for any single character
110 # *   stands for any number (including zero) of arbitrary characters
111 # \   escapes the next character and makes it stand for itself; that is:
112 #     \? stands for a question mark (not the "any single character" placeholder)
113 # []  stands for a group of characters:
114 #     [abc]      stands for letters a, b or c
115 #     [^abc]     stands for any character but a, b, and c  (^ as first character -> inversion)
116 #     [1-4]      stands for digits 1 to 4 (1, 2, 3, 4)
117 #     [1-4-]     stands for digits 1 to 4, and the minus
118 #     [-1-4]     same as above
119 #     [1-3-6]    stands for digits 1 to 3, minus, and 6
120 #     [1-3-6-9]  stands for digits 1 to 3, minus, and 6 to 9
121 #     [][]       stands for the closing and the opening bracket (']' must be first!)
122 #     [^^]       stands for all characters but the caret symbol
123 #     [\/]       stands for a backslash or a slash  (the backslash isn't an
124 #                escape character in a [] character group)
125 #
126 #     Note that a minus can't be a range delimiter, as in [a--e],
127 #     which would be interpreted as any of a, e, or minus.
128 #
129 # Example:
130 #     string.match(name, "*[0-9].xml"); ... true if 'name' ends with digit followed by ".xml"
131 #
132 match = func(str, patt) {
133         var s = 0;
134         for (var p = 0; p < size(patt) and s < size(str); ) {
135                 if (patt[p] == `\\`) {
136                         if ((p += 1) >= size(patt))
137                                 return 0;  # pattern ends with backslash
138
139                 } elsif (patt[p] == `?`) {
140                         s += 1;
141                         p += 1;
142                         continue;
143
144                 } elsif (patt[p] == `*`) {
145                         for (; p < size(patt); p += 1)
146                                 if (patt[p] != `*`)
147                                         break;
148                         if (p >= size(patt))
149                                 return 1;
150
151                         for (; s < size(str); s += 1)
152                                 if (caller(0)[1](substr(str, s), substr(patt, p)))
153                                         return 1;
154                         continue;
155
156                 } elsif (patt[p] == `[`) {
157                         setsize(var x = [], 256);
158                         var invert = 0;
159                         if ((p += 1) < size(patt) and patt[p] == `^`) {
160                                 p += 1;
161                                 invert = 1;
162                         }
163                         for (var i = 0; p < size(patt); p += 1) {
164                                 if (patt[p] == `]` and i)
165                                         break;
166                                 x[patt[p]] = 1;
167                                 i += 1;
168
169                                 if (p + 2 < patt[p] and patt[p] != `-` and patt[p + 1] == `-`
170                                                 and patt[p + 2] != `]` and patt[p + 2] != `-`) {
171                                         var from = patt[p];
172                                         var to = patt[p += 2];
173                                         for (var c = from; c <= to; c += 1)
174                                                 x[c] = 1;
175                                 }
176                         }
177                         if (invert ? !!x[str[s]] : !x[str[s]])
178                                 return 0;
179                         s += 1;
180                         p += 1;
181                         continue;
182                 }
183
184                 if (str[s] != patt[p])
185                         return 0;
186                 s += 1;
187                 p += 1;
188         }
189         return s == size(str) and p == size(patt);
190 }
191
192
193 ##
194 # Removes superfluous slashes, empty and "." elements,
195 # expands all ".." elements keeping relative paths,
196 # and turns all backslashes into slashes.
197 # The result will start with a slash if it started with a slash or backslash,
198 # it will end without slash.
199 #
200 normpath = func(path) {
201         path = replace(path, "\\", "/");
202         var prefix = size(path) and path[0] == `/` ? "/" : "";
203
204         var stack = [];
205         var relative = 1;
206
207         foreach (var e; split("/", path)) {
208                 if (e == "." or e == "")
209                         continue;
210                 elsif (e == ".." and !relative)
211                         pop(stack);
212                 else {
213                         append(stack, e);
214                         relative = 0;
215                 }
216         }
217         return size(stack) ? prefix ~ join("/", stack) : "/";
218 }
219
220
221 ##
222 # Join all elements of a list inserting a separator between every two of them.
223 #
224 join = func(sep, list) {
225         if (!size(list))
226                 return "";
227         var str = list[0];
228         foreach (var s; subvec(list, 1))
229                 str ~= sep ~ s;
230         return str;
231 }
232
233
234 ##
235 # Replace all occurrences of 'old' by 'new'.
236 #
237 replace = func(str, old, new) {
238         return join(new, split(old, str));
239 }
240
241 })(); # end tamper-proof environment
242
243
244 ##
245 # Get a function out of a string template for fast insertion of template
246 # parameters. This allows to use the same templates as with most available tile
247 # mapping engines (eg. Leaflet, Polymaps). Return a callable function object on
248 # success, and nil if parsing the templated fails. See string._template_getargs
249 # for more on calling a compile object.
250 #
251 # Example (Build MapQuest tile url):
252 #
253 #    var makeUrl = string.compileTemplate(
254 #      "http://otile1.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.jpg"
255 #    );
256 #    print( makeUrl({x: 5, y: 4, z: 3}) );
257 #
258 # Output:
259 #
260 #    http://otile1.mqcdn.com/tiles/1.0.0/map/3/5/4.jpg
261 #
262 var compileTemplate = func(template, type=nil) {
263         var code = 'func(__ENV=nil) { string._template_getargs();';
264         var len = size(template);
265         var start = 0;
266         var end = 0;
267         if (type == nil or type == "simple_names") {
268                 # See http://james.padolsey.com/javascript/straight-up-interpolation/
269                 while( (start = template.find('{', end)) >= 0 )
270                 {
271                         if( end > 0 )
272                                 code ~= '~';
273                         code ~= '"' ~ substr(template, end, start - end) ~ '"';
274                         if( (end = template.find('}', start)) < 0 )
275                         {
276                                 debug.warn("string.compileTemplate: unclosed brace pair (" ~ template ~ ")");
277                                 return nil;
278                         }
279
280                         code ~= '~__ENV["' ~ substr(template, start + 1, end - start - 1) ~ '"]';
281                         end += 1;
282                 }
283                 if( end < len )
284                         code ~= '~"' ~ substr(template, end, len - end) ~ '"';
285         } elsif (type == "nasal_expression") {
286                 var level = 0;
287                 for (var i=0; i<len; i+=1) {
288                         if (template[i] != `{`) continue;
289                         start = i;
290                         if ( end > 0 )
291                                 code ~= "~";
292                         code ~= '"' ~ substr(template, end, start - end) ~ '"';
293                         level = 1; var skip = 0;
294                         for (var j=i+1; j<len and level > 0; j+=1)
295                                 if (template[j] == `{`) level += 1;
296                                 elsif (template[j] == `}`) level -= 1;
297                                 elsif (template[j] == `"`)
298                                         if    (skip == `"`) skip = 0;
299                                         elsif (skip != `'`) skip = `"`;
300                                 elsif (template[j] == `'`)
301                                         if    (skip == `'`) skip = 0;
302                                         elsif (skip != '"') skip = `'`;
303                                 elsif (skip)
304                                         if ((skip == `'` or skip == `"`) and template[j] == `\\`)
305                                                 skip = `\\`;
306                                         elsif (skip == `\\` and template[j] == `\\`)
307                                                 skip = skip;
308                                         else
309                                                 skip = 0;
310                         if (level)
311                                 die("string.compileTemplate: unclosed brace pair (" ~ template ~ ")");
312                         end = j;
313                         code ~= '~(' ~ substr(template, start + 1, end - start - 1)~")";
314                         end += 1;
315                         i = end;
316                 }
317                 if( end < len )
318                         code ~= '~"' ~ substr(template, end, len - end) ~ '"';
319         }
320         code ~= "}";
321         var fn = compile(code)(); # get the inside function with the argument __ENV=nil
322         var (ns,fn1) = caller(1);
323         return bind(fn, ns, fn1);
324 }
325
326 ##
327 # Private function used by string.naCompileTemplate. Expands any __ENV parameter
328 # into the locals of the caller. This allows both named arguments and manual hash
329 # arguments via __ENV.
330 #
331 # Examples using (format = func(__ENV) {string._template_getargs()}):
332 #
333 # Pass arguments as hash:
334 #   format({a: 1, "b ":2});
335 # Or:
336 #   format(__ENV:{a: 1, "b ":2});
337 # Pass arguments as named:
338 #   format(a: 1, "b ":2);
339 # Pass arguments as both named and hash, using
340 # __ENV to specify the latter:
341 #   format(a: 1, __ENV:{"b ": 2});
342 #
343 var _template_getargs = func() {
344         var ns = caller(1)[0];
345         if (contains(ns, "__ENV")) {
346                 var __ENV = ns.__ENV;
347                 if (__ENV != nil)
348                         foreach (var k; keys(__ENV))
349                                 ns[k] = __ENV[k];
350         }
351         ns.__ENV = ns;
352 }
353
354
355 ##
356 # Simple scanf function. Takes an input string, a pattern, and a
357 # vector. It returns 0 if the format didn't match, and appends
358 # all found elements to the given vector. Return values:
359 #
360 # -1 string matched format ending with % (i.e. more chars than format cared about)
361 #  0 string didn't match format
362 #  1 string matched, but would still match if the right chars were added
363 #  2 string matched, and would not if any character would be added
364 #
365 #   var r = string.scanf("comm3freq123.456", "comm%ufreq%f", var result = []);
366 #
367 # The result vector will be set to [3, 123.456].
368 #
369 var Scan = {
370         new : func(s) {{ str: s, pos: 0, parents: [Scan] }},
371         getc : func {
372                 if (me.pos >= size(me.str))
373                         return nil;
374                 var c = me.str[me.pos];
375                 me.pos += 1;
376                 return c;
377         },
378         ungetc : func { me.pos -= 1 },
379         rest : func { substr(me.str, me.pos) },
380 };
381
382
383 var scanf = func(test, format, result) {
384         if (find("%", format) < 0)
385                 return cmp(test, format) ? 0 : 2;
386
387         var success = 0;
388         var str = Scan.new(test);
389         var format = Scan.new(format);
390
391         while (1) {
392                 var f = format.getc();
393                 if (f == nil) {
394                         break;
395
396                 } elsif (f == `%`) {
397                         success = 1;            # unsafe match
398                         f = format.getc();
399                         if (f == nil)
400                                 return -1;      # format ended with %
401                         if (f == `%`) {
402                                 if (str.getc() != `%`)
403                                         return 0;
404                                 success = 2;
405                                 continue;
406                         }
407
408                         if (isdigit(f)) {
409                                 var fnum = f - `0`;
410                                 while ((f = format.getc()) != nil and isdigit(f))
411                                         fnum = fnum * 10 + f - `0`;
412                         } else {
413                                 var fnum = -2; # because we add one if !prefix
414                         }
415
416                         var scanstr = "";
417                         var prefix = 0;
418                         var sign = 1;
419                         if (f == `d` or f == `f` or f == `u`) {
420                                 var c = str.getc();
421                                 if (c == nil) {
422                                         return 0;
423                                 } elsif (c == `+`) {
424                                         prefix = 1;
425                                 } elsif (c == `-`) {
426                                         if (f == `u`)
427                                                 return 0;
428                                         (prefix, sign) = (1, -1);
429                                 } else {
430                                         str.ungetc();
431                                 }
432                                 if (!prefix)
433                                         fnum += 1;
434
435                                 while ((var c = str.getc()) != nil and (fnum -= 1)) {
436                                         if (f != `f` and c == `.`)
437                                                 break;
438                                         elsif (num(scanstr ~ chr(c) ~ '0') != nil) # append 0 to digest e/E
439                                                 scanstr ~= chr(c);
440                                         else
441                                                 break;
442                                 }
443                                 if (c != nil)
444                                         str.ungetc();
445                                 if (num(scanstr) == nil)
446                                         return 0;
447                                 if (!size(scanstr) and prefix)
448                                         return 0;
449                                 append(result, sign * num(scanstr));
450
451                         } elsif (f == `s`) {
452                                 fnum += 1;
453                                 while ((var c = str.getc()) != nil and c != ` ` and (fnum -= 1))
454                                         scanstr ~= chr(c);
455
456                                 if (c != nil)
457                                         str.ungetc();
458                                 if (!size(scanstr))
459                                         return 0;
460
461                                 append(result, scanstr);
462
463                         } else {
464                                 die("scanf: bad format element %" ~ chr(f));
465                         }
466
467
468                 } elsif (isspace(f)) {
469                         while ((var c = str.getc()) != nil and isspace(c))
470                                 nil;
471                         if (c != nil)
472                                 str.ungetc();
473
474                 } elsif (f != (var c = str.getc())) {
475                         return 0;
476
477                 } else {
478                         success = 2;            # safe match
479                 }
480         }
481         return str.getc() == nil and format.getc() == nil ? success : 0;
482 }
483
484
485 ##
486 # ANSI colors  (see $ man console_codes)
487 #
488 var setcolors = func(enabled) {
489         color_enabled = (enabled and getprop("/sim/startup/stderr-to-terminal"));
490 }
491 var color = func(color, s, enabled=nil) {
492         if (enabled == nil) enabled = color_enabled;
493         return enabled ? "\x1b[" ~ color ~ "m" ~ s ~ "\x1b[m" : s;
494 }
495
496
497 ##
498 # Add ANSI color codes to string, if terminal-ansi-colors are enabled and
499 # stderr prints to a terminal. Example:
500 #
501 #   print(string.color("31;1", "this is red"));
502 #
503 var color_enabled = 0;
504 _setlistener("/sim/signals/nasal-dir-initialized", func {
505         setlistener("/sim/startup/terminal-ansi-colors", func(n) setcolors(n.getBoolValue()), 1, 0);
506 });
507
508