(141 lines)
1 -- Expand the capabilities of the built-in strings2 {:List, :Dict} = require "containers"3 {:reverse, :upper, :lower, :find, :byte, :match, :gmatch, :gsub, :sub, :format, :rep, :char} = string5 isplit = (sep='%s+')=>6 step = (i)=>7 start = @pos8 return unless start9 i += 110 nl = find(@str, @sep, start)11 @pos = nl and (nl+1) or nil12 line = sub(@str, start, nl and (nl-1) or #@str)13 return i, line, start, (nl and (nl-1) or #@str)14 return step, {str:@, pos:1, :sep}, 016 lua_keywords = {17 ["and"]:true, ["break"]:true, ["do"]:true, ["else"]:true, ["elseif"]:true, ["end"]:true,18 ["false"]:true, ["for"]:true, ["function"]:true, ["goto"]:true, ["if"]:true,19 ["in"]:true, ["local"]:true, ["nil"]:true, ["not"]:true, ["or"]:true, ["repeat"]:true,20 ["return"]:true, ["then"]:true, ["true"]:true, ["until"]:true, ["while"]:true21 }22 is_lua_id = (str)->23 match(str, "^[_a-zA-Z][_a-zA-Z0-9]*$") and not lua_keywords[str]25 -- Convert an arbitrary text into a valid Lua identifier. This function is injective,26 -- but not idempotent. In logic terms: (x != y) => (as_lua_id(x) != as_lua_id(y)),27 -- but not (as_lua_id(a) == b) => (as_lua_id(b) == b).28 as_lua_id = (str)->29 -- Escape 'x' (\x78) when it precedes something that looks like an uppercase hex sequence.30 -- This way, all Lua IDs can be unambiguously reverse-engineered, but normal usage31 -- of 'x' won't produce ugly Lua IDs.32 -- i.e. "x" -> "x", "oxen" -> "oxen", but "Hex2Dec" -> "Hex782Dec" and "He-ec" -> "Hex2Dec"33 str = gsub str, "x([0-9A-F][0-9A-F])", "x78%1"34 -- Map spaces to underscores, and everything else non-alphanumeric to hex escape sequences35 str = gsub str, "%W", (c)->36 if c == ' ' then '_'37 else format("x%02X", byte(c))39 unless is_lua_id(match(str, "^_*(.*)$"))40 str = "_"..str41 return str43 -- from_lua_id(as_lua_id(str)) == str, but behavior is unspecified for inputs that44 -- did not come from as_lua_id()45 from_lua_id = (str)->46 unless is_lua_id(match(str, "^_*(.*)$"))47 str = sub(str,2,-1)48 str = gsub(str, "_", " ")49 str = gsub(str, "x([0-9A-F][0-9A-F])", (hex)-> char(tonumber(hex, 16)))50 return str52 Text = {53 :isplit, uppercase:upper, lowercase:lower, reversed:reverse, :is_lua_id54 capitalized: => (gsub(@, '%l', upper, 1))55 byte: byte, as_a_number: tonumber, as_a_base_1_number: tonumber,56 bytes: (i, j)=> List{byte(@, i or 1, j or -1)}57 split: (sep)=> List[chunk for i,chunk in isplit(@, sep)]58 starts_with: (s)=> sub(@, 1, #s) == s59 ends_with: (s)=> #@ >= #s and sub(@, #@-#s, -1) == s60 lines: => List[line for i,line in isplit(@, '\n')]61 line: (line_num)=>62 for i, line, start in isplit(@, '\n')63 return line if i == line_num65 line_info_at: (pos)=>66 assert(type(pos) == 'number', "Invalid string position")67 for i, line, start, stop in isplit(@, '\n')68 if stop+1 >= pos69 return line, i, (pos-start+1)71 indented: (indent=" ")=>72 indent..(gsub(@, "\n", "\n"..indent))74 as_lua: =>75 escaped = gsub(@, "\\", "\\\\")76 escaped = gsub(escaped, "\n", "\\n")77 escaped = gsub(escaped, '"', '\\"')78 escaped = gsub(escaped, "[^ %g]", (c)-> format("\\%03d", byte(c, 1)))79 return '"'..escaped..'"'81 as_nomsu: => @as_lua!83 formatted_with:format, byte:byte,84 position_of:((...)->(find(...))), position_of_1_after:((...)->(find(...))),85 as_a_lua_identifier: as_lua_id, is_a_lua_identifier: is_lua_id,86 as_lua_id: as_lua_id, as_a_lua_id: as_lua_id, is_a_lua_id: is_lua_id,87 is_lua_id: is_lua_id, from_lua_id: from_lua_id,88 bytes_1_to: (start, stop)=> List{byte(@, start, stop)}89 [as_lua_id "with 1 ->"]: (...)-> (gsub(...))90 bytes: => List{byte(@, 1, -1)},91 wrapped_to: (maxlen=80, margin=8)=>92 lines = {}93 for line in *@lines!94 while #line > maxlen95 chunk = sub(line, 1, maxlen)96 split = find(chunk, ' ', maxlen-margin, true) or maxlen97 chunk = sub(line, 1, split)98 line = sub(line, split+1, -1)99 lines[#lines+1] = chunk100 lines[#lines+1] = line101 return table.concat(lines, "\n")103 line_at: (i)=> (@line_info_at(i))104 line_number_at: (i)=> select(2, @line_info_at(i))105 line_position_at: (i)=> select(3, @line_info_at(i))106 matches: (patt)=> match(@, patt) and true or false107 matching: (patt)=> (match(@, patt))108 matching_groups: (patt)=> List{match(@, patt)}109 [as_lua_id "* 1"]: (n)=> rep(@, n)110 all_matches_of: (patt)=>111 result = {}112 stepper,x,i = gmatch(@, patt)113 while true114 tmp = List{stepper(x,i)}115 break if #tmp == 0116 i = tmp[1]117 result[#result+1] = (#tmp == 1) and tmp[1] or tmp118 return List(result)119 from_1_to: sub, from: sub,120 character: (i)=> sub(@, i, i)121 }122 for k,v in pairs(string) do Text[k] or= v124 _1_as_text = (x)->125 if x == true then return "yes"126 if x == false then return "no"127 return tostring(x)129 setmetatable Text,130 __call: (...)=>131 ret = {...}132 for i=1,select("#", ...)133 ret[i] = _1_as_text(ret[i])134 return table.concat(ret)136 debug.setmetatable "",137 __type: "Text"138 __index: (k)=> Text[k] or (type(k) == 'number' and sub(@, k, k) or nil)139 __add: (x)=> _1_as_text(@).._1_as_text(x)141 return Text