1068 lines
33 KiB
Lua
1068 lines
33 KiB
Lua
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 = {
|
||
['&'] = '&',
|
||
['<'] = '<',
|
||
['>'] = '>',
|
||
['"'] = '"',
|
||
["'"] = '''
|
||
}
|
||
|
||
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
|