var iscntrl = func(c) c >= 1 and c <= 31 or c == 127; var isascii = func(c) c >= 0 and c <= 127; var isupper = func(c) c >= `A` and c <= `Z`; var islower = func(c) c >= `a` and c <= `z`; var isdigit = func(c) c >= `0` and c <= `9`; var isblank = func(c) c == ` ` or c == `\t`; var ispunct = func(c) c >= `!` and c <= `/` or c >= `:` and c <= `@` or c >= `[` and c <= `\`` or c >= `{` and c <= `~`; var isxdigit = func(c) isdigit(c) or c >= `a` and c <= `f` or c >= `A` and c <= `F`; var isspace = func(c) c == ` ` or c >= `\t` and c <= `\r`; var isalpha = func(c) isupper(c) or islower(c); var isalnum = func(c) isalpha(c) or isdigit(c); var isgraph = func(c) isalnum(c) or ispunct(c); var isprint = func(c) isgraph(c) or c == ` `; var toupper = func(c) islower(c) ? c + `A` - `a` : c; var tolower = func(c) isupper(c) ? c + `a` - `A` : c; var isxspace = func(c) isspace(c) or c == `\n`; ## # trim spaces at the left (lr < 0), at the right (lr > 0), or both (lr = 0) # An optional function argument defines which characters should be trimmed: # # string.trim(a); # trim spaces # string.trim(a, 1, string.isdigit); # trim digits at the right # string.trim(a, 0, func(c) c == `\\` or c == `/`); # trim slashes/backslashes # var trim = func(s, lr = 0, istrim = nil) { if (istrim == nil) istrim = isspace; var l = 0; if (lr <= 0) for (; l < size(s); l += 1) if (!istrim(s[l])) break; var r = size(s) - 1; if (lr >= 0) for (; r >= 0; r -= 1) if (!istrim(s[r])) break; return r < l ? "" : substr(s, l, r - l + 1); } ## # truncate at the first match # # string.truncateAt("file.xml", ".xml"); # "file.xml" -> "file" # string.truncateAt("file.xml", ".txt"); # "file.xml" -> "file.xml" # var truncateAt = func(src, match){ var rv = nil; call(func { if (src != nil and match !=nil) { var pos = find(match,src); if (pos>=0) src=substr(src,0,pos); } }, nil, var err = []); return src; } ## # return string converted to lower case letters # var lc = func(str) { var s = ""; for (var i = 0; i < size(str); i += 1) s ~= chr(tolower(str[i])); return s; } ## # return string converted to upper case letters # var uc = func(str) { var s = ""; for (var i = 0; i < size(str); i += 1) s ~= chr(toupper(str[i])); return s; } ## # case insensitive string compare and match functions # (not very efficient -- converting the array to be sorted # first is faster) # var icmp = func(a, b) cmp(lc(a), lc(b)); var imatch = func(a, b) match(lc(a), lc(b)); ## # check if string matches shell style pattern # # Rules: # ? stands for any single character # * stands for any number (including zero) of arbitrary characters # \ escapes the next character and makes it stand for itself; that is: # \? stands for a question mark (not the "any single character" placeholder) # [] stands for a group of characters: # [abc] stands for letters a, b or c # [^abc] stands for any character but a, b, and c (^ as first character -> inversion) # [1-4] stands for digits 1 to 4 (1, 2, 3, 4) # [1-4-] stands for digits 1 to 4, and the minus # [-1-4] same as above # [1-3-6] stands for digits 1 to 3, minus, and 6 # [1-3-6-9] stands for digits 1 to 3, minus, and 6 to 9 # [][] stands for the closing and the opening bracket (']' must be first!) # [^^] stands for all characters but the caret symbol # [\/] stands for a backslash or a slash (the backslash isn't an # escape character in a [] character group) # # Note that a minus can't be a range delimiter, as in [a--e], # which would be interpreted as any of a, e, or minus. # # Example: # string.match(name, "*[0-9].xml"); ... true if 'name' ends with digit followed by ".xml" # match = func(str, patt) { var s = 0; for (var p = 0; p < size(patt) and s < size(str); ) { if (patt[p] == `\\`) { if ((p += 1) >= size(patt)) return 0; # pattern ends with backslash } elsif (patt[p] == `?`) { s += 1; p += 1; continue; } elsif (patt[p] == `*`) { for (; p < size(patt); p += 1) if (patt[p] != `*`) break; if (p >= size(patt)) return 1; for (; s < size(str); s += 1) if (caller(0)[1](substr(str, s), substr(patt, p))) return 1; continue; } elsif (patt[p] == `[`) { setsize(var x = [], 256); var invert = 0; if ((p += 1) < size(patt) and patt[p] == `^`) { p += 1; invert = 1; } for (var i = 0; p < size(patt); p += 1) { if (patt[p] == `]` and i) break; x[patt[p]] = 1; i += 1; if (p + 2 < patt[p] and patt[p] != `-` and patt[p + 1] == `-` and patt[p + 2] != `]` and patt[p + 2] != `-`) { var from = patt[p]; var to = patt[p += 2]; for (var c = from; c <= to; c += 1) x[c] = 1; } } if (invert ? !!x[str[s]] : !x[str[s]]) return 0; s += 1; p += 1; continue; } if (str[s] != patt[p]) return 0; s += 1; p += 1; } # eat trailing * in the pattern; this is needed to fix: # https://sourceforge.net/p/flightgear/codetickets/2016/ for (;p < size(patt) and patt[p] == `*`; p += 1) {} return s == size(str) and p == size(patt); } ## # Removes superfluous slashes, empty and "." elements, # expands all ".." elements keeping relative paths, # and turns all backslashes into slashes. # The result will start with a slash if it started with a slash or backslash, # it will end without slash. # normpath = func(path) { path = replace(path, "\\", "/"); var prefix = size(path) and path[0] == `/` ? "/" : ""; var stack = []; var relative = 1; foreach (var e; split("/", path)) { if (e == "." or e == "") continue; elsif (e == ".." and !relative) pop(stack); else { append(stack, e); relative = 0; } } return size(stack) ? prefix ~ join("/", stack) : "/"; } ## # Join all elements of a list inserting a separator between every two of them. # join = func(sep, list) { if (!size(list)) return ""; var str = list[0]; foreach (var s; subvec(list, 1)) str ~= sep ~ s; return str; } ## # Replace all occurrences of 'old' by 'new'. # replace = func(str, old, new) { return join(new, split(old, str)); } ## # Get a function out of a string template for fast insertion of template # parameters. This allows to use the same templates as with most available tile # mapping engines (eg. Leaflet, Polymaps). Return a callable function object on # success, and nil if parsing the templated fails. See string._template_getargs # for more on calling a compile object. # # Example (Build MapQuest tile url): # # var makeUrl = string.compileTemplate( # "http://otile1.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.jpg" # ); # print( makeUrl({x: 5, y: 4, z: 3}) ); # # Output: # # http://otile1.mqcdn.com/tiles/1.0.0/map/3/5/4.jpg # var compileTemplate = func(template, type=nil) { var code = 'func(__ENV=nil) { string._template_getargs();'; var len = size(template); var start = 0; var end = 0; if (type == nil or type == "simple_names") { # See http://james.padolsey.com/javascript/straight-up-interpolation/ while( (start = template.find('{', end)) >= 0 ) { if( end > 0 ) code ~= '~'; code ~= '"' ~ substr(template, end, start - end) ~ '"'; if( (end = template.find('}', start)) < 0 ) { debug.warn("string.compileTemplate: unclosed brace pair (" ~ template ~ ")"); return nil; } code ~= '~__ENV["' ~ substr(template, start + 1, end - start - 1) ~ '"]'; end += 1; } if( end < len ) code ~= '~"' ~ substr(template, end, len - end) ~ '"'; } elsif (type == "nasal_expression") { var level = 0; for (var i=0; i 0 ) code ~= "~"; code ~= '"' ~ substr(template, end, start - end) ~ '"'; level = 1; var skip = 0; for (var j=i+1; j 0; j+=1) if (template[j] == `{`) level += 1; elsif (template[j] == `}`) level -= 1; elsif (template[j] == `"`) if (skip == `"`) skip = 0; elsif (skip != `'`) skip = `"`; elsif (template[j] == `'`) if (skip == `'`) skip = 0; elsif (skip != '"') skip = `'`; elsif (skip) if ((skip == `'` or skip == `"`) and template[j] == `\\`) skip = `\\`; elsif (skip == `\\` and template[j] == `\\`) skip = skip; else skip = 0; if (level) die("string.compileTemplate: unclosed brace pair (" ~ template ~ ")"); end = j; code ~= '~(' ~ substr(template, start + 1, end - start - 1)~")"; end += 1; i = end; } if( end < len ) code ~= '~"' ~ substr(template, end, len - end) ~ '"'; } code ~= "}"; var fn = compile(code)(); # get the inside function with the argument __ENV=nil var (ns,fn1) = caller(1); return bind(fn, ns, fn1); } ## # Private function used by string.naCompileTemplate. Expands any __ENV parameter # into the locals of the caller. This allows both named arguments and manual hash # arguments via __ENV. # # Examples using (format = func(__ENV) {string._template_getargs()}): # # Pass arguments as hash: # format({a: 1, "b ":2}); # Or: # format(__ENV:{a: 1, "b ":2}); # Pass arguments as named: # format(a: 1, "b ":2); # Pass arguments as both named and hash, using # __ENV to specify the latter: # format(a: 1, __ENV:{"b ": 2}); # var _template_getargs = func() { var ns = caller(1)[0]; if (contains(ns, "__ENV")) { var __ENV = ns.__ENV; if (__ENV != nil) foreach (var k; keys(__ENV)) ns[k] = __ENV[k]; } ns.__ENV = ns; } ## # Simple scanf function. Takes an input string, a pattern, and a # vector. It returns 0 if the format didn't match, and appends # all found elements to the given vector. Return values: # # -1 string matched format ending with % (i.e. more chars than format cared about) # 0 string didn't match format # 1 string matched, but would still match if the right chars were added # 2 string matched, and would not if any character would be added # # var r = string.scanf("comm3freq123.456", "comm%ufreq%f", var result = []); # # The result vector will be set to [3, 123.456]. # var Scan = { new : func(s) {{ str: s, pos: 0, parents: [Scan] }}, getc : func { if (me.pos >= size(me.str)) return nil; var c = me.str[me.pos]; me.pos += 1; return c; }, ungetc : func { me.pos -= 1 }, rest : func { substr(me.str, me.pos) }, }; var scanf = func(test, format, result) { if (find("%", format) < 0) return cmp(test, format) ? 0 : 2; var success = 0; var str = Scan.new(test); var format = Scan.new(format); while (1) { var f = format.getc(); if (f == nil) { break; } elsif (f == `%`) { success = 1; # unsafe match f = format.getc(); if (f == nil) return -1; # format ended with % if (f == `%`) { if (str.getc() != `%`) return 0; success = 2; continue; } if (isdigit(f)) { var fnum = f - `0`; while ((f = format.getc()) != nil and isdigit(f)) fnum = fnum * 10 + f - `0`; } else { var fnum = -2; # because we add one if !prefix } var scanstr = ""; var prefix = 0; var sign = 1; if (f == `d` or f == `f` or f == `u`) { var c = str.getc(); if (c == nil) { return 0; } elsif (c == `+`) { prefix = 1; } elsif (c == `-`) { if (f == `u`) return 0; (prefix, sign) = (1, -1); } else { str.ungetc(); } if (!prefix) fnum += 1; while ((var c = str.getc()) != nil and (fnum -= 1)) { if (f != `f` and c == `.`) break; elsif (num(scanstr ~ chr(c) ~ '0') != nil) # append 0 to digest e/E scanstr ~= chr(c); else break; } if (c != nil) str.ungetc(); if (num(scanstr) == nil) return 0; if (!size(scanstr) and prefix) return 0; append(result, sign * num(scanstr)); } elsif (f == `s`) { fnum += 1; while ((var c = str.getc()) != nil and c != ` ` and (fnum -= 1)) scanstr ~= chr(c); if (c != nil) str.ungetc(); if (!size(scanstr)) return 0; append(result, scanstr); } else { die("scanf: bad format element %" ~ chr(f)); } } elsif (isspace(f)) { while ((var c = str.getc()) != nil and isspace(c)) nil; if (c != nil) str.ungetc(); } elsif (f != (var c = str.getc())) { return 0; } else { success = 2; # safe match } } return str.getc() == nil and format.getc() == nil ? success : 0; } ## # ANSI colors (see $ man console_codes) # var setcolors = func(enabled) { color_enabled = (enabled and getprop("/sim/startup/stderr-to-terminal")); } var color = func(color, s, enabled=nil) { if (enabled == nil) enabled = color_enabled; return enabled ? "\x1b[" ~ color ~ "m" ~ s ~ "\x1b[m" : s; } ## # Add ANSI color codes to string, if terminal-ansi-colors are enabled and # stderr prints to a terminal. Example: # # print(string.color("31;1", "this is red")); # var color_enabled = 0; _setlistener("/sim/signals/nasal-dir-initialized", func { setlistener("/sim/startup/terminal-ansi-colors", func(n) setcolors(n.getBoolValue()), 1, 0); });