diff --git a/metadata/version.go b/metadata/version.go index 9070f19..3307c11 100644 --- a/metadata/version.go +++ b/metadata/version.go @@ -1,7 +1,5 @@ package metadata const ( - Version = "1.0.0" - CommitHash = "placeholder" - BuildDate = "25/07/2025" + Version = "1.0.0" ) diff --git a/modules/http/http.go b/modules/http/http.go index 65f2b2b..05bb33f 100644 --- a/modules/http/http.go +++ b/modules/http/http.go @@ -1,6 +1,7 @@ package http import ( + "Moonshark/metadata" "context" "fmt" "net" @@ -49,6 +50,7 @@ func http_create_server(s *luajit.State) int { } globalServer = &fasthttp.Server{ + Name: "Moonshark/" + metadata.Version, Handler: handleRequest, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, @@ -174,6 +176,7 @@ func http_register_static(s *luajit.State) int { urlPrefix := s.ToString(1) rootPath := s.ToString(2) + noCache := s.ToBoolean(3) // Ensure prefix starts with / if !strings.HasPrefix(urlPrefix, "/") { @@ -188,7 +191,7 @@ func http_register_static(s *luajit.State) int { return 2 } - RegisterStaticHandler(urlPrefix, absPath) + RegisterStaticHandler(urlPrefix, absPath, noCache) s.PushBoolean(true) return 1 } @@ -293,16 +296,36 @@ func isLikelyStaticFile(path string) bool { } // RegisterStaticHandler adds a static file handler -func RegisterStaticHandler(urlPrefix, rootPath string) { +func RegisterStaticHandler(urlPrefix, rootPath string, noCache bool) { staticMu.Lock() defer staticMu.Unlock() + var cacheDuration time.Duration + var compress bool + if noCache { + cacheDuration = 0 + compress = false + } else { + cacheDuration = 3600 * time.Second + compress = true + } + fs := &fasthttp.FS{ Root: rootPath, + CompressRoot: rootPath + "/.cache", IndexNames: []string{"index.html"}, GenerateIndexPages: false, - Compress: true, + Compress: compress, + CompressBrotli: compress, + CompressZstd: compress, + CacheDuration: cacheDuration, AcceptByteRange: true, + PathNotFound: func(ctx *fasthttp.RequestCtx) { + path := ctx.Path() + fmt.Printf("404 not found: %s\n", path) + ctx.SetStatusCode(fasthttp.StatusNotFound) + ctx.SetBodyString("404 not found") + }, } staticHandlers[urlPrefix] = fs diff --git a/modules/http/http.lua b/modules/http/http.lua index 05a1de4..318db78 100644 --- a/modules/http/http.lua +++ b/modules/http/http.lua @@ -920,14 +920,16 @@ function Server:close() return _G.__IS_WORKER or moonshark.http_close_server() end -function Server:static(root_path, url_prefix) +function Server:static(root_path, url_prefix, no_cache) + if not no_cache or no_cache ~= true then no_cache = false end + url_prefix = url_prefix or "/" if not string.starts_with(url_prefix, "/") then url_prefix = "/" .. url_prefix end if not _G.__IS_WORKER then - local success, err = moonshark.http_register_static(url_prefix, root_path) + local success, err = moonshark.http_register_static(url_prefix, root_path, no_cache) if not success then error("Failed to register static handler: " .. (err or "unknown error")) end diff --git a/modules/string+/string.lua b/modules/string+/string.lua index 790a33d..6f6a0b5 100644 --- a/modules/string+/string.lua +++ b/modules/string+/string.lua @@ -670,3 +670,398 @@ function string.random(length, charset) end return result end + +-- Template processing with code execution +function string.render(template_str, env) + local function is_control_structure(code) + -- Check if code is a control structure that doesn't produce output + local trimmed = code:match("^%s*(.-)%s*$") + return trimmed == "else" or + trimmed == "end" or + trimmed:match("^if%s") or + trimmed:match("^elseif%s") or + trimmed:match("^for%s") or + trimmed:match("^while%s") or + trimmed:match("^repeat%s*$") or + trimmed:match("^until%s") or + trimmed:match("^do%s*$") or + trimmed:match("^local%s") or + trimmed:match("^function%s") or + trimmed:match(".*=%s*function%s*%(") or + trimmed:match(".*then%s*$") or + trimmed:match(".*do%s*$") + end + + local pos, chunks = 1, {} + while pos <= #template_str do + local unescaped_start = template_str:find("{{{", pos, true) + local escaped_start = template_str:find("{{", pos, true) + + local start, tag_type, open_len + if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then + start, tag_type, open_len = unescaped_start, "-", 3 + elseif escaped_start then + start, tag_type, open_len = escaped_start, "=", 2 + else + table.insert(chunks, template_str:sub(pos)) + break + end + + if start > pos then + table.insert(chunks, template_str:sub(pos, start-1)) + end + + pos = start + open_len + local close_tag = tag_type == "-" and "}}}" or "}}" + local close_start, close_stop = template_str:find(close_tag, pos, true) + if not close_start then + error("Failed to find closing tag at position " .. pos) + end + + local code = template_str:sub(pos, close_start-1):match("^%s*(.-)%s*$") + local is_control = is_control_structure(code) + + table.insert(chunks, {tag_type, code, pos, is_control}) + pos = close_stop + 1 + end + + local buffer = {"local _tostring, _escape, _b, _b_i = ...\n"} + for _, chunk in ipairs(chunks) do + local t = type(chunk) + if t == "string" then + table.insert(buffer, "_b_i = _b_i + 1\n") + table.insert(buffer, "_b[_b_i] = " .. string.format("%q", chunk) .. "\n") + else + local tag_type, code, pos, is_control = chunk[1], chunk[2], chunk[3], chunk[4] + + if is_control then + -- Control structure - just insert as raw Lua code + table.insert(buffer, "--[[" .. pos .. "]] " .. code .. "\n") + elseif tag_type == "=" then + -- Simple variable check + if code:match("^[%w_]+$") then + table.insert(buffer, "_b_i = _b_i + 1\n") + table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _escape(_tostring(" .. code .. "))\n") + else + -- Expression output with escaping + table.insert(buffer, "_b_i = _b_i + 1\n") + table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _escape(_tostring(" .. code .. "))\n") + end + elseif tag_type == "-" then + -- Unescaped output + table.insert(buffer, "_b_i = _b_i + 1\n") + table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _tostring(" .. code .. ")\n") + end + end + end + table.insert(buffer, "return _b") + + local generated_code = table.concat(buffer) + + -- DEBUG: Uncomment to see generated code + -- print("Generated Lua code:") + -- print(generated_code) + -- print("---") + + local fn, err = loadstring(generated_code) + if not fn then + print("Generated code that failed to compile:") + print(generated_code) + error(err) + end + + env = env or {} + local runtime_env = setmetatable({}, {__index = function(_, k) return env[k] or _G[k] end}) + setfenv(fn, runtime_env) + + local output_buffer = {} + fn(tostring, html_special_chars, output_buffer, 0) + return table.concat(output_buffer) +end + +-- Named placeholder processing +function string.parse(template_str, env) + local pos, output = 1, {} + env = env or {} + + while pos <= #template_str do + local unescaped_start, unescaped_end, unescaped_name = template_str:find("{{{%s*([%w_]+)%s*}}}", pos) + local escaped_start, escaped_end, escaped_name = template_str:find("{{%s*([%w_]+)%s*}}", pos) + + local next_pos, placeholder_end, name, escaped + if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then + next_pos, placeholder_end, name, escaped = unescaped_start, unescaped_end, unescaped_name, false + elseif escaped_start then + next_pos, placeholder_end, name, escaped = escaped_start, escaped_end, escaped_name, true + else + local text = template_str:sub(pos) + if text and #text > 0 then + table.insert(output, text) + end + break + end + + local text = template_str:sub(pos, next_pos - 1) + if text and #text > 0 then + table.insert(output, text) + end + + local value = env[name] + local str = tostring(value or "") + if escaped then + str:html_special_chars() + end + table.insert(output, str) + + pos = placeholder_end + 1 + end + + return table.concat(output) +end + +-- Indexed placeholder processing +function string.iparse(template_str, values) + local pos, output, value_index = 1, {}, 1 + values = values or {} + + while pos <= #template_str do + local unescaped_start, unescaped_end = template_str:find("{{{}}}", pos, true) + local escaped_start, escaped_end = template_str:find("{{}}", pos, true) + + local next_pos, placeholder_end, escaped + if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then + next_pos, placeholder_end, escaped = unescaped_start, unescaped_end, false + elseif escaped_start then + next_pos, placeholder_end, escaped = escaped_start, escaped_end, true + else + local text = template_str:sub(pos) + if text and #text > 0 then + table.insert(output, text) + end + break + end + + local text = template_str:sub(pos, next_pos - 1) + if text and #text > 0 then + table.insert(output, text) + end + + local value = values[value_index] + local str = tostring(value or "") + if escaped then + str:html_special_chars() + end + table.insert(output, str) + + pos = placeholder_end + 1 + value_index = value_index + 1 + end + + return table.concat(output) +end + +string.special_chars_pattern = '[&<>"\']' +string.entity_decode_pattern = '&[#%w]+;' + +string.special_encode_map = { + ['&'] = '&', + ['<'] = '<', + ['>'] = '>', + ['"'] = '"', + ["'"] = ''' +} + +string.special_decode_map = { + ['&'] = '&', + ['<'] = '<', + ['>'] = '>', + ['"'] = '"', + ['''] = "'", + ['''] = "'" +} + +string.extended_encode_map = { + -- Special chars + ['&'] = '&', + ['<'] = '<', + ['>'] = '>', + ['"'] = '"', + ["'"] = ''', + -- Extended characters + [' '] = ' ', + ['¡'] = '¡', + ['¢'] = '¢', + ['£'] = '£', + ['¤'] = '¤', + ['¥'] = '¥', + ['¦'] = '¦', + ['§'] = '§', + ['¨'] = '¨', + ['©'] = '©', + ['ª'] = 'ª', + ['«'] = '«', + ['¬'] = '¬', + ['®'] = '®', + ['¯'] = '¯', + ['°'] = '°', + ['±'] = '±', + ['²'] = '²', + ['³'] = '³', + ['´'] = '´', + ['µ'] = 'µ', + ['¶'] = '¶', + ['·'] = '·', + ['¸'] = '¸', + ['¹'] = '¹', + ['º'] = 'º', + ['»'] = '»', + ['¼'] = '¼', + ['½'] = '½', + ['¾'] = '¾', + ['¿'] = '¿', + ['À'] = 'À', + ['Á'] = 'Á', + ['Â'] = 'Â', + ['Ã'] = 'Ã', + ['Ä'] = 'Ä', + ['Å'] = 'Å', + ['Æ'] = 'Æ', + ['Ç'] = 'Ç', + ['È'] = 'È', + ['É'] = 'É', + ['Ê'] = 'Ê', + ['Ë'] = 'Ë', + ['Ì'] = 'Ì', + ['Í'] = 'Í', + ['Î'] = 'Î', + ['Ï'] = 'Ï', + ['Ð'] = 'Ð', + ['Ñ'] = 'Ñ', + ['Ò'] = 'Ò', + ['Ó'] = 'Ó', + ['Ô'] = 'Ô', + ['Õ'] = 'Õ', + ['Ö'] = 'Ö', + ['×'] = '×', + ['Ø'] = 'Ø', + ['Ù'] = 'Ù', + ['Ú'] = 'Ú', + ['Û'] = 'Û', + ['Ü'] = 'Ü', + ['Ý'] = 'Ý', + ['Þ'] = 'Þ', + ['ß'] = 'ß', + ['à'] = 'à', + ['á'] = 'á', + ['â'] = 'â', + ['ã'] = 'ã', + ['ä'] = 'ä', + ['å'] = 'å', + ['æ'] = 'æ', + ['ç'] = 'ç', + ['è'] = 'è', + ['é'] = 'é', + ['ê'] = 'ê', + ['ë'] = 'ë', + ['ì'] = 'ì', + ['í'] = 'í', + ['î'] = 'î', + ['ï'] = 'ï', + ['ð'] = 'ð', + ['ñ'] = 'ñ', + ['ò'] = 'ò', + ['ó'] = 'ó', + ['ô'] = 'ô', + ['õ'] = 'õ', + ['ö'] = 'ö', + ['÷'] = '÷', + ['ø'] = 'ø', + ['ù'] = 'ù', + ['ú'] = 'ú', + ['û'] = 'û', + ['ü'] = 'ü', + ['ý'] = 'ý', + ['þ'] = 'þ', + ['ÿ'] = 'ÿ' +} + +string.extended_decode_map = {} +for char, entity in pairs(string.extended_encode_map) do + string.extended_decode_map[entity] = char +end +-- Add common named entities not in extended_encode_map +string.extended_decode_map['''] = "'" +string.extended_decode_map[' '] = ' ' + +-- Converts HTML special characters (&, <, >, ", ') to entities +function string.html_special_chars(str) + if not str then return nil end + if not str:find(string.special_chars_pattern) then return str end + return (str:gsub(string.special_chars_pattern, string.special_encode_map)) +end +getmetatable("").__index.html_special_chars = string.html_special_chars + +-- Decodes HTML special character entities back to characters +function string.html_special_chars_decode(str) + if not str then return nil end + + return (str:gsub('&[lg]t;', string.special_decode_map) + :gsub('"', '"') + :gsub(''', "'") + :gsub(''', "'") + :gsub('&', '&')) -- Must be last to avoid double-decoding +end +getmetatable("").__index.html_special_chars_decode = string.html_special_chars_decode + +-- More comprehensive HTML entity encoding +-- Handles special chars plus extended Latin-1 characters +function string.html_entities(str) + if not str then return nil end + + local result = {} + local result_len = 0 + + for i = 1, #str do + local char = str:sub(i, i) + local entity = string.extended_encode_map[char] + + if entity then + result_len = result_len + 1 + result[result_len] = entity + else + local byte = string.byte(char) + if byte > 127 then + -- Encode high-bit characters as numeric entities + result_len = result_len + 1 + result[result_len] = '&#' .. byte .. ';' + else + result_len = result_len + 1 + result[result_len] = char + end + end + end + + return table.concat(result) +end +getmetatable("").__index.html_entities = string.html_entities + +function string.html_entities_decode(str) + if not str then return nil end + + -- Handle numeric entities first + local result = str:gsub('&#(%d+);', function(num) + local n = tonumber(num) + if n and n >= 0 and n <= 255 then + return string.char(n) + end + return '&#' .. num .. ';' -- Return unchanged if invalid + end) + + -- Handle named entities + result = result:gsub(string.entity_decode_pattern, function(entity) + return string.extended_decode_map[entity] or entity + end) + + return result +end +getmetatable("").__index.html_entities_decode = string.html_entities_decode diff --git a/moonshark.go b/moonshark.go index 8b8bb66..ad53b51 100644 --- a/moonshark.go +++ b/moonshark.go @@ -16,7 +16,7 @@ import ( var ( watchFlag = flag.Bool("watch", false, "Watch script files for changes and restart") - wFlag = flag.Bool("w", false, "Watch script files for changes and restart (short)") + wFlag = flag.Bool("w", false, "Watch script files for changes and restart") ) func main() {