| 1 | local llex = require "llex" |
| 2 | |
| 3 | local base_char = 128; |
| 4 | local keywords = { "and", "break", "do", "else", "elseif", |
| 5 | "end", "false", "for", "function", "if", |
| 6 | "in", "local", "nil", "not", "or", "repeat", |
| 7 | "return", "then", "true", "until", "while" } |
| 8 | |
| 9 | function uglify_file(infile_fn, outfile_fn) |
| 10 | local infile, err = io.open(infile_fn); |
| 11 | if not infile then |
| 12 | print_err("Can't open input file for reading: "..tostring(err)); |
| 13 | return; |
| 14 | end |
| 15 | |
| 16 | local outfile, err = io.open(outfile_fn..".uglified", "w+b"); |
| 17 | if not outfile then |
| 18 | print_err("Can't open output file for writing: "..tostring(err)); |
| 19 | return; |
| 20 | end |
| 21 | |
| 22 | local data = infile:read("*a"); |
| 23 | infile:close(); |
| 24 | |
| 25 | local shebang, newdata = data:match("^(#.-\n)(.+)$"); |
| 26 | local code = newdata or data; |
| 27 | if shebang then |
| 28 | outfile:write(shebang) |
| 29 | end |
| 30 | |
| 31 | |
| 32 | while base_char + #keywords <= 255 and code:find("["..string.char(base_char).."-"..string.char(base_char+#keywords-1).."]") do |
| 33 | base_char = base_char + 1; |
| 34 | end |
| 35 | if base_char + #keywords > 255 then |
| 36 | -- Sorry, can't uglify this file :( |
| 37 | -- We /could/ use a multi-byte marker, but that would complicate |
| 38 | -- things and lower the compression ratio (there are quite a few |
| 39 | -- 2-letter keywords) |
| 40 | outfile:write(code); |
| 41 | outfile:close(); |
| 42 | os.rename(outfile_fn..".uglified", outfile_fn); |
| 43 | return; |
| 44 | end |
| 45 | |
| 46 | local keyword_map_to_char = {} |
| 47 | for i, keyword in ipairs(keywords) do |
| 48 | keyword_map_to_char[keyword] = string.char(base_char + i); |
| 49 | end |
| 50 | |
| 51 | -- Write loadstring and open string |
| 52 | local maxequals = 0; |
| 53 | data:gsub("(=+)", function (equals_string) maxequals = math.max(maxequals, #equals_string); end); |
| 54 | |
| 55 | -- Go lexer! |
| 56 | llex.init(code, "@"..infile_fn); |
| 57 | llex.llex() |
| 58 | local seminfo = llex.seminfo; |
| 59 | |
| 60 | if opts.uglify_level == "full" and base_char+#keywords < 255 then |
| 61 | -- Find longest TK_NAME and TK_STRING tokens |
| 62 | local scores = {}; |
| 63 | for k,v in ipairs(llex.tok) do |
| 64 | if v == "TK_NAME" or v == "TK_STRING" then |
| 65 | local key = string.format("%q,%q", v, seminfo[k]); |
| 66 | if not scores[key] then |
| 67 | scores[key] = { type = v, value = seminfo[k], count = 0 }; |
| 68 | scores[#scores+1] = scores[key]; |
| 69 | end |
| 70 | scores[key].count = scores[key].count + 1; |
| 71 | end |
| 72 | end |
| 73 | for i=1,#scores do |
| 74 | local v = scores[i]; |
| 75 | v.score = (v.count)*(#v.value-1)- #string.format("%q", v.value) - 1; |
| 76 | end |
| 77 | table.sort(scores, function (a, b) return a.score > b.score; end); |
| 78 | local free_space = 255-(base_char+#keywords); |
| 79 | for i=free_space+1,#scores do |
| 80 | scores[i] = nil; -- Drop any over the limit |
| 81 | end |
| 82 | |
| 83 | local base_keywords_len = #keywords; |
| 84 | for k,v in ipairs(scores) do |
| 85 | if v.score > 0 then |
| 86 | table.insert(keywords, v.value); |
| 87 | keyword_map_to_char[v.value] = string.char(base_char+base_keywords_len+k); |
| 88 | end |
| 89 | end |
| 90 | end |
| 91 | |
| 92 | outfile:write("local base_char,keywords=", tostring(base_char), ",{"); |
| 93 | for _, keyword in ipairs(keywords) do |
| 94 | outfile:write(string.format("%q", keyword), ','); |
| 95 | end |
| 96 | outfile:write[[}; function prettify(code) return code:gsub("["..string.char(base_char).."-"..string.char(base_char+#keywords).."]", |
| 97 | function (c) return keywords[c:byte()-base_char]; end) end ]] |
| 98 | |
| 99 | outfile:write [[return setfenv(assert(loadstring(prettify]] |
| 100 | outfile:write("[", string.rep("=", maxequals+1), "["); |
| 101 | |
| 102 | -- Write code, substituting tokens as we go |
| 103 | for k,v in ipairs(llex.tok) do |
| 104 | if v == "TK_KEYWORD" or v == "TK_NAME" or v == "TK_STRING" then |
| 105 | local keyword_char = keyword_map_to_char[seminfo[k]]; |
| 106 | if keyword_char then |
| 107 | outfile:write(keyword_char); |
| 108 | else -- Those who think Lua shouldn't have 'continue, fix this please :) |
| 109 | outfile:write(seminfo[k]); |
| 110 | end |
| 111 | else |
| 112 | outfile:write(seminfo[k]); |
| 113 | end |
| 114 | end |
| 115 | |
| 116 | -- Close string/functions |
| 117 | outfile:write("]", string.rep("=", maxequals+1), "]"); |
| 118 | outfile:write(", '@", outfile_fn,"')), getfenv())()"); |
| 119 | outfile:close(); |
| 120 | os.rename(outfile_fn..".uglified", outfile_fn); |
| 121 | end |
| 122 | |
| 123 | if opts.uglify then |
| 124 | print_info("Uglifying "..out_fn.."..."); |
| 125 | uglify_file(out_fn, out_fn); |
| 126 | print_info("OK!"); |
| 127 | end |