1068 lines
33 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

local _orig_find = string.find
local _orig_match = string.match
local REVERSE_THRESHOLD = 100
local LENGTH_THRESHOLD = 1000
function string.split(s, delimiter)
if type(s) ~= "string" then error("string.split: first argument must be a string", 2) end
if type(delimiter) ~= "string" then error("string.split: second argument must be a string", 2) end
if delimiter == "" then
local result = {}
for i = 1, #s do
result[i] = s:sub(i, i)
end
return result
end
local result = {}
local start = 1
local delimiter_len = #delimiter
while true do
local pos = _orig_find(s, delimiter, start, true) -- Use original find
if not pos then
table.insert(result, s:sub(start))
break
end
table.insert(result, s:sub(start, pos - 1))
start = pos + delimiter_len
end
return result
end
getmetatable("").__index.split = string.split
function string.join(arr, separator)
if type(arr) ~= "table" then error("string.join: first argument must be a table", 2) end
if type(separator) ~= "string" then error("string.join: second argument must be a string", 2) end
return table.concat(arr, separator)
end
function string.trim(s, cutset)
if type(s) ~= "string" then error("string.trim: first argument must be a string", 2) end
if cutset then
if type(cutset) ~= "string" then error("string.trim: second argument must be a string", 2) end
local escaped = cutset:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
local pattern = "^[" .. escaped .. "]*(.-)[" .. escaped .. "]*$"
return s:match(pattern)
else
return s:match("^%s*(.-)%s*$")
end
end
getmetatable("").__index.trim = string.trim
function string.trim_left(s, cutset)
if type(s) ~= "string" then error("string.trim_left: first argument must be a string", 2) end
if cutset then
if type(cutset) ~= "string" then error("string.trim_left: second argument must be a string", 2) end
local pattern = "^[" .. cutset:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") .. "]*"
return s:gsub(pattern, "")
else
return s:match("^%s*(.*)")
end
end
getmetatable("").__index.trim_left = string.trim_left
function string.trim_right(s, cutset)
if type(s) ~= "string" then error("string.trim_right: first argument must be a string", 2) end
if cutset then
if type(cutset) ~= "string" then error("string.trim_right: second argument must be a string", 2) end
local escaped = cutset:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
local pattern = "[" .. escaped .. "]*$"
return s:gsub(pattern, "")
else
return s:match("(.-)%s*$")
end
end
function string.title(s)
if type(s) ~= "string" then error("string.title: argument must be a string", 2) end
return s:gsub("(%w)([%w]*)", function(first, rest)
return first:upper() .. rest:lower()
end)
end
getmetatable("").__index.title = string.title
function string.contains(s, substr)
if type(s) ~= "string" then error("string.contains: first argument must be a string", 2) end
if type(substr) ~= "string" then error("string.contains: second argument must be a string", 2) end
return _orig_find(s, substr, 1, true) ~= nil
end
getmetatable("").__index.contains = string.contains
function string.starts_with(s, prefix)
if type(s) ~= "string" then error("string.starts_with: first argument must be a string", 2) end
if type(prefix) ~= "string" then error("string.starts_with: second argument must be a string", 2) end
return s:sub(1, #prefix) == prefix
end
getmetatable("").__index.starts_with = string.starts_with
function string.ends_with(s, suffix)
if type(s) ~= "string" then error("string.ends_with: first argument must be a string", 2) end
if type(suffix) ~= "string" then error("string.ends_with: second argument must be a string", 2) end
if #suffix == 0 then return true end
return s:sub(-#suffix) == suffix
end
getmetatable("").__index.ends_with = string.ends_with
function string.replace(s, old, new)
if type(s) ~= "string" then error("string.replace: first argument must be a string", 2) end
if type(old) ~= "string" then error("string.replace: second argument must be a string", 2) end
if type(new) ~= "string" then error("string.replace: third argument must be a string", 2) end
if old == "" then error("string.replace: cannot replace empty string", 2) end
return s:gsub(old:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1"), new)
end
getmetatable("").__index.replace = string.replace
function string.replace_n(s, old, new, n)
if type(s) ~= "string" then error("string.replace_n: first argument must be a string", 2) end
if type(old) ~= "string" then error("string.replace_n: second argument must be a string", 2) end
if type(new) ~= "string" then error("string.replace_n: third argument must be a string", 2) end
if type(n) ~= "number" or n < 0 or n ~= math.floor(n) then
error("string.replace_n: fourth argument must be a non-negative integer", 2)
end
if old == "" then error("string.replace_n: cannot replace empty string", 2) end
local escaped = old:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
return (s:gsub(escaped, new, n))
end
getmetatable("").__index.replace_n = string.replace_n
function string.index(s, substr)
if type(s) ~= "string" then error("string.index: first argument must be a string", 2) end
if type(substr) ~= "string" then error("string.index: second argument must be a string", 2) end
local pos = _orig_find(s, substr, 1, true)
return pos
end
getmetatable("").__index.index = string.index
function string.last_index(s, substr)
if type(s) ~= "string" then error("string.last_index: first argument must be a string", 2) end
if type(substr) ~= "string" then error("string.last_index: second argument must be a string", 2) end
local last_pos = nil
local pos = 1
while true do
local found = _orig_find(s, substr, pos, true)
if not found then break end
last_pos = found
pos = found + 1
end
return last_pos
end
getmetatable("").__index.last_index = string.last_index
function string.count(s, substr)
if type(s) ~= "string" then error("string.count: first argument must be a string", 2) end
if type(substr) ~= "string" then error("string.count: second argument must be a string", 2) end
if substr == "" then return #s + 1 end
local count = 0
local pos = 1
while true do
local found = _orig_find(s, substr, pos, true)
if not found then break end
count = count + 1
pos = found + #substr
end
return count
end
getmetatable("").__index.count = string.count
function string.repeat_(s, n)
if type(s) ~= "string" then error("string.repeat_: first argument must be a string", 2) end
if type(n) ~= "number" or n < 0 or n ~= math.floor(n) then
error("string.repeat_: second argument must be a non-negative integer", 2)
end
return string.rep(s, n)
end
function string.reverse(s)
if type(s) ~= "string" then error("string.reverse: argument must be a string", 2) end
if #s > REVERSE_THRESHOLD then
local result, err = moonshark.string_reverse(s)
if not result then error("string.reverse: " .. err, 2) end
return result
else
local result = {}
for i = #s, 1, -1 do
result[#result + 1] = s:sub(i, i)
end
return table.concat(result)
end
end
getmetatable("").__index.reverse = string.reverse
function string.length(s)
if type(s) ~= "string" then error("string.length: argument must be a string", 2) end
return moonshark.string_length(s)
end
getmetatable("").__index.length = string.length
function string.byte_length(s)
if type(s) ~= "string" then error("string.byte_length: argument must be a string", 2) end
return moonshark.string_byte_length(s)
end
getmetatable("").__index.byte_length = string.byte_length
function string.lines(s)
if type(s) ~= "string" then error("string.lines: argument must be a string", 2) end
if s == "" then return {""} end
s = s:gsub("\r\n", "\n"):gsub("\r", "\n")
local lines = {}
for line in (s .. "\n"):gmatch("([^\n]*)\n") do
table.insert(lines, line)
end
if #lines > 0 and lines[#lines] == "" then
table.remove(lines)
end
return lines
end
getmetatable("").__index.lines = string.lines
function string.words(s)
if type(s) ~= "string" then error("string.words: argument must be a string", 2) end
local words = {}
for word in s:gmatch("%S+") do
table.insert(words, word)
end
return words
end
getmetatable("").__index.words = string.words
function string.pad_left(s, width, pad_char)
if type(s) ~= "string" then error("string.pad_left: first argument must be a string", 2) end
if type(width) ~= "number" or width < 0 or width ~= math.floor(width) then
error("string.pad_left: second argument must be a non-negative integer", 2)
end
pad_char = pad_char or " "
if type(pad_char) ~= "string" then error("string.pad_left: third argument must be a string", 2) end
if #pad_char == 0 then pad_char = " " else pad_char = pad_char:sub(1,1) end
local current_len = string.length(s)
if current_len >= width then return s end
return string.rep(pad_char, width - current_len) .. s
end
getmetatable("").__index.pad_left = string.pad_left
function string.pad_right(s, width, pad_char)
if type(s) ~= "string" then error("string.pad_right: first argument must be a string", 2) end
if type(width) ~= "number" or width < 0 or width ~= math.floor(width) then
error("string.pad_right: second argument must be a non-negative integer", 2)
end
pad_char = pad_char or " "
if type(pad_char) ~= "string" then error("string.pad_right: third argument must be a string", 2) end
if #pad_char == 0 then pad_char = " " else pad_char = pad_char:sub(1,1) end
local current_len = string.length(s)
if current_len >= width then return s end
return s .. string.rep(pad_char, width - current_len)
end
getmetatable("").__index.pad_right = string.pad_right
function string.slice(s, start, end_pos)
if type(s) ~= "string" then error("string.slice: first argument must be a string", 2) end
if type(start) ~= "number" or start ~= math.floor(start) then
error("string.slice: second argument must be an integer", 2)
end
if end_pos ~= nil and (type(end_pos) ~= "number" or end_pos ~= math.floor(end_pos)) then
error("string.slice: third argument must be an integer", 2)
end
local result, err = moonshark.string_slice(s, start, end_pos)
if not result then error("string.slice: " .. err, 2) end
return result
end
getmetatable("").__index.slice = string.slice
function string.find(s, pattern, init, plain)
if type(s) ~= "string" then error("string.find: first argument must be a string", 2) end
if type(pattern) ~= "string" then error("string.find: second argument must be a string", 2) end
return _orig_find(s, pattern, init, plain)
end
getmetatable("").__index.find = string.find
function string.find_match(s, pattern, init, plain)
if type(s) ~= "string" then error("string.find_match: first argument must be a string", 2) end
if type(pattern) ~= "string" then error("string.find_match: second argument must be a string", 2) end
local start_pos, end_pos = _orig_find(s, pattern, init, plain)
if start_pos then
return s:sub(start_pos, end_pos)
end
return nil
end
getmetatable("").__index.find_match = string.find_match
function string.find_all(s, pattern)
if type(s) ~= "string" then error("string.find_all: first argument must be a string", 2) end
if type(pattern) ~= "string" then error("string.find_all: second argument must be a string", 2) end
local matches = {}
for match in s:gmatch(pattern) do
table.insert(matches, match)
end
return matches
end
getmetatable("").__index.find_all = string.find_all
function string.to_number(s)
if type(s) ~= "string" then error("string.to_number: argument must be a string", 2) end
s = string.trim(s)
return tonumber(s)
end
getmetatable("").__index.to_number = string.to_number
function string.is_numeric(s)
if type(s) ~= "string" then error("string.is_numeric: argument must be a string", 2) end
s = string.trim(s)
return tonumber(s) ~= nil
end
getmetatable("").__index.is_numeric = string.is_numeric
function string.is_alpha(s)
if type(s) ~= "string" then error("string.is_alpha: argument must be a string", 2) end
if #s == 0 then return false end
return s:match("^%a+$") ~= nil
end
getmetatable("").__index.is_alpha = string.is_alpha
function string.is_alphanumeric(s)
if type(s) ~= "string" then error("string.is_alphanumeric: argument must be a string", 2) end
if #s == 0 then return false end
return s:match("^%w+$") ~= nil
end
getmetatable("").__index.is_alphanumeric = string.is_alphanumeric
function string.is_utf8(s)
if type(s) ~= "string" then error("string.is_utf8: argument must be a string", 2) end
return moonshark.string_is_valid_utf8(s)
end
getmetatable("").__index.is_utf8 = string.is_utf8
function string.is_empty(s)
return s == nil or s == ""
end
getmetatable("").__index.is_empty = string.is_empty
function string.is_blank(s)
return s == nil or s == "" or string.trim(s) == ""
end
getmetatable("").__index.is_blank = string.is_blank
function string.capitalize(s)
if type(s) ~= "string" then error("string.capitalize: argument must be a string", 2) end
return s:gsub("(%a)([%w_']*)", function(first, rest)
return first:upper() .. rest:lower()
end)
end
getmetatable("").__index.capitalize = string.capitalize
function string.camel_case(s)
if type(s) ~= "string" then error("string.camel_case: argument must be a string", 2) end
local words = string.words(s)
if #words == 0 then return s end
local result = words[1]:lower()
for i = 2, #words do
result = result .. words[i]:sub(1,1):upper() .. words[i]:sub(2):lower()
end
return result
end
getmetatable("").__index.camel_case = string.camel_case
function string.pascal_case(s)
if type(s) ~= "string" then error("string.pascal_case: argument must be a string", 2) end
local words = string.words(s)
local result = ""
for _, word in ipairs(words) do
result = result .. word:sub(1,1):upper() .. word:sub(2):lower()
end
return result
end
getmetatable("").__index.pascal_case = string.pascal_case
function string.snake_case(s)
if type(s) ~= "string" then error("string.snake_case: argument must be a string", 2) end
local words = string.words(s)
local result = {}
for _, word in ipairs(words) do
table.insert(result, word:lower())
end
return table.concat(result, "_")
end
getmetatable("").__index.snake_case = string.snake_case
function string.kebab_case(s)
if type(s) ~= "string" then error("string.kebab_case: argument must be a string", 2) end
local words = string.words(s)
local result = {}
for _, word in ipairs(words) do
table.insert(result, word:lower())
end
return table.concat(result, "-")
end
getmetatable("").__index.kebab_case = string.kebab_case
function string.screaming_snake_case(s)
if type(s) ~= "string" then error("string.screaming_snake_case: argument must be a string", 2) end
return string.snake_case(s):upper()
end
getmetatable("").__index.screaming_snake_case = string.screaming_snake_case
function string.center(s, width, fill_char)
if type(s) ~= "string" then error("string.center: first argument must be a string", 2) end
if type(width) ~= "number" or width < 0 or width ~= math.floor(width) then
error("string.center: second argument must be a non-negative integer", 2)
end
fill_char = fill_char or " "
if type(fill_char) ~= "string" or #fill_char == 0 then
error("string.center: fill character must be a non-empty string", 2)
end
fill_char = fill_char:sub(1,1)
local len = string.length(s)
if len >= width then return s end
local pad_total = width - len
local pad_left = math.floor(pad_total / 2)
local pad_right = pad_total - pad_left
return string.rep(fill_char, pad_left) .. s .. string.rep(fill_char, pad_right)
end
getmetatable("").__index.center = string.center
function string.truncate(s, max_length, suffix)
if type(s) ~= "string" then error("string.truncate: first argument must be a string", 2) end
if type(max_length) ~= "number" or max_length < 0 or max_length ~= math.floor(max_length) then
error("string.truncate: second argument must be a non-negative integer", 2)
end
suffix = suffix or "..."
if type(suffix) ~= "string" then error("string.truncate: third argument must be a string", 2) end
local len = string.length(s)
if len <= max_length then return s end
local suffix_len = string.length(suffix)
if max_length <= suffix_len then
return string.slice(suffix, 1, max_length)
end
local main_part = string.slice(s, 1, max_length - suffix_len)
main_part = string.trim_right(main_part)
return main_part .. suffix
end
getmetatable("").__index.truncate = string.truncate
function string.wrap(s, width)
if type(s) ~= "string" then error("string.wrap: first argument must be a string", 2) end
if type(width) ~= "number" or width <= 0 or width ~= math.floor(width) then
error("string.wrap: second argument must be a positive integer", 2)
end
if s == "" then return {""} end
local words = string.words(s)
if #words == 0 then return {""} end
local lines = {}
local current_line = ""
for _, word in ipairs(words) do
if string.length(word) > width then
if current_line ~= "" then
table.insert(lines, current_line)
current_line = ""
end
table.insert(lines, word)
elseif current_line == "" then
current_line = word
elseif string.length(current_line) + 1 + string.length(word) <= width then
current_line = current_line .. " " .. word
else
table.insert(lines, current_line)
current_line = word
end
end
if current_line ~= "" then
table.insert(lines, current_line)
end
return lines
end
getmetatable("").__index.wrap = string.wrap
function string.dedent(s)
if type(s) ~= "string" then error("string.dedent: argument must be a string", 2) end
local lines = string.lines(s)
if #lines == 0 then return s end
local min_indent = math.huge
for _, line in ipairs(lines) do
if string.trim(line) ~= "" then
local indent = 0
for i = 1, #line do
if line:sub(i,i) == " " then
indent = indent + 1
else
break
end
end
min_indent = math.min(min_indent, indent)
end
end
if min_indent == math.huge then return s end
local result = {}
for _, line in ipairs(lines) do
if string.trim(line) == "" then
table.insert(result, "")
else
table.insert(result, line:sub(min_indent + 1))
end
end
return table.concat(result, "\n")
end
getmetatable("").__index.dedent = string.dedent
function string.escape(s)
if type(s) ~= "string" then error("string.escape: argument must be a string", 2) end
return (s:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1"))
end
getmetatable("").__index.escape = string.escape
function string.shell_quote(s)
if type(s) ~= "string" then error("string.shell_quote: argument must be a string", 2) end
if s:match("^[%w%.%-_/]+$") then
return s
end
return "'" .. s:gsub("'", "'\"'\"'") .. "'"
end
getmetatable("").__index.shell_quote = string.shell_quote
function string.url_encode(s)
if type(s) ~= "string" then error("string.url_encode: argument must be a string", 2) end
return s:gsub("([^%w%-%.%_%~])", function(c)
return string.format("%%%02X", string.byte(c))
end)
end
getmetatable("").__index.url_encode = string.url_encode
function string.url_decode(s)
if type(s) ~= "string" then error("string.url_decode: argument must be a string", 2) end
s = s:gsub("+", " ")
return s:gsub("%%(%x%x)", function(hex)
return string.char(tonumber(hex, 16))
end)
end
getmetatable("").__index.url_decode = string.url_decode
function string.slug(s)
if type(s) ~= "string" then error("string.slug: argument must be a string", 2) end
if s == "" then return "" end
local result = s:lower()
-- Remove accents first
result = string.remove_accents(result)
-- Keep only alphanumeric, spaces, and hyphens
result = result:gsub("[^%w%s%-]", "")
-- Replace spaces with hyphens
result = result:gsub("%s+", "-")
-- Remove duplicate hyphens
result = result:gsub("%-+", "-")
-- Remove leading/trailing hyphens
result = result:gsub("^%-", "")
result = result:gsub("%-$", "")
return result
end
getmetatable("").__index.slug = string.slug
function string.iequals(a, b)
if type(a) ~= "string" then error("string.iequals: first argument must be a string", 2) end
if type(b) ~= "string" then error("string.iequals: second argument must be a string", 2) end
return string.lower(a) == string.lower(b)
end
getmetatable("").__index.iequals = string.iequals
function string.is_whitespace(s)
if type(s) ~= "string" then error("string.is_whitespace: argument must be a string", 2) end
return s:match("^%s*$") ~= nil
end
getmetatable("").__index.is_whitespace = string.is_whitespace
function string.strip_whitespace(s)
if type(s) ~= "string" then error("string.strip_whitespace: argument must be a string", 2) end
return s:gsub("%s", "")
end
getmetatable("").__index.strip_whitespace = string.strip_whitespace
function string.normalize_whitespace(s)
if type(s) ~= "string" then error("string.normalize_whitespace: argument must be a string", 2) end
return string.trim((s:gsub("%s+", " ")))
end
getmetatable("").__index.normalize_whitespace = string.normalize_whitespace
function string.extract_numbers(s)
if type(s) ~= "string" then error("string.extract_numbers: argument must be a string", 2) end
local numbers = {}
for num in s:gmatch("%-?%d+%.?%d*") do
local n = tonumber(num)
if n then table.insert(numbers, n) end
end
return numbers
end
getmetatable("").__index.extract_numbers = string.extract_numbers
function string.remove_accents(s)
if type(s) ~= "string" then error("string.remove_accents: argument must be a string", 2) end
local accents = {
["á"] = "a", ["à"] = "a", ["ä"] = "a", ["â"] = "a", ["ã"] = "a", ["å"] = "a",
["Á"] = "A", ["À"] = "A", ["Ä"] = "A", ["Â"] = "A", ["Ã"] = "A", ["Å"] = "A",
["é"] = "e", ["è"] = "e", ["ë"] = "e", ["ê"] = "e",
["É"] = "E", ["È"] = "E", ["Ë"] = "E", ["Ê"] = "E",
["í"] = "i", ["ì"] = "i", ["ï"] = "i", ["î"] = "i",
["Í"] = "I", ["Ì"] = "I", ["Ï"] = "I", ["Î"] = "I",
["ó"] = "o", ["ò"] = "o", ["ö"] = "o", ["ô"] = "o", ["õ"] = "o",
["Ó"] = "O", ["Ò"] = "O", ["Ö"] = "O", ["Ô"] = "O", ["Õ"] = "O",
["ú"] = "u", ["ù"] = "u", ["ü"] = "u", ["û"] = "u",
["Ú"] = "U", ["Ù"] = "U", ["Ü"] = "U", ["Û"] = "U",
["ñ"] = "n", ["Ñ"] = "N",
["ç"] = "c", ["Ç"] = "C"
}
local result = s
for accented, plain in pairs(accents) do
result = result:gsub(accented, plain)
end
return result
end
getmetatable("").__index.remove_accents = string.remove_accents
function string.template(template_str, vars)
if type(template_str) ~= "string" then error("string.template: first argument must be a string", 2) end
if type(vars) ~= "table" then error("string.template: second argument must be a table", 2) end
return template_str:gsub("%${([%w_%.-]+)}", function(path)
local value = vars
-- Handle simple variables (no dots)
if not path:match("%.") then
return tostring(value[path] or "")
end
-- Handle nested properties
for key in path:gmatch("[^%.]+") do
if type(value) == "table" and value[key] ~= nil then
value = value[key]
else
return ""
end
end
return tostring(value)
end)
end
getmetatable("").__index.template = string.template
function string.random(length, charset)
local result, err = moonshark.random_string(length, charset)
if not result then
error(err)
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 = {
['&'] = '&amp;',
['<'] = '&lt;',
['>'] = '&gt;',
['"'] = '&quot;',
["'"] = '&#039;'
}
string.special_decode_map = {
['&amp;'] = '&',
['&lt;'] = '<',
['&gt;'] = '>',
['&quot;'] = '"',
['&#039;'] = "'",
['&apos;'] = "'"
}
string.extended_encode_map = {
-- Special chars
['&'] = '&amp;',
['<'] = '&lt;',
['>'] = '&gt;',
['"'] = '&quot;',
["'"] = '&#039;',
-- Extended characters
[' '] = '&nbsp;',
['¡'] = '&iexcl;',
['¢'] = '&cent;',
['£'] = '&pound;',
['¤'] = '&curren;',
['¥'] = '&yen;',
['¦'] = '&brvbar;',
['§'] = '&sect;',
['¨'] = '&uml;',
['©'] = '&copy;',
['ª'] = '&ordf;',
['«'] = '&laquo;',
['¬'] = '&not;',
['®'] = '&reg;',
['¯'] = '&macr;',
['°'] = '&deg;',
['±'] = '&plusmn;',
['²'] = '&sup2;',
['³'] = '&sup3;',
['´'] = '&acute;',
['µ'] = '&micro;',
[''] = '&para;',
['·'] = '&middot;',
['¸'] = '&cedil;',
['¹'] = '&sup1;',
['º'] = '&ordm;',
['»'] = '&raquo;',
['¼'] = '&frac14;',
['½'] = '&frac12;',
['¾'] = '&frac34;',
['¿'] = '&iquest;',
['À'] = '&Agrave;',
['Á'] = '&Aacute;',
['Â'] = '&Acirc;',
['Ã'] = '&Atilde;',
['Ä'] = '&Auml;',
['Å'] = '&Aring;',
['Æ'] = '&AElig;',
['Ç'] = '&Ccedil;',
['È'] = '&Egrave;',
['É'] = '&Eacute;',
['Ê'] = '&Ecirc;',
['Ë'] = '&Euml;',
['Ì'] = '&Igrave;',
['Í'] = '&Iacute;',
['Î'] = '&Icirc;',
['Ï'] = '&Iuml;',
['Ð'] = '&ETH;',
['Ñ'] = '&Ntilde;',
['Ò'] = '&Ograve;',
['Ó'] = '&Oacute;',
['Ô'] = '&Ocirc;',
['Õ'] = '&Otilde;',
['Ö'] = '&Ouml;',
['×'] = '&times;',
['Ø'] = '&Oslash;',
['Ù'] = '&Ugrave;',
['Ú'] = '&Uacute;',
['Û'] = '&Ucirc;',
['Ü'] = '&Uuml;',
['Ý'] = '&Yacute;',
['Þ'] = '&THORN;',
['ß'] = '&szlig;',
['à'] = '&agrave;',
['á'] = '&aacute;',
['â'] = '&acirc;',
['ã'] = '&atilde;',
['ä'] = '&auml;',
['å'] = '&aring;',
['æ'] = '&aelig;',
['ç'] = '&ccedil;',
['è'] = '&egrave;',
['é'] = '&eacute;',
['ê'] = '&ecirc;',
['ë'] = '&euml;',
['ì'] = '&igrave;',
['í'] = '&iacute;',
['î'] = '&icirc;',
['ï'] = '&iuml;',
['ð'] = '&eth;',
['ñ'] = '&ntilde;',
['ò'] = '&ograve;',
['ó'] = '&oacute;',
['ô'] = '&ocirc;',
['õ'] = '&otilde;',
['ö'] = '&ouml;',
['÷'] = '&divide;',
['ø'] = '&oslash;',
['ù'] = '&ugrave;',
['ú'] = '&uacute;',
['û'] = '&ucirc;',
['ü'] = '&uuml;',
['ý'] = '&yacute;',
['þ'] = '&thorn;',
['ÿ'] = '&yuml;'
}
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['&apos;'] = "'"
string.extended_decode_map['&nbsp;'] = ' '
-- 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('&quot;', '"')
:gsub('&#039;', "'")
:gsub('&apos;', "'")
:gsub('&amp;', '&')) -- 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