Module:Util: Difference between revisions
Jump to navigation
Jump to search
>OmegaK2 No edit summary |
(Added function to remove duplicate values from an array) |
||
(148 intermediate revisions by 6 users not shown) | |||
Line 1: | Line 1: | ||
-- | ------------------------------------------------------------------------------- | ||
-- | |||
-- Module:Util | |||
-- | |||
-- This meta module contains a number of utility functions | |||
------------------------------------------------------------------------------- | |||
local getArgs -- Lazy load require('Module:Arguments').getArgs | |||
local m_cargo -- Lazy load require('Module:Cargo') | |||
-- The cfg table contains all localisable strings and configuration, to make it | |||
-- easier to port this module to another wiki. | |||
local cfg = mw.loadData('Module:Util/config') | |||
local i18n = cfg.i18n | |||
local util = {} | local util = {} | ||
Line 9: | Line 22: | ||
util.cast = {} | util.cast = {} | ||
function util.cast. | function util.cast.text(value, args) | ||
-- Takes an | -- Takes an arbitary value and converts it to text. | ||
-- | -- | ||
-- for strings false will be according to | -- Also strips any categories | ||
-- | |||
-- args | |||
-- cast_nil - Cast lua nil value to "nil" string | |||
-- Default: false | |||
-- discard_empty - if the string is empty, return nil rather then empty string | |||
-- Default: true | |||
args = args or {} | |||
if args.discard_empty == nil then | |||
args.discard_empty = true | |||
end | |||
if value == nil and not args.cast_nil then | |||
return | |||
end | |||
value = tostring(value) | |||
if value == '' and args.discard_empty then | |||
return | |||
end | |||
-- Reassign to variable before returning since string.gsub returns two values | |||
value = string.gsub(value, '%[%[Category:[%w_ ]+%]%]', '') | |||
return value | |||
end | |||
function util.cast.boolean(value, args) | |||
-- Takes an arbitrary value and attempts to convert it to a boolean. | |||
-- | |||
-- for strings false will be according to i18n.bool_false | |||
-- | |||
-- args | |||
-- cast_nil if set to false, it will not cast nil values | |||
args = args or {} | |||
local t = type(value) | local t = type(value) | ||
if t == 'nil' then | if t == 'nil' then | ||
return false | if args.cast_nil == nil or args.cast_nil == true then | ||
return false | |||
else | |||
return | |||
end | |||
elseif t == 'boolean' then | elseif t == 'boolean' then | ||
return value | return value | ||
Line 25: | Line 74: | ||
elseif t == 'string' then | elseif t == 'string' then | ||
local tmp = string.lower(value) | local tmp = string.lower(value) | ||
for _, v in ipairs( | for _, v in ipairs(i18n.bool_false) do | ||
if v == tmp then | if v == tmp then | ||
return false | return false | ||
Line 32: | Line 81: | ||
return true | return true | ||
else | else | ||
error(string.format( | error(string.format(i18n.errors.not_a_boolean, tostring(value), t)) | ||
end | end | ||
end | end | ||
function util.cast.number(value, args) | function util.cast.number(value, args) | ||
-- Takes an | -- Takes an arbitrary value and attempts to convert it to a number. | ||
-- | -- | ||
-- args | -- args | ||
-- default | -- default for strings, if default is nil and the conversion fails, an error will be returned | ||
-- min | -- min error if <min | ||
-- max | -- max error if >max | ||
args = args or {} | |||
local t = type(value) | local t = type(value) | ||
local val | local val | ||
if t == 'nil' then | if t == 'nil' then | ||
val = nil | val = nil | ||
Line 64: | Line 111: | ||
val = tonumber(value) | val = tonumber(value) | ||
end | end | ||
if val == nil then | if val == nil then | ||
if args.default ~= nil then | if args.default ~= nil then | ||
val = args.default | val = args.default | ||
else | else | ||
error(string.format( | error(string.format(i18n.errors.not_a_number, tostring(value), t)) | ||
end | end | ||
end | end | ||
if args.min ~= nil and val < args.min then | if args.min ~= nil and val < args.min then | ||
error(string.format( | error(string.format(i18n.errors.number_too_small, val, args.min)) | ||
end | end | ||
if args.max ~= nil and val > args.max then | if args.max ~= nil and val > args.max then | ||
error(string.format( | error(string.format(i18n.errors.number_too_large, val, args.max)) | ||
end | end | ||
return val | return val | ||
end | |||
function util.cast.table(value, args) | |||
-- Takes an arbitrary value and attempts to convert it to a table. | |||
-- | |||
-- args | |||
-- split_args If true, create an association table (rather than an array) | |||
-- pattern The pattern to split strings by. Default: ',%s*' | |||
-- split_args_pattern The pattern to split keys from values by. Ignored if split_args is not true. | |||
-- Default: '%s*=%s*' | |||
-- callback A callback function to call on each value | |||
args = args or {} | |||
local pattern = args.pattern or ',%s*' | |||
local split_args_pattern = args.split_args_pattern or '%s*=%s*' | |||
local tbl | |||
if type(value) == 'string' then | |||
if args.split_args then | |||
tbl = util.string.split_args(value, { sep = pattern, kvsep = split_args_pattern } ) | |||
else | |||
tbl = util.string.split(value, pattern) | |||
end | |||
elseif type(value) ~= 'table' then | |||
tbl = {value} | |||
else | |||
tbl = value | |||
end | |||
if args.callback then | |||
for k, v in ipairs(tbl) do | |||
tbl[k] = args.callback(v) | |||
end | |||
end | |||
return tbl | |||
end | end | ||
Line 87: | Line 166: | ||
-- Takes a string value and returns as version number | -- Takes a string value and returns as version number | ||
-- If the version number is invalid an error is raised | -- If the version number is invalid an error is raised | ||
-- | -- | ||
-- args: | -- args: | ||
-- return_type: defaults to "table" | -- return_type: defaults to "table" | ||
-- table - Returns the version number broken down into sub versions as a table | -- table - Returns the version number broken down into sub versions as a table | ||
-- string - Returns the version number as string | -- string - Returns the version number as string | ||
-- | -- | ||
if args == nil then | if args == nil then | ||
args = {} | args = {} | ||
end | end | ||
local result | local result | ||
if args.return_type == 'table' or args.return_type == nil then | if args.return_type == 'table' or args.return_type == nil then | ||
result = util.string.split(value, '%.') | result = util.string.split(value, '%.') | ||
if #result ~= 3 then | if #result ~= 3 then | ||
error(string.format( | error(string.format(i18n.errors.malformed_version_string, value)) | ||
end | end | ||
result[4] = string.match(result[3], '%a+') | result[4] = string.match(result[3], '%a+') | ||
result[3] = string.match(result[3], '%d+') | result[3] = string.match(result[3], '%d+') | ||
for i=1,3 do | for i=1,3 do | ||
local v = tonumber(result[i]) | local v = tonumber(result[i]) | ||
if v == nil then | if v == nil then | ||
error(string.format( | error(string.format(i18n.errors.non_number_version_component, value)) | ||
end | end | ||
result[i] = v | result[i] = v | ||
Line 118: | Line 197: | ||
result = string.match(value, '%d+%.%d+%.%d+%a*') | result = string.match(value, '%d+%.%d+%.%d+%a*') | ||
end | end | ||
if result == nil then | if result == nil then | ||
error(string.format( | error(string.format(i18n.errors.unrecognized_version_number, value)) | ||
end | end | ||
return result | return result | ||
end | |||
function util.cast.replace_if_match(value, args) | |||
-- Returns a function that returns its input unchanged, unless the string value | |||
-- matches the 'pattern' argument, in which case the 'replacewith' value is returned. | |||
if ((args == nil) or (args.pattern == nil) or (value == nil)) then | |||
return value | |||
elseif string.find(tostring(value),args.pattern) then | |||
return args.replacewith | |||
else | |||
return value | |||
end | |||
end | |||
-- ---------------------------------------------------------------------------- | |||
-- util.validate | |||
-- ---------------------------------------------------------------------------- | |||
util.validate = {} | |||
util.validate.factory = {} | |||
function util.validate.factory.number_in_range(args) | |||
-- Returns a function that validates whether a number is within a range of | |||
-- values. An error is thrown if the value is not a number or if it is not | |||
-- within the specified range. | |||
args = args or {} | |||
args.min = args.min or -math.huge | |||
args.max = args.max or math.huge | |||
return function (value) | |||
if type(value) ~= 'number' then | |||
error(string.format(i18n.errors.not_a_number, tostring(value), type(value))) | |||
end | |||
if value < args.min or value > args.max then | |||
error(string.format(args.errmsg or i18n.errors.number_out_of_range, tostring(value), tostring(args.min), tostring(args.max)), args.errlvl or 2) | |||
end | |||
return value | |||
end | |||
end | |||
function util.validate.factory.string_length(args) | |||
-- Returns a function that validates whether a string has has the correct | |||
-- length. An error is thrown if the value is not a string or if its length | |||
-- restrictions are not met. | |||
args = args or {} | |||
args.min = args.min or 0 | |||
args.max = args.max or math.huge | |||
return function (value) | |||
if type(value) ~= 'string' then | |||
error(string.format(i18n.errors.not_a_string, tostring(value), type(value))) | |||
end | |||
local length = mw.ustring.len(value) | |||
if length < args.min or length > args.max then | |||
error(string.format(args.errmsg or i18n.errors.string_length_incorrect, tostring(value), tostring(args.min), tostring(args.max)), args.errlvl or 2) | |||
end | |||
return value | |||
end | |||
end | |||
function util.validate.factory.in_table(args) | |||
-- Returns a function that validates whether a table contains a value. | |||
-- An error is thrown if the value is not found. | |||
args = args or {} | |||
return function (value) | |||
if not util.table.contains(args.tbl or {}, value) then | |||
error(string.format(args.errmsg or i18n.errors.value_not_in_table, tostring(value)), args.errlvl or 2) | |||
end | |||
return value | |||
end | |||
end | |||
function util.validate.factory.in_table_keys(args) | |||
-- Returns a function that validates whether a table has a value as one of | |||
-- its keys. An error is thrown if the key does not exist. | |||
args = args or {} | |||
return function (value) | |||
if not util.table.has_key(args.tbl or {}, value) then | |||
error(string.format(args.errmsg or i18n.errors.value_not_in_table_keys, tostring(value)), args.errlvl or 2) | |||
end | |||
return value | |||
end | |||
end | |||
-- | |||
-- util.cast.factory | |||
-- | |||
-- This section is used to generate new functions for common argument parsing tasks based on specific options | |||
-- | |||
-- All functions return a function which accepts two arguments: | |||
-- tpl_args - arguments from the template | |||
-- frame - current frame object | |||
-- | |||
-- All factory functions accept have two arguments on creation: | |||
-- k - the key in the tpl_args to retrive the value from | |||
-- args - any addtional arguments (see function for details) | |||
util.cast.factory = {} | |||
function util.cast.factory.table(k, args) | |||
args = args or {} | |||
return function (tpl_args, frame) | |||
args.value = tpl_args[k] | |||
if args.value == nil then | |||
return | |||
end | |||
local value = util.table.find_in_nested_array(args) | |||
if value == nil then | |||
error(string.format(args.errmsg or i18n.errors.missing_element, k)) | |||
end | |||
tpl_args[args.key_out or k] = value | |||
end | |||
end | |||
function util.cast.factory.assoc_table(k, args) | |||
-- Arguments: | |||
-- | |||
-- tbl | |||
-- errmsg | |||
-- key_out | |||
return function (tpl_args, frame) | |||
local elements | |||
if tpl_args[k] ~= nil then | |||
elements = util.string.split(tpl_args[k], ',%s*') | |||
for _, element in ipairs(elements) do | |||
if args.tbl[element] == nil then | |||
error(util.html.error{msg=string.format(args.errmsg or i18n.errors.missing_element, element)}) | |||
end | |||
end | |||
tpl_args[args.key_out or k] = elements | |||
end | |||
end | |||
end | |||
function util.cast.factory.number(k, args) | |||
args = args or {} | |||
return function (tpl_args, frame) | |||
tpl_args[args.key_out or k] = tonumber(tpl_args[k]) | |||
end | |||
end | |||
function util.cast.factory.boolean(k, args) | |||
args = args or {} | |||
return function(tpl_args, frame) | |||
if tpl_args[k] ~= nil then | |||
tpl_args[args.key_out or k] = util.cast.boolean(tpl_args[k]) | |||
end | |||
end | |||
end | |||
function util.cast.factory.percentage(k, args) | |||
args = args or {} | |||
return function (tpl_args, frame) | |||
local v = tonumber(tpl_args[k]) | |||
if v == nil then | |||
return util.html.error{msg=string.format(i18n.errors.invalid_argument, k)} | |||
end | |||
if v < 0 or v > 100 then | |||
return util.html.error{msg=string.format(i18n.errors.not_a_percentage, k)} | |||
end | |||
tpl_args[args.key_out or k] = v | |||
end | |||
end | end | ||
Line 131: | Line 375: | ||
util.args = {} | util.args = {} | ||
function util.args.stats(argtbl, args) | function util.args.stats(argtbl, args) | ||
Line 146: | Line 382: | ||
-- args: | -- args: | ||
-- prefix: prefix if any | -- prefix: prefix if any | ||
-- property_prefix: property prefix if any | -- property_prefix: property prefix if any | ||
-- subobject_prefix: subobject prefix if any | -- subobject_prefix: subobject prefix if any | ||
Line 152: | Line 387: | ||
args = args or {} | args = args or {} | ||
args.prefix = args.prefix or '' | args.prefix = args.prefix or '' | ||
local i = 0 | local i = 0 | ||
local stats = {} | local stats = {} | ||
repeat | repeat | ||
i = i + 1 | i = i + 1 | ||
local prefix = string.format('%s%s%s_%s', args.prefix, i18n.args.stat_infix, i, '%s') | |||
local id = { | local id = { | ||
id = string.format( | id = string.format(prefix, i18n.args.stat_id), | ||
min = string.format( | min = string.format(prefix, i18n.args.stat_min), | ||
max = string.format( | max = string.format(prefix, i18n.args.stat_max), | ||
value = string.format( | value = string.format(prefix, i18n.args.stat_value), | ||
} | } | ||
local value = {} | local value = {} | ||
for key, args_key in pairs(id) do | for key, args_key in pairs(id) do | ||
value[key] = argtbl[args_key] | value[key] = argtbl[args_key] | ||
end | end | ||
if value.id ~= nil and ((value.min ~= nil and value.max ~= nil and value.value == nil) or (value.min == nil and value.max == nil and value.value ~= nil)) then | if value.id ~= nil and ((value.min ~= nil and value.max ~= nil and value.value == nil) or (value.min == nil and value.max == nil and value.value ~= nil)) then | ||
if value.value then | if value.value then | ||
value.value = util.cast.number(value.value) | value.value = util.cast.number(value.value) | ||
Line 183: | Line 415: | ||
value.max = util.cast.number(value.max) | value.max = util.cast.number(value.max) | ||
argtbl[id.max] = value.max | argtbl[id.max] = value.max | ||
-- Also set average value | -- Also set average value | ||
value.avg = (value.min + value.max)/2 | value.avg = (value.min + value.max)/2 | ||
Line 190: | Line 422: | ||
argtbl[string.format('%sstat%s', args.prefix, i)] = value | argtbl[string.format('%sstat%s', args.prefix, i)] = value | ||
stats[#stats+1] = value | stats[#stats+1] = value | ||
elseif util.table.has_all_value(value, {'id', 'min', 'max', 'value'}, nil) then | elseif util.table.has_all_value(value, {'id', 'min', 'max', 'value'}, nil) then | ||
value = nil | value = nil | ||
-- all other cases should be improperly set value | -- all other cases should be improperly set value | ||
else | else | ||
error(string.format( | error(string.format(i18n.errors.improper_stat, args.prefix, i)) | ||
end | end | ||
until value == nil | until value == nil | ||
argtbl[string.format('%sstats', args.prefix)] = stats | |||
end | |||
function util.args.spawn_weight_list(argtbl, args) | |||
args = args or {} | |||
args.input_argument = i18n.args.spawn_weight_prefix | |||
args.output_argument = 'spawn_weights' | |||
args.cargo_table = 'spawn_weights' | |||
argtbl[string.format('% | util.args.weight_list(argtbl, args) | ||
end | |||
function util.args.generation_weight_list(argtbl, args) | |||
args = args or {} | |||
args.input_argument = i18n.args.generation_weight_prefix | |||
args.output_argument = 'generation_weights' | |||
args.cargo_table = 'generation_weights' | |||
util.args.weight_list(argtbl, args) | |||
end | |||
function util.args.weight_list(argtbl, args) | |||
-- Parses a weighted pair of lists and sets properties | |||
-- | |||
-- argtbl: argument table to work with | |||
-- args: | |||
-- output_argument - if set, set arguments to this value | |||
-- input_argument - input prefix for parsing the arguments from the argtbl | |||
-- subobject_name - name of the subobject | |||
m_cargo = m_cargo or require('Module:Cargo') | |||
args = args or {} | |||
args.input_argument = args.input_argument or 'spawn_weight' | |||
local i = 0 | |||
local id = nil | |||
local value = nil | |||
if args.output_argument then | |||
argtbl[args.output_argument] = {} | |||
end | |||
repeat | |||
i = i + 1 | |||
id = { | |||
tag = string.format('%s%s_tag', args.input_argument, i), | |||
value = string.format('%s%s_value', args.input_argument, i), | |||
} | |||
value = { | |||
tag = argtbl[id.tag], | |||
value = argtbl[id.value], | |||
} | |||
if value.tag ~= nil and value.value ~= nil then | |||
if args.output_argument then | |||
argtbl[args.output_argument][i] = value | |||
end | |||
if args.cargo_table then | |||
m_cargo.store({ | |||
_table = args.cargo_table, | |||
ordinal = i, | |||
tag = value.tag, | |||
weight = util.cast.number(value.value, {min=0}), | |||
}) | |||
end | |||
elseif not (value.tag == nil and value.value == nil) then | |||
error(string.format(i18n.errors.invalid_weight, id.tag, id.value)) | |||
end | |||
until value.tag == nil | |||
end | end | ||
function util.args.version (argtbl, args) | function util.args.version(argtbl, args) | ||
-- in any prefix spaces should be included | -- in any prefix spaces should be included | ||
-- | -- | ||
-- argtbl: argument table to work with | -- argtbl: argument table to work with | ||
-- args: | -- args: | ||
-- set_properties: if defined, set properties on the page | -- set_properties: if defined, set properties on the page | ||
-- variables: table of prefixes of | -- variables: table of prefixes | ||
-- | -- ignore_unknowns: if defined, treat a version number of '?' as if it | ||
-- were not present | |||
-- noquery: For testing; if defined, skips the query | |||
-- return_ids_and_keys: For testing; on return, args.version_ids and | |||
-- args.versionkeys are set to the IDs and keys found | |||
args = args or {} | args = args or {} | ||
args.variables = args.variables or { | args.variables = args.variables or { | ||
release = { | release = {}, | ||
removal = {}, | |||
removal = { | |||
} | } | ||
local version_ids = {} | local version_ids={} | ||
local version_keys = {} | local version_keys={} | ||
for key, data in pairs(args.variables) do | for key, data in pairs(args.variables) do | ||
local full_key = string.format('%s_version', key) | local full_key = string.format('%s_version', key) | ||
if argtbl[full_key] ~= nil then | if args.ignore_unknowns and (argtbl[full_key] == '?') then | ||
argtbl[full_key] = nil | |||
elseif argtbl[full_key] ~= nil then | |||
local value = util.cast.version(argtbl[full_key], {return_type = 'string'}) | local value = util.cast.version(argtbl[full_key], {return_type = 'string'}) | ||
argtbl[full_key] = value | argtbl[full_key] = value | ||
data.value = value | if value ~= nil then | ||
data.value = value | |||
if data.property ~= nil then | |||
version_ids[#version_ids+1] = value | |||
version_keys[value] = key | |||
end | |||
end | end | ||
end | end | ||
end | end | ||
-- no need to do a query if nothing was fetched | -- no need to do a query if nothing was fetched | ||
if #version_ids > 0 then | if (args.noquery == nil) and (#version_ids > 0) then | ||
for i, id in ipairs(version_ids) do | |||
version_ids[i] = string.format('Versions.version="%s"', id) | |||
end | end | ||
local query | local results = m_cargo.query( | ||
{'Versions'}, | |||
{'release_date', 'version'}, | |||
{ | |||
where = table.concat(version_ids, ' OR '), | |||
} | |||
) | |||
if #results ~= #version_ids then | if #results ~= #version_ids then | ||
error(string.format( | error(string.format(i18n.too_many_versions, #results, #version_ids)) | ||
end | end | ||
for _, row in ipairs(results) do | for _, row in ipairs(results) do | ||
local key = version_keys[row | local key = version_keys[row.version] | ||
argtbl[string.format('%s_date', key)] = row | argtbl[string.format('%s_date', key)] = row.release_date | ||
end | end | ||
end | end | ||
if args.return_ids_and_keys ~= nil then | |||
args.version_ids = version_ids | |||
args.version_keys = version_keys | |||
end | |||
end | |||
function util.args.from_cargo_map(args) | |||
m_cargo = m_cargo or require('Module:Cargo') | |||
return m_cargo.store_mapped_args(args) | |||
end | |||
function util.args.template_to_lua(str) | |||
--[[ | |||
Convert templates to lua format. Simplifes debugging and creating | |||
examples. | |||
Parameters | |||
---------- | |||
str : string | |||
The entire template wrapped into string. Tip: Use Lua's square | |||
bracket syntax for defining string literals. | |||
Returns | |||
------- | |||
out : table | |||
out.template - Template name. | |||
out.args - arguments in table format. | |||
out.args_to_str - arguments in readable string format. | |||
]] | |||
local out = {} | |||
-- Get the template name: | |||
out.template = string.match(str, '{{(.-)%s*|') | |||
-- Remove everything but the arguments: | |||
str = string.gsub(str, '%s*{{.-|', '') | |||
str = string.gsub(str, '%s*}}%s*', '') | |||
-- Split up the arguments: | |||
out.args = {} | |||
for i, v in ipairs(util.string.split(str, '%s*|%s*')) do | |||
local arg = util.string.split(v, '%s*=%s*') | |||
out.args[arg[1]] = arg[2] | |||
out.args[#out.args+1] = arg[1] | |||
end | |||
-- Concate for easy copy/pasting: | |||
local tbl = {} | |||
for i, v in ipairs(out.args) do | |||
tbl[#tbl+1]= string.format("%s='%s'", v, out.args[v]) | |||
end | |||
out.args_to_str = table.concat(tbl, ',\n') | |||
return out | |||
end | end | ||
Line 290: | Line 627: | ||
util.html = {} | util.html = {} | ||
function util.html.abbr( | |||
return string. | function util.html.abbr(text, title, options) | ||
-- Outputs html tag <abbr> as string or as mw.html node. | |||
-- | |||
-- options | |||
-- class: class attribute | |||
-- output: set to mw.html to return a mw.html node instead of a string | |||
if not title then | |||
return text | |||
end | |||
options = options or {} | |||
local abbr = mw.html.create('abbr') | |||
abbr:attr('title', title) | |||
local class | |||
if type(options) == 'table' and options.class then | |||
class = options.class | |||
else | |||
class = options | |||
end | |||
if type(class) == 'string' then | |||
abbr:attr('class', class) | |||
end | |||
abbr:wikitext(text) | |||
if options.output == mw.html then | |||
return abbr | |||
end | |||
return tostring(abbr) | |||
end | end | ||
Line 297: | Line 659: | ||
-- Create an error message box | -- Create an error message box | ||
-- | -- | ||
-- | -- args | ||
-- | -- msg str The error message | ||
args = args or {} | |||
local err = mw.html.create('strong') | |||
:addClass('error') | |||
:tag('span') | |||
local err = mw.html.create(' | :addClass('module-error') | ||
:wikitext(i18n.errors.module_error .. (args.msg or '')) | |||
: | :done() | ||
return tostring(err) | return tostring(err) | ||
end | end | ||
function util.html.poe_color(label, text) | function util.html.poe_color(label, text, class) | ||
if text == nil or text == '' then | if text == nil or text == '' then | ||
return nil | return nil | ||
end | end | ||
local em = mw.html.create('em') | |||
: | :addClass('tc -' .. label) | ||
:wikitext(text)) | :addClass(class or '') | ||
:wikitext(text) | |||
return tostring(em) | |||
end | end | ||
util.html.poe_colour = util.html.poe_color | |||
util.html. | function util.html.tooltip(abbr, text, class) | ||
function util.html. | return string.format('<span class="hoverbox c-tooltip %s"><span class="hoverbox__activator c-tooltip__activator">%s</span><span class="hoverbox__display c-tooltip__display">%s</span></span>', class or '', abbr or '', text or '') | ||
-- | end | ||
function util.html.table_cell(type) | |||
-- Table cells | |||
-- type: 'yes', 'no', 'na' | |||
-- Returns mw.html node | |||
local td = mw.html.create('td') | local td = mw.html.create('td') | ||
td | td | ||
:attr('class', ' | :attr('data-sort-value', i18n.table_cells[type].sort) | ||
:addClass(i18n.table_cells[type].class) | |||
: | :wikitext(i18n.table_cells[type].text) | ||
return | return td | ||
end | |||
function util.html.format_value(tpl_args, value, options) | |||
-- value: table | |||
-- min: | |||
-- max: | |||
-- options: table | |||
-- func: Function to transform the value retrieved from the database | |||
-- fmt: Format string (or function that returns format string) to use for the value. | |||
-- Default: '%s' | |||
-- fmt_range: Format string to use for range value. | |||
-- Default: '(%s-%s)' | |||
-- color: poe_color code to use for the value. False for no color. | |||
-- Default: 'value' if value is unmodified; 'mod' if modified | |||
-- class: Additional css class added to color tag | |||
-- inline: Format string to use for the output | |||
-- inline_color: poe_color code to use for the output. False for no color. | |||
-- Default: Inherits from value color | |||
-- inline_class: Additional css class added to inline color tag | |||
-- no_color: (Deprecated; use color=false instead) | |||
-- return_color: (Deprecated; returns both value.out and value without this) | |||
-- Make shallow copy to avoid modifying the original table | |||
local value_copy = {} | |||
for k, v in pairs(value) do | |||
value_copy[k] = v | |||
end | |||
local default_color = 'value' | |||
local base = { | |||
min = value_copy.base_min or value_copy.base, | |||
max = value_copy.base_max or value_copy.base, | |||
} | |||
if value_copy.min ~= base.min or value_copy.max ~= base.max then | |||
default_color = 'mod' | |||
end | |||
if options.color ~= false and options.no_color == nil then | |||
value_copy.color = options.color or default_color | |||
end | |||
if options.func then | |||
value_copy.min = options.func(tpl_args, value_copy.min) | |||
value_copy.max = options.func(tpl_args, value_copy.max) | |||
end | |||
local fmt = options.fmt or '%s' | |||
if type(fmt) == 'function' then -- Function that returns the format string | |||
fmt = fmt(tpl_args, value_copy) | |||
end | |||
if value_copy.min == value_copy.max then -- Static value | |||
value_copy.out = string.format(fmt, value_copy.min) | |||
else -- Range value | |||
local fmt_range = options.fmt_range or i18n.range | |||
value_copy.out = string.format( | |||
string.format(fmt_range, fmt, fmt), | |||
value_copy.min, | |||
value_copy.max | |||
) | |||
end | |||
local inline = options.inline | |||
if type(inline) == 'function' then | |||
inline = inline(tpl_args, value_copy) | |||
end | |||
inline = inline ~= '' and inline or nil -- TODO: Eliminate the need for this? | |||
local inline_color = options.inline_color | |||
if value_copy.color and (not inline or inline_color ~= nil) then | |||
value_copy.out = util.html.poe_color(value_copy.color, value_copy.out, options.class) | |||
end | |||
if inline then | |||
value_copy.out = string.format(inline, value_copy.out) | |||
if inline_color or inline_color == nil and options.color ~= false then | |||
inline_color = inline_color or value_copy.color or default_color | |||
value_copy.out = util.html.poe_color(inline_color, value_copy.out, options.inline_class) | |||
end | |||
end | |||
if options.return_color then | |||
return value_copy.out, value_copy.color | |||
end | |||
return value_copy.out, value_copy | |||
end | |||
function util.html.wikilink(page, text) | |||
if text then | |||
return string.format('[[%s|%s]]', page, text) | |||
end | |||
return string.format('[[%s]]', page) | |||
end | |||
function util.html.url_link(url, text) | |||
return string.format('[%s %s]', url, text) | |||
end | end | ||
Line 337: | Line 791: | ||
util.misc = {} | util.misc = {} | ||
function util.misc.invoker_factory(func, options) | |||
-- Returns a function that can be called directly or with #invoke. | |||
return function (frame) | |||
frame = frame or {} | |||
local args | |||
if type(frame.args) == 'table' then | |||
-- Called via #invoke, so use getArgs(). | |||
getArgs = getArgs or require('Module:Arguments').getArgs | |||
args = getArgs(frame, options) | |||
else | |||
-- Called from another module or from the debug console, so assume args | |||
-- are passed in directly. | |||
args = frame | |||
end | |||
return func(args) | |||
end | |||
end | |||
function util.misc.is_frame(frame) | function util.misc.is_frame(frame) | ||
-- the type of the frame is a table containing the functions, so check whether some of these exist | -- the type of the frame is a table containing the functions, so check whether some of these exist | ||
Line 344: | Line 817: | ||
function util.misc.get_frame(frame) | function util.misc.get_frame(frame) | ||
-- OBSOLETE. Use mw.getCurrentFrame() instead. | |||
return mw.getCurrentFrame() | |||
end | |||
function util.misc.get_args_raw(frame) | |||
-- Simple method for getting arguments. Use this instead of Module:Arguments | |||
-- when the extra options provided by the latter would be overkill. | |||
if util.misc.is_frame(frame) then | if util.misc.is_frame(frame) then | ||
return frame | -- Called via {{#invoke:}}, so use the args that were passed into the | ||
-- template. | |||
return frame.args | |||
end | end | ||
return | -- Called from another module or from the debug console, so assume args | ||
-- are passed in directly. | |||
return frame | |||
end | end | ||
util.misc. | function util.misc.maybe_sandbox(module_name) | ||
-- Did we load or {{#invoke:}} a module sandbox? | |||
if module_name and package.loaded[string.format('Module:%s/sandbox', module_name)] ~= nil or string.find(mw.getCurrentFrame():getTitle(), 'sandbox', 1, true) then | |||
return true | |||
end | |||
return false | |||
end | |||
function util.misc.add_category(categories, args) | function util.misc.add_category(categories, args) | ||
Line 369: | Line 846: | ||
-- args: table of extra arguments | -- args: table of extra arguments | ||
-- namespace: id of namespace to validate against | -- namespace: id of namespace to validate against | ||
-- | -- ignore_blacklist: set to non-nil to ignore the blacklist | ||
-- sub_page_blacklist: blacklist of subpages to use (if empty, use default) | -- sub_page_blacklist: blacklist of subpages to use (if empty, use default) | ||
-- namespace_blacklist: blacklist of namespaces to use (if empty, use default) | -- namespace_blacklist: blacklist of namespaces to use (if empty, use default) | ||
Line 375: | Line 852: | ||
categories = {categories} | categories = {categories} | ||
end | end | ||
if args == nil then | if args == nil then | ||
args = {} | args = {} | ||
end | end | ||
local title = mw.title.getCurrentTitle() | local title = mw.title.getCurrentTitle() | ||
local sub_blacklist = args.sub_page_blacklist or | local sub_blacklist = args.sub_page_blacklist or cfg.misc.category_blacklist.sub_pages | ||
local ns_blacklist = args.namespace_blacklist or | local ns_blacklist = args.namespace_blacklist or cfg.misc.category_blacklist.namespaces | ||
if args.namespace ~= nil and title.namespace ~= args.namespace then | if args.namespace ~= nil and title.namespace ~= args.namespace then | ||
return '' | return '' | ||
end | end | ||
if args. | if args.ignore_blacklist == nil and (sub_blacklist[title.subpageText] or ns_blacklist[title.subjectNsText]) then | ||
return '' | return '' | ||
end | end | ||
local cats = {} | local cats = {} | ||
for i, cat in ipairs(categories) do | for i, cat in ipairs(categories) do | ||
cats[i] = string.format('[[Category:%s]]', cat) | cats[i] = string.format('[[Category:%s]]', cat) | ||
Line 401: | Line 877: | ||
end | end | ||
-- ---------------------------------------------------------------------------- | |||
-- util.Error | |||
-- ---------------------------------------------------------------------------- | |||
-- Prototype error object | |||
local Error_prototype = { | |||
message = i18n.errors.unspecified, | |||
code = 'module_error', | |||
issue = true, -- Whether to issue error | |||
level = 2, | |||
} | |||
Error_prototype.__index = Error_prototype | |||
function Error_prototype:throw(force) | |||
if force or self.issue then | |||
error(self.message, self.level) | |||
error( | |||
end | end | ||
return self | |||
end | |||
function Error_prototype:get_html() | |||
return util.html.error{msg=self.message} | |||
end | |||
function Error_prototype:get_category(args) | |||
return util.misc.add_category(self.category, args) | |||
end | |||
function util.Error(obj) | |||
-- Create a new error object | |||
obj = obj or {} | |||
setmetatable(obj, Error_prototype) | |||
return obj | |||
end | end | ||
-- ---------------------------------------------------------------------------- | -- ---------------------------------------------------------------------------- | ||
-- util. | -- util.string | ||
-- ---------------------------------------------------------------------------- | -- ---------------------------------------------------------------------------- | ||
util. | util.string = {} | ||
util. | function util.string.trim(str, charset) | ||
--[[ | |||
Trims leading and trailing characters in charset from a string. | |||
Charset is '%s' by default, which matches whitespace characters | |||
This works much like mw.text.trim, using the string library instead | |||
of the ustring library. This function may return erroneous results | |||
if the charset needs to be Unicode-aware. | |||
--]] | |||
charset = charset or '%s' | |||
-- | str = string.gsub(str, '^[' .. charset .. ']*(.-)[' .. charset .. ']*$', '%1') | ||
return str | |||
end | end | ||
function util. | function util.string.strip_wikilinks(str) | ||
--[[ | |||
Removes wikilinks from a string, leaving the plain text | |||
--]] | |||
str = mw.ustring.gsub(str, '%[%[:?([^%]|]+)%]%]', '%1') | |||
str = mw.ustring.gsub(str, '%[%[:?[^|]+|([^%]|]+)%]%]', '%1') | |||
return str | |||
end | end | ||
function util. | function util.string.strip_html(str) | ||
--[[ | |||
Removes html tags from a string, leaving the plain text | |||
--]] | |||
str = mw.ustring.gsub(str, '<[^>]*>', '') | |||
return str | |||
end | end | ||
function util. | function util.string.split(str, pattern, plain) | ||
-- | --[[ | ||
Splits a string into a table | |||
This does essentially the same thing as mw.text.split, but with | |||
significantly better performance. This function may return erroneous | |||
results if the pattern needs to be Unicode-aware. | |||
str String to split | |||
pattern Pattern to use for splitting | |||
plain If true, pattern is interpreted as a literal string | |||
--]] | |||
local out = {} | local out = {} | ||
local init = 1 | |||
local split_start, split_end = string.find(str, pattern, init, plain) | |||
while split_start do | |||
out[#out+1] = string.sub(str, init, split_start-1) | |||
init = split_end+1 | |||
split_start, split_end = string.find(str, pattern, init, plain) | |||
end | end | ||
out[#out+1] = string.sub(str, init) | |||
return out | return out | ||
end | end | ||
function util. | function util.string.split_outer(str, pattern, outer) | ||
-- | --[[ | ||
Split a string into a table according to the pattern, ignoring | |||
matching patterns inside the outer patterns. | |||
if | Parameters | ||
---------- | |||
str : string | |||
String to split. | |||
pattern : string | |||
Pattern to split on. | |||
outer : table of strings where #outer = 2. | |||
Table with 2 strings that defines the opening and closing patterns | |||
to match, for example parantheses or brackets. | |||
Returns | |||
------- | |||
out : table | |||
table of split strings. | |||
Examples | |||
-------- | |||
-- Nesting at the end: | |||
str = 'mods.id, CONCAT(mods.id, mods.name)' | |||
mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'})) | |||
table#1 { | |||
"mods.id", | |||
"CONCAT(mods.id, mods.name)", | |||
} | |||
-- Nesting in the middle: | |||
str = 'mods.id, CONCAT(mods.id, mods.name), mods.required_level' | |||
mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'})) | |||
table#1 { | |||
"mods.id", | |||
"CONCAT(mods.id, mods.name)", | |||
"mods.required_level", | |||
} | |||
]] | |||
local out = {} | |||
local nesting_level = 0 | |||
local i = 0 | |||
local pttrn = '(.-)' .. '(' .. pattern .. ')' | |||
for v, sep in string.gmatch(str, pttrn) do | |||
if nesting_level == 0 then | |||
-- No nesting is occuring: | |||
out[#out+1] = v | |||
else | |||
-- Nesting is occuring: | |||
out[#out] = (out[math.max(#out, 1)] or '') .. v | |||
end | |||
-- Increase nesting level: | |||
if string.find(v, outer[1]) then -- Multiple matches? | |||
nesting_level = nesting_level + 1 | |||
end | |||
if string.find(v, outer[2]) then | |||
nesting_level = nesting_level - 1 | |||
end | |||
-- Add back the separator if nesting is occuring: | |||
if nesting_level ~= 0 then | |||
out[#out] = out[#out] .. sep | |||
end | |||
-- Get the last index value: | |||
i = i + #v + #sep | |||
end | end | ||
-- Complement with the last part of the string: | |||
if nesting_level == 0 then | |||
out[#out+1] = string.sub(str, math.max(i+1, 1)) | |||
else | |||
out[#out] = out[#out] .. string.sub(str, math.max(i+1, 1)) | |||
-- TODO: Check if nesting level is zero? | |||
end | end | ||
return out | return out | ||
end | end | ||
Line 561: | Line 1,060: | ||
-- kvsep: separator to use for key value pairs (default: =) | -- kvsep: separator to use for key value pairs (default: =) | ||
local out = {} | local out = {} | ||
if args == nil then | if args == nil then | ||
args = {} | args = {} | ||
end | end | ||
args.sep = args.sep or ',' | args.sep = args.sep or ',' | ||
args.kvsep = args.kvsep or '=' | args.kvsep = args.kvsep or '=' | ||
if str ~= nil then | if str ~= nil then | ||
local row | local row | ||
Line 574: | Line 1,073: | ||
row = util.string.split(str, args.kvsep) | row = util.string.split(str, args.kvsep) | ||
if #row == 1 then | if #row == 1 then | ||
out[#out+1] = row[1] | out[#out+1] = row[1] | ||
elseif #row == 2 then | elseif #row == 2 then | ||
out[row[1]] = row[2] | out[row[1]] = row[2] | ||
else | else | ||
error(string.format( | error(string.format(i18n.number_of_arguments_too_large, #row)) | ||
end | end | ||
end | end | ||
end | end | ||
return out | return out | ||
end | |||
function util.string.format(format, ...) | |||
--[[ | |||
String replacement with support for numbered argument conversion | |||
specifications. This is useful for i18n, as translating can sometimes | |||
change the order of words around. | |||
The format can contain either numbered argument conversion specifications | |||
(i.e., "%n$"), or unnumbered argument conversion specifications (i.e., "%"), | |||
but not both. | |||
If numbered argument conversion specifications are not needed, consider | |||
using string.format() from the Lua string library instead. | |||
Example: | |||
local format = 'Bubba ate %2$d %1$s. That\'s a lot of %1$s!' | |||
util.string.format(format, 'hotdogs', 26) | |||
-> Bubba ate 26 hotdogs. That's a lot of hotdogs! | |||
]] | |||
local values = {} | |||
for v in string.gmatch(format, '%%(%d+)%$') do | |||
values[#values+1] = select(v, ...) | |||
end | |||
if #values == 0 then | |||
-- Using unnumbered argument conversion specifications, so just pass | |||
-- args to string.format(). | |||
return string.format(format, ...) | |||
end | |||
format = string.gsub(format, '%%%d+%$', '%%') | |||
return string.format(format, unpack(values)) | |||
end | |||
function util.string.first_to_upper(str) | |||
--[[ | |||
Converts the first letter of a string to uppercase | |||
--]] | |||
-- Reassign to variable before returning since string.gsub returns two values | |||
str = str:gsub('^%l', string.upper) | |||
return str | |||
end | |||
util.string.pattern = {} | |||
function util.string.pattern.valid_var_name() | |||
--[[ | |||
Get a pattern for a valid variable name. | |||
]] | |||
return '%A?([%a_]+[%w_]*)[^%w_]?' | |||
end | end | ||
Line 591: | Line 1,140: | ||
util.table = {} | util.table = {} | ||
function util.table.length(tbl) | |||
-- Get number of elements in a table. Counts both numerically indexed | |||
-- elements and associative elements. Does not count nil elements. | |||
local count = 0 | |||
for _ in pairs(tbl) do | |||
count = count + 1 | |||
end | |||
return count | |||
end | |||
util.table.count = util.table.length | |||
function util.table.contains(tbl, value) | |||
-- Checks whether a table contains a value | |||
for _, v in pairs(tbl) do | |||
if v == value then | |||
return true | |||
end | |||
end | |||
return false | |||
end | |||
function util.table.has_key(tbl, key) | |||
-- Checks whether a table has a key | |||
return tbl[key] ~= nil | |||
end | |||
function util.table.has_any_key(tbl, keys) | |||
-- Checks whether a table has at least one of the keys | |||
for _, key in ipairs(keys or {}) do | |||
if tbl[key] ~= nil then | |||
return true | |||
end | |||
end | |||
return false | |||
end | |||
function util.table.has_all_keys(tbl, keys) | |||
-- Checks whether a table has all of the keys | |||
for _, key in ipairs(keys or {}) do | |||
if tbl[key] == nil then | |||
return false | |||
end | |||
end | |||
return true | |||
end | |||
function util.table.keys(tbl) | |||
-- Returns the keys of a table | |||
local keys = {} | |||
for k, _ in pairs(tbl) do | |||
keys[#keys+1] = k | |||
end | |||
return keys | |||
end | |||
util.table.assoc_to_array = util.table.keys | |||
function util.table.column(tbl, colkey, idxkey) | |||
--[[ | |||
Returns the values of one column of a multi-dimensional table | |||
tbl A multi-dimensional table | |||
colkey The column key from the inner tables | |||
idxkey If provided, the column from the inner tables to index the | |||
returned values by. Default: nil | |||
--]] | |||
local col = {} | |||
for _, row in pairs(tbl) do | |||
if type(row) == 'table' and row[colkey] ~= nil then | |||
if idxkey ~= nil and row[idxkey] ~= nil then | |||
col[row[idxkey]] = row[colkey] | |||
else | |||
col[#col+1] = row[colkey] | |||
end | |||
end | |||
end | |||
return col | |||
end | |||
function util.table.merge(...) | |||
--[[ | |||
Merges the keys and values of multiple tables into a single table. If | |||
the input tables share non-numerical keys, then the later values for those | |||
keys will overwrite the previous ones. Numerical keys are instead appended | |||
and renumbered, incrementing from 1. | |||
--]] | |||
local tbl = {} | |||
for _, t in ipairs({...}) do | |||
for k, v in pairs(t) do | |||
if type(k) == 'number' then | |||
table.insert(tbl, v) | |||
else | |||
tbl[k] = v | |||
end | |||
end | |||
end | |||
return tbl | |||
end | |||
function util.table.diff(tbl1, tbl2) | |||
-- Finds the difference between two tables, returning a table containing the | |||
-- values in tbl1 that are not in tbl2. Indexing is ignored; only values are | |||
-- compared. | |||
local diff = {} | |||
for _, k in pairs(tbl1) do | |||
if not util.table.contains(tbl2, k) then | |||
table.insert(diff, k) | |||
end | |||
end | |||
return diff | |||
end | |||
function util.table.remove_duplicates(tbl) | |||
-- Removes duplicate values from an array. | |||
local new_tbl = {} | |||
local exists = {} | |||
for _, v in ipairs(tbl) do | |||
if not exists[v] then | |||
new_tbl[#new_tbl+1] = v | |||
exists[v] = true | |||
end | |||
end | |||
return new_tbl | |||
end | |||
function util.table.has_all_value(tbl, keys, value) | function util.table.has_all_value(tbl, keys, value) | ||
-- Whether all the table values with the specified keys are the specified value | -- Whether all the table values with the specified keys are the specified value | ||
Line 611: | Line 1,285: | ||
end | end | ||
function util.table.find_in_nested_array(args) | function util.table.find_in_nested_array(args) | ||
-- Iterates thoguh the given nested array and finds the given value | -- Iterates thoguh the given nested array and finds the given value | ||
-- | -- | ||
-- ex. | -- ex. | ||
-- data = { | -- data = { | ||
-- {a=5}, {a=6}} | -- {a=5}, {a=6}} | ||
Line 620: | Line 1,294: | ||
-- find_nested_array(arg=10, tbl=data, key='a'} -> nil | -- find_nested_array(arg=10, tbl=data, key='a'} -> nil | ||
-- -> returns "6" | -- -> returns "6" | ||
-- | -- | ||
-- args: Table containing: | -- args: Table containing: | ||
Line 628: | Line 1,302: | ||
-- rtrkey: if key is table, return this key instead of the value instead | -- rtrkey: if key is table, return this key instead of the value instead | ||
-- rtrvalue: default: true | -- rtrvalue: default: true | ||
local rtr | local rtr | ||
if type(args.key) == 'table' then | if type(args.key) == 'table' then | ||
for _, item in ipairs(args.tbl) do | for _, item in ipairs(args.tbl) do | ||
Line 655: | Line 1,329: | ||
end | end | ||
end | end | ||
if rtr == nil then | if rtr == nil then | ||
return rtr | return rtr | ||
end | end | ||
if args.rtrkey ~= nil then | if args.rtrkey ~= nil then | ||
return rtr[args.rtrkey] | return rtr[args.rtrkey] | ||
elseif args.rtrvalue or args.rtrvalue == nil then | elseif args.rtrvalue or args.rtrvalue == nil then |
Latest revision as of 20:20, 25 October 2024
This is a meta module.
This module is meant to be used only by other modules. It should not be invoked in wikitext.
Overview
Provides utility functions for programming modules.
Structure
Group | Description |
---|---|
util.cast | utilities for casting values (i.e. from arguments) |
util.html | shorthand functions for creating some html tags |
util.misc | miscellaneous functions |
Usage
This module should be loaded with require()
.
The above documentation is transcluded from Module:Util/doc.
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
-------------------------------------------------------------------------------
--
-- Module:Util
--
-- This meta module contains a number of utility functions
-------------------------------------------------------------------------------
local getArgs -- Lazy load require('Module:Arguments').getArgs
local m_cargo -- Lazy load require('Module:Cargo')
-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = mw.loadData('Module:Util/config')
local i18n = cfg.i18n
local util = {}
-- ----------------------------------------------------------------------------
-- util.cast
-- ----------------------------------------------------------------------------
util.cast = {}
function util.cast.text(value, args)
-- Takes an arbitary value and converts it to text.
--
-- Also strips any categories
--
-- args
-- cast_nil - Cast lua nil value to "nil" string
-- Default: false
-- discard_empty - if the string is empty, return nil rather then empty string
-- Default: true
args = args or {}
if args.discard_empty == nil then
args.discard_empty = true
end
if value == nil and not args.cast_nil then
return
end
value = tostring(value)
if value == '' and args.discard_empty then
return
end
-- Reassign to variable before returning since string.gsub returns two values
value = string.gsub(value, '%[%[Category:[%w_ ]+%]%]', '')
return value
end
function util.cast.boolean(value, args)
-- Takes an arbitrary value and attempts to convert it to a boolean.
--
-- for strings false will be according to i18n.bool_false
--
-- args
-- cast_nil if set to false, it will not cast nil values
args = args or {}
local t = type(value)
if t == 'nil' then
if args.cast_nil == nil or args.cast_nil == true then
return false
else
return
end
elseif t == 'boolean' then
return value
elseif t == 'number' then
if value == 0 then return false end
return true
elseif t == 'string' then
local tmp = string.lower(value)
for _, v in ipairs(i18n.bool_false) do
if v == tmp then
return false
end
end
return true
else
error(string.format(i18n.errors.not_a_boolean, tostring(value), t))
end
end
function util.cast.number(value, args)
-- Takes an arbitrary value and attempts to convert it to a number.
--
-- args
-- default for strings, if default is nil and the conversion fails, an error will be returned
-- min error if <min
-- max error if >max
args = args or {}
local t = type(value)
local val
if t == 'nil' then
val = nil
elseif t == 'boolean' then
if value then
val = 1
else
val = 0
end
elseif t == 'number' then
val = value
elseif t == 'string' then
val = tonumber(value)
end
if val == nil then
if args.default ~= nil then
val = args.default
else
error(string.format(i18n.errors.not_a_number, tostring(value), t))
end
end
if args.min ~= nil and val < args.min then
error(string.format(i18n.errors.number_too_small, val, args.min))
end
if args.max ~= nil and val > args.max then
error(string.format(i18n.errors.number_too_large, val, args.max))
end
return val
end
function util.cast.table(value, args)
-- Takes an arbitrary value and attempts to convert it to a table.
--
-- args
-- split_args If true, create an association table (rather than an array)
-- pattern The pattern to split strings by. Default: ',%s*'
-- split_args_pattern The pattern to split keys from values by. Ignored if split_args is not true.
-- Default: '%s*=%s*'
-- callback A callback function to call on each value
args = args or {}
local pattern = args.pattern or ',%s*'
local split_args_pattern = args.split_args_pattern or '%s*=%s*'
local tbl
if type(value) == 'string' then
if args.split_args then
tbl = util.string.split_args(value, { sep = pattern, kvsep = split_args_pattern } )
else
tbl = util.string.split(value, pattern)
end
elseif type(value) ~= 'table' then
tbl = {value}
else
tbl = value
end
if args.callback then
for k, v in ipairs(tbl) do
tbl[k] = args.callback(v)
end
end
return tbl
end
function util.cast.version(value, args)
-- Takes a string value and returns as version number
-- If the version number is invalid an error is raised
--
-- args:
-- return_type: defaults to "table"
-- table - Returns the version number broken down into sub versions as a table
-- string - Returns the version number as string
--
if args == nil then
args = {}
end
local result
if args.return_type == 'table' or args.return_type == nil then
result = util.string.split(value, '%.')
if #result ~= 3 then
error(string.format(i18n.errors.malformed_version_string, value))
end
result[4] = string.match(result[3], '%a+')
result[3] = string.match(result[3], '%d+')
for i=1,3 do
local v = tonumber(result[i])
if v == nil then
error(string.format(i18n.errors.non_number_version_component, value))
end
result[i] = v
end
elseif args.return_type == 'string' then
result = string.match(value, '%d+%.%d+%.%d+%a*')
end
if result == nil then
error(string.format(i18n.errors.unrecognized_version_number, value))
end
return result
end
function util.cast.replace_if_match(value, args)
-- Returns a function that returns its input unchanged, unless the string value
-- matches the 'pattern' argument, in which case the 'replacewith' value is returned.
if ((args == nil) or (args.pattern == nil) or (value == nil)) then
return value
elseif string.find(tostring(value),args.pattern) then
return args.replacewith
else
return value
end
end
-- ----------------------------------------------------------------------------
-- util.validate
-- ----------------------------------------------------------------------------
util.validate = {}
util.validate.factory = {}
function util.validate.factory.number_in_range(args)
-- Returns a function that validates whether a number is within a range of
-- values. An error is thrown if the value is not a number or if it is not
-- within the specified range.
args = args or {}
args.min = args.min or -math.huge
args.max = args.max or math.huge
return function (value)
if type(value) ~= 'number' then
error(string.format(i18n.errors.not_a_number, tostring(value), type(value)))
end
if value < args.min or value > args.max then
error(string.format(args.errmsg or i18n.errors.number_out_of_range, tostring(value), tostring(args.min), tostring(args.max)), args.errlvl or 2)
end
return value
end
end
function util.validate.factory.string_length(args)
-- Returns a function that validates whether a string has has the correct
-- length. An error is thrown if the value is not a string or if its length
-- restrictions are not met.
args = args or {}
args.min = args.min or 0
args.max = args.max or math.huge
return function (value)
if type(value) ~= 'string' then
error(string.format(i18n.errors.not_a_string, tostring(value), type(value)))
end
local length = mw.ustring.len(value)
if length < args.min or length > args.max then
error(string.format(args.errmsg or i18n.errors.string_length_incorrect, tostring(value), tostring(args.min), tostring(args.max)), args.errlvl or 2)
end
return value
end
end
function util.validate.factory.in_table(args)
-- Returns a function that validates whether a table contains a value.
-- An error is thrown if the value is not found.
args = args or {}
return function (value)
if not util.table.contains(args.tbl or {}, value) then
error(string.format(args.errmsg or i18n.errors.value_not_in_table, tostring(value)), args.errlvl or 2)
end
return value
end
end
function util.validate.factory.in_table_keys(args)
-- Returns a function that validates whether a table has a value as one of
-- its keys. An error is thrown if the key does not exist.
args = args or {}
return function (value)
if not util.table.has_key(args.tbl or {}, value) then
error(string.format(args.errmsg or i18n.errors.value_not_in_table_keys, tostring(value)), args.errlvl or 2)
end
return value
end
end
--
-- util.cast.factory
--
-- This section is used to generate new functions for common argument parsing tasks based on specific options
--
-- All functions return a function which accepts two arguments:
-- tpl_args - arguments from the template
-- frame - current frame object
--
-- All factory functions accept have two arguments on creation:
-- k - the key in the tpl_args to retrive the value from
-- args - any addtional arguments (see function for details)
util.cast.factory = {}
function util.cast.factory.table(k, args)
args = args or {}
return function (tpl_args, frame)
args.value = tpl_args[k]
if args.value == nil then
return
end
local value = util.table.find_in_nested_array(args)
if value == nil then
error(string.format(args.errmsg or i18n.errors.missing_element, k))
end
tpl_args[args.key_out or k] = value
end
end
function util.cast.factory.assoc_table(k, args)
-- Arguments:
--
-- tbl
-- errmsg
-- key_out
return function (tpl_args, frame)
local elements
if tpl_args[k] ~= nil then
elements = util.string.split(tpl_args[k], ',%s*')
for _, element in ipairs(elements) do
if args.tbl[element] == nil then
error(util.html.error{msg=string.format(args.errmsg or i18n.errors.missing_element, element)})
end
end
tpl_args[args.key_out or k] = elements
end
end
end
function util.cast.factory.number(k, args)
args = args or {}
return function (tpl_args, frame)
tpl_args[args.key_out or k] = tonumber(tpl_args[k])
end
end
function util.cast.factory.boolean(k, args)
args = args or {}
return function(tpl_args, frame)
if tpl_args[k] ~= nil then
tpl_args[args.key_out or k] = util.cast.boolean(tpl_args[k])
end
end
end
function util.cast.factory.percentage(k, args)
args = args or {}
return function (tpl_args, frame)
local v = tonumber(tpl_args[k])
if v == nil then
return util.html.error{msg=string.format(i18n.errors.invalid_argument, k)}
end
if v < 0 or v > 100 then
return util.html.error{msg=string.format(i18n.errors.not_a_percentage, k)}
end
tpl_args[args.key_out or k] = v
end
end
-- ----------------------------------------------------------------------------
-- util.args
-- ----------------------------------------------------------------------------
util.args = {}
function util.args.stats(argtbl, args)
-- in any prefix spaces should be included
--
-- argtbl: argument table to work with
-- args:
-- prefix: prefix if any
-- property_prefix: property prefix if any
-- subobject_prefix: subobject prefix if any
-- properties: table of properties to add if any
args = args or {}
args.prefix = args.prefix or ''
local i = 0
local stats = {}
repeat
i = i + 1
local prefix = string.format('%s%s%s_%s', args.prefix, i18n.args.stat_infix, i, '%s')
local id = {
id = string.format(prefix, i18n.args.stat_id),
min = string.format(prefix, i18n.args.stat_min),
max = string.format(prefix, i18n.args.stat_max),
value = string.format(prefix, i18n.args.stat_value),
}
local value = {}
for key, args_key in pairs(id) do
value[key] = argtbl[args_key]
end
if value.id ~= nil and ((value.min ~= nil and value.max ~= nil and value.value == nil) or (value.min == nil and value.max == nil and value.value ~= nil)) then
if value.value then
value.value = util.cast.number(value.value)
argtbl[id.value] = value.value
else
value.min = util.cast.number(value.min)
argtbl[id.min] = value.min
value.max = util.cast.number(value.max)
argtbl[id.max] = value.max
-- Also set average value
value.avg = (value.min + value.max)/2
argtbl[string.format('%sstat%s_avg', args.prefix, i)] = value.avg
end
argtbl[string.format('%sstat%s', args.prefix, i)] = value
stats[#stats+1] = value
elseif util.table.has_all_value(value, {'id', 'min', 'max', 'value'}, nil) then
value = nil
-- all other cases should be improperly set value
else
error(string.format(i18n.errors.improper_stat, args.prefix, i))
end
until value == nil
argtbl[string.format('%sstats', args.prefix)] = stats
end
function util.args.spawn_weight_list(argtbl, args)
args = args or {}
args.input_argument = i18n.args.spawn_weight_prefix
args.output_argument = 'spawn_weights'
args.cargo_table = 'spawn_weights'
util.args.weight_list(argtbl, args)
end
function util.args.generation_weight_list(argtbl, args)
args = args or {}
args.input_argument = i18n.args.generation_weight_prefix
args.output_argument = 'generation_weights'
args.cargo_table = 'generation_weights'
util.args.weight_list(argtbl, args)
end
function util.args.weight_list(argtbl, args)
-- Parses a weighted pair of lists and sets properties
--
-- argtbl: argument table to work with
-- args:
-- output_argument - if set, set arguments to this value
-- input_argument - input prefix for parsing the arguments from the argtbl
-- subobject_name - name of the subobject
m_cargo = m_cargo or require('Module:Cargo')
args = args or {}
args.input_argument = args.input_argument or 'spawn_weight'
local i = 0
local id = nil
local value = nil
if args.output_argument then
argtbl[args.output_argument] = {}
end
repeat
i = i + 1
id = {
tag = string.format('%s%s_tag', args.input_argument, i),
value = string.format('%s%s_value', args.input_argument, i),
}
value = {
tag = argtbl[id.tag],
value = argtbl[id.value],
}
if value.tag ~= nil and value.value ~= nil then
if args.output_argument then
argtbl[args.output_argument][i] = value
end
if args.cargo_table then
m_cargo.store({
_table = args.cargo_table,
ordinal = i,
tag = value.tag,
weight = util.cast.number(value.value, {min=0}),
})
end
elseif not (value.tag == nil and value.value == nil) then
error(string.format(i18n.errors.invalid_weight, id.tag, id.value))
end
until value.tag == nil
end
function util.args.version(argtbl, args)
-- in any prefix spaces should be included
--
-- argtbl: argument table to work with
-- args:
-- set_properties: if defined, set properties on the page
-- variables: table of prefixes
-- ignore_unknowns: if defined, treat a version number of '?' as if it
-- were not present
-- noquery: For testing; if defined, skips the query
-- return_ids_and_keys: For testing; on return, args.version_ids and
-- args.versionkeys are set to the IDs and keys found
args = args or {}
args.variables = args.variables or {
release = {},
removal = {},
}
local version_ids={}
local version_keys={}
for key, data in pairs(args.variables) do
local full_key = string.format('%s_version', key)
if args.ignore_unknowns and (argtbl[full_key] == '?') then
argtbl[full_key] = nil
elseif argtbl[full_key] ~= nil then
local value = util.cast.version(argtbl[full_key], {return_type = 'string'})
argtbl[full_key] = value
if value ~= nil then
data.value = value
if data.property ~= nil then
version_ids[#version_ids+1] = value
version_keys[value] = key
end
end
end
end
-- no need to do a query if nothing was fetched
if (args.noquery == nil) and (#version_ids > 0) then
for i, id in ipairs(version_ids) do
version_ids[i] = string.format('Versions.version="%s"', id)
end
local results = m_cargo.query(
{'Versions'},
{'release_date', 'version'},
{
where = table.concat(version_ids, ' OR '),
}
)
if #results ~= #version_ids then
error(string.format(i18n.too_many_versions, #results, #version_ids))
end
for _, row in ipairs(results) do
local key = version_keys[row.version]
argtbl[string.format('%s_date', key)] = row.release_date
end
end
if args.return_ids_and_keys ~= nil then
args.version_ids = version_ids
args.version_keys = version_keys
end
end
function util.args.from_cargo_map(args)
m_cargo = m_cargo or require('Module:Cargo')
return m_cargo.store_mapped_args(args)
end
function util.args.template_to_lua(str)
--[[
Convert templates to lua format. Simplifes debugging and creating
examples.
Parameters
----------
str : string
The entire template wrapped into string. Tip: Use Lua's square
bracket syntax for defining string literals.
Returns
-------
out : table
out.template - Template name.
out.args - arguments in table format.
out.args_to_str - arguments in readable string format.
]]
local out = {}
-- Get the template name:
out.template = string.match(str, '{{(.-)%s*|')
-- Remove everything but the arguments:
str = string.gsub(str, '%s*{{.-|', '')
str = string.gsub(str, '%s*}}%s*', '')
-- Split up the arguments:
out.args = {}
for i, v in ipairs(util.string.split(str, '%s*|%s*')) do
local arg = util.string.split(v, '%s*=%s*')
out.args[arg[1]] = arg[2]
out.args[#out.args+1] = arg[1]
end
-- Concate for easy copy/pasting:
local tbl = {}
for i, v in ipairs(out.args) do
tbl[#tbl+1]= string.format("%s='%s'", v, out.args[v])
end
out.args_to_str = table.concat(tbl, ',\n')
return out
end
-- ----------------------------------------------------------------------------
-- util.html
-- ----------------------------------------------------------------------------
util.html = {}
function util.html.abbr(text, title, options)
-- Outputs html tag <abbr> as string or as mw.html node.
--
-- options
-- class: class attribute
-- output: set to mw.html to return a mw.html node instead of a string
if not title then
return text
end
options = options or {}
local abbr = mw.html.create('abbr')
abbr:attr('title', title)
local class
if type(options) == 'table' and options.class then
class = options.class
else
class = options
end
if type(class) == 'string' then
abbr:attr('class', class)
end
abbr:wikitext(text)
if options.output == mw.html then
return abbr
end
return tostring(abbr)
end
function util.html.error(args)
-- Create an error message box
--
-- args
-- msg str The error message
args = args or {}
local err = mw.html.create('strong')
:addClass('error')
:tag('span')
:addClass('module-error')
:wikitext(i18n.errors.module_error .. (args.msg or ''))
:done()
return tostring(err)
end
function util.html.poe_color(label, text, class)
if text == nil or text == '' then
return nil
end
local em = mw.html.create('em')
:addClass('tc -' .. label)
:addClass(class or '')
:wikitext(text)
return tostring(em)
end
util.html.poe_colour = util.html.poe_color
function util.html.tooltip(abbr, text, class)
return string.format('<span class="hoverbox c-tooltip %s"><span class="hoverbox__activator c-tooltip__activator">%s</span><span class="hoverbox__display c-tooltip__display">%s</span></span>', class or '', abbr or '', text or '')
end
function util.html.table_cell(type)
-- Table cells
-- type: 'yes', 'no', 'na'
-- Returns mw.html node
local td = mw.html.create('td')
td
:attr('data-sort-value', i18n.table_cells[type].sort)
:addClass(i18n.table_cells[type].class)
:wikitext(i18n.table_cells[type].text)
return td
end
function util.html.format_value(tpl_args, value, options)
-- value: table
-- min:
-- max:
-- options: table
-- func: Function to transform the value retrieved from the database
-- fmt: Format string (or function that returns format string) to use for the value.
-- Default: '%s'
-- fmt_range: Format string to use for range value.
-- Default: '(%s-%s)'
-- color: poe_color code to use for the value. False for no color.
-- Default: 'value' if value is unmodified; 'mod' if modified
-- class: Additional css class added to color tag
-- inline: Format string to use for the output
-- inline_color: poe_color code to use for the output. False for no color.
-- Default: Inherits from value color
-- inline_class: Additional css class added to inline color tag
-- no_color: (Deprecated; use color=false instead)
-- return_color: (Deprecated; returns both value.out and value without this)
-- Make shallow copy to avoid modifying the original table
local value_copy = {}
for k, v in pairs(value) do
value_copy[k] = v
end
local default_color = 'value'
local base = {
min = value_copy.base_min or value_copy.base,
max = value_copy.base_max or value_copy.base,
}
if value_copy.min ~= base.min or value_copy.max ~= base.max then
default_color = 'mod'
end
if options.color ~= false and options.no_color == nil then
value_copy.color = options.color or default_color
end
if options.func then
value_copy.min = options.func(tpl_args, value_copy.min)
value_copy.max = options.func(tpl_args, value_copy.max)
end
local fmt = options.fmt or '%s'
if type(fmt) == 'function' then -- Function that returns the format string
fmt = fmt(tpl_args, value_copy)
end
if value_copy.min == value_copy.max then -- Static value
value_copy.out = string.format(fmt, value_copy.min)
else -- Range value
local fmt_range = options.fmt_range or i18n.range
value_copy.out = string.format(
string.format(fmt_range, fmt, fmt),
value_copy.min,
value_copy.max
)
end
local inline = options.inline
if type(inline) == 'function' then
inline = inline(tpl_args, value_copy)
end
inline = inline ~= '' and inline or nil -- TODO: Eliminate the need for this?
local inline_color = options.inline_color
if value_copy.color and (not inline or inline_color ~= nil) then
value_copy.out = util.html.poe_color(value_copy.color, value_copy.out, options.class)
end
if inline then
value_copy.out = string.format(inline, value_copy.out)
if inline_color or inline_color == nil and options.color ~= false then
inline_color = inline_color or value_copy.color or default_color
value_copy.out = util.html.poe_color(inline_color, value_copy.out, options.inline_class)
end
end
if options.return_color then
return value_copy.out, value_copy.color
end
return value_copy.out, value_copy
end
function util.html.wikilink(page, text)
if text then
return string.format('[[%s|%s]]', page, text)
end
return string.format('[[%s]]', page)
end
function util.html.url_link(url, text)
return string.format('[%s %s]', url, text)
end
-- ----------------------------------------------------------------------------
-- util.misc
-- ----------------------------------------------------------------------------
util.misc = {}
function util.misc.invoker_factory(func, options)
-- Returns a function that can be called directly or with #invoke.
return function (frame)
frame = frame or {}
local args
if type(frame.args) == 'table' then
-- Called via #invoke, so use getArgs().
getArgs = getArgs or require('Module:Arguments').getArgs
args = getArgs(frame, options)
else
-- Called from another module or from the debug console, so assume args
-- are passed in directly.
args = frame
end
return func(args)
end
end
function util.misc.is_frame(frame)
-- the type of the frame is a table containing the functions, so check whether some of these exist
-- should be enough to avoid collisions.
return not(frame == nil or type(frame) ~= 'table' or (frame.argumentPairs == nil and frame.callParserFunction == nil))
end
function util.misc.get_frame(frame)
-- OBSOLETE. Use mw.getCurrentFrame() instead.
return mw.getCurrentFrame()
end
function util.misc.get_args_raw(frame)
-- Simple method for getting arguments. Use this instead of Module:Arguments
-- when the extra options provided by the latter would be overkill.
if util.misc.is_frame(frame) then
-- Called via {{#invoke:}}, so use the args that were passed into the
-- template.
return frame.args
end
-- Called from another module or from the debug console, so assume args
-- are passed in directly.
return frame
end
function util.misc.maybe_sandbox(module_name)
-- Did we load or {{#invoke:}} a module sandbox?
if module_name and package.loaded[string.format('Module:%s/sandbox', module_name)] ~= nil or string.find(mw.getCurrentFrame():getTitle(), 'sandbox', 1, true) then
return true
end
return false
end
function util.misc.add_category(categories, args)
-- categories: table of categories
-- args: table of extra arguments
-- namespace: id of namespace to validate against
-- ignore_blacklist: set to non-nil to ignore the blacklist
-- sub_page_blacklist: blacklist of subpages to use (if empty, use default)
-- namespace_blacklist: blacklist of namespaces to use (if empty, use default)
if type(categories) == 'string' then
categories = {categories}
end
if args == nil then
args = {}
end
local title = mw.title.getCurrentTitle()
local sub_blacklist = args.sub_page_blacklist or cfg.misc.category_blacklist.sub_pages
local ns_blacklist = args.namespace_blacklist or cfg.misc.category_blacklist.namespaces
if args.namespace ~= nil and title.namespace ~= args.namespace then
return ''
end
if args.ignore_blacklist == nil and (sub_blacklist[title.subpageText] or ns_blacklist[title.subjectNsText]) then
return ''
end
local cats = {}
for i, cat in ipairs(categories) do
cats[i] = string.format('[[Category:%s]]', cat)
end
return table.concat(cats)
end
-- ----------------------------------------------------------------------------
-- util.Error
-- ----------------------------------------------------------------------------
-- Prototype error object
local Error_prototype = {
message = i18n.errors.unspecified,
code = 'module_error',
issue = true, -- Whether to issue error
level = 2,
}
Error_prototype.__index = Error_prototype
function Error_prototype:throw(force)
if force or self.issue then
error(self.message, self.level)
end
return self
end
function Error_prototype:get_html()
return util.html.error{msg=self.message}
end
function Error_prototype:get_category(args)
return util.misc.add_category(self.category, args)
end
function util.Error(obj)
-- Create a new error object
obj = obj or {}
setmetatable(obj, Error_prototype)
return obj
end
-- ----------------------------------------------------------------------------
-- util.string
-- ----------------------------------------------------------------------------
util.string = {}
function util.string.trim(str, charset)
--[[
Trims leading and trailing characters in charset from a string.
Charset is '%s' by default, which matches whitespace characters
This works much like mw.text.trim, using the string library instead
of the ustring library. This function may return erroneous results
if the charset needs to be Unicode-aware.
--]]
charset = charset or '%s'
str = string.gsub(str, '^[' .. charset .. ']*(.-)[' .. charset .. ']*$', '%1')
return str
end
function util.string.strip_wikilinks(str)
--[[
Removes wikilinks from a string, leaving the plain text
--]]
str = mw.ustring.gsub(str, '%[%[:?([^%]|]+)%]%]', '%1')
str = mw.ustring.gsub(str, '%[%[:?[^|]+|([^%]|]+)%]%]', '%1')
return str
end
function util.string.strip_html(str)
--[[
Removes html tags from a string, leaving the plain text
--]]
str = mw.ustring.gsub(str, '<[^>]*>', '')
return str
end
function util.string.split(str, pattern, plain)
--[[
Splits a string into a table
This does essentially the same thing as mw.text.split, but with
significantly better performance. This function may return erroneous
results if the pattern needs to be Unicode-aware.
str String to split
pattern Pattern to use for splitting
plain If true, pattern is interpreted as a literal string
--]]
local out = {}
local init = 1
local split_start, split_end = string.find(str, pattern, init, plain)
while split_start do
out[#out+1] = string.sub(str, init, split_start-1)
init = split_end+1
split_start, split_end = string.find(str, pattern, init, plain)
end
out[#out+1] = string.sub(str, init)
return out
end
function util.string.split_outer(str, pattern, outer)
--[[
Split a string into a table according to the pattern, ignoring
matching patterns inside the outer patterns.
Parameters
----------
str : string
String to split.
pattern : string
Pattern to split on.
outer : table of strings where #outer = 2.
Table with 2 strings that defines the opening and closing patterns
to match, for example parantheses or brackets.
Returns
-------
out : table
table of split strings.
Examples
--------
-- Nesting at the end:
str = 'mods.id, CONCAT(mods.id, mods.name)'
mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'}))
table#1 {
"mods.id",
"CONCAT(mods.id, mods.name)",
}
-- Nesting in the middle:
str = 'mods.id, CONCAT(mods.id, mods.name), mods.required_level'
mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'}))
table#1 {
"mods.id",
"CONCAT(mods.id, mods.name)",
"mods.required_level",
}
]]
local out = {}
local nesting_level = 0
local i = 0
local pttrn = '(.-)' .. '(' .. pattern .. ')'
for v, sep in string.gmatch(str, pttrn) do
if nesting_level == 0 then
-- No nesting is occuring:
out[#out+1] = v
else
-- Nesting is occuring:
out[#out] = (out[math.max(#out, 1)] or '') .. v
end
-- Increase nesting level:
if string.find(v, outer[1]) then -- Multiple matches?
nesting_level = nesting_level + 1
end
if string.find(v, outer[2]) then
nesting_level = nesting_level - 1
end
-- Add back the separator if nesting is occuring:
if nesting_level ~= 0 then
out[#out] = out[#out] .. sep
end
-- Get the last index value:
i = i + #v + #sep
end
-- Complement with the last part of the string:
if nesting_level == 0 then
out[#out+1] = string.sub(str, math.max(i+1, 1))
else
out[#out] = out[#out] .. string.sub(str, math.max(i+1, 1))
-- TODO: Check if nesting level is zero?
end
return out
end
function util.string.split_args(str, args)
-- Splits arguments string into a table
--
-- str: String of arguments to split
-- args: table of extra arguments
-- sep: separator to use (default: ,)
-- kvsep: separator to use for key value pairs (default: =)
local out = {}
if args == nil then
args = {}
end
args.sep = args.sep or ','
args.kvsep = args.kvsep or '='
if str ~= nil then
local row
for _, str in ipairs(util.string.split(str, args.sep)) do
row = util.string.split(str, args.kvsep)
if #row == 1 then
out[#out+1] = row[1]
elseif #row == 2 then
out[row[1]] = row[2]
else
error(string.format(i18n.number_of_arguments_too_large, #row))
end
end
end
return out
end
function util.string.format(format, ...)
--[[
String replacement with support for numbered argument conversion
specifications. This is useful for i18n, as translating can sometimes
change the order of words around.
The format can contain either numbered argument conversion specifications
(i.e., "%n$"), or unnumbered argument conversion specifications (i.e., "%"),
but not both.
If numbered argument conversion specifications are not needed, consider
using string.format() from the Lua string library instead.
Example:
local format = 'Bubba ate %2$d %1$s. That\'s a lot of %1$s!'
util.string.format(format, 'hotdogs', 26)
-> Bubba ate 26 hotdogs. That's a lot of hotdogs!
]]
local values = {}
for v in string.gmatch(format, '%%(%d+)%$') do
values[#values+1] = select(v, ...)
end
if #values == 0 then
-- Using unnumbered argument conversion specifications, so just pass
-- args to string.format().
return string.format(format, ...)
end
format = string.gsub(format, '%%%d+%$', '%%')
return string.format(format, unpack(values))
end
function util.string.first_to_upper(str)
--[[
Converts the first letter of a string to uppercase
--]]
-- Reassign to variable before returning since string.gsub returns two values
str = str:gsub('^%l', string.upper)
return str
end
util.string.pattern = {}
function util.string.pattern.valid_var_name()
--[[
Get a pattern for a valid variable name.
]]
return '%A?([%a_]+[%w_]*)[^%w_]?'
end
-- ----------------------------------------------------------------------------
-- util.table
-- ----------------------------------------------------------------------------
util.table = {}
function util.table.length(tbl)
-- Get number of elements in a table. Counts both numerically indexed
-- elements and associative elements. Does not count nil elements.
local count = 0
for _ in pairs(tbl) do
count = count + 1
end
return count
end
util.table.count = util.table.length
function util.table.contains(tbl, value)
-- Checks whether a table contains a value
for _, v in pairs(tbl) do
if v == value then
return true
end
end
return false
end
function util.table.has_key(tbl, key)
-- Checks whether a table has a key
return tbl[key] ~= nil
end
function util.table.has_any_key(tbl, keys)
-- Checks whether a table has at least one of the keys
for _, key in ipairs(keys or {}) do
if tbl[key] ~= nil then
return true
end
end
return false
end
function util.table.has_all_keys(tbl, keys)
-- Checks whether a table has all of the keys
for _, key in ipairs(keys or {}) do
if tbl[key] == nil then
return false
end
end
return true
end
function util.table.keys(tbl)
-- Returns the keys of a table
local keys = {}
for k, _ in pairs(tbl) do
keys[#keys+1] = k
end
return keys
end
util.table.assoc_to_array = util.table.keys
function util.table.column(tbl, colkey, idxkey)
--[[
Returns the values of one column of a multi-dimensional table
tbl A multi-dimensional table
colkey The column key from the inner tables
idxkey If provided, the column from the inner tables to index the
returned values by. Default: nil
--]]
local col = {}
for _, row in pairs(tbl) do
if type(row) == 'table' and row[colkey] ~= nil then
if idxkey ~= nil and row[idxkey] ~= nil then
col[row[idxkey]] = row[colkey]
else
col[#col+1] = row[colkey]
end
end
end
return col
end
function util.table.merge(...)
--[[
Merges the keys and values of multiple tables into a single table. If
the input tables share non-numerical keys, then the later values for those
keys will overwrite the previous ones. Numerical keys are instead appended
and renumbered, incrementing from 1.
--]]
local tbl = {}
for _, t in ipairs({...}) do
for k, v in pairs(t) do
if type(k) == 'number' then
table.insert(tbl, v)
else
tbl[k] = v
end
end
end
return tbl
end
function util.table.diff(tbl1, tbl2)
-- Finds the difference between two tables, returning a table containing the
-- values in tbl1 that are not in tbl2. Indexing is ignored; only values are
-- compared.
local diff = {}
for _, k in pairs(tbl1) do
if not util.table.contains(tbl2, k) then
table.insert(diff, k)
end
end
return diff
end
function util.table.remove_duplicates(tbl)
-- Removes duplicate values from an array.
local new_tbl = {}
local exists = {}
for _, v in ipairs(tbl) do
if not exists[v] then
new_tbl[#new_tbl+1] = v
exists[v] = true
end
end
return new_tbl
end
function util.table.has_all_value(tbl, keys, value)
-- Whether all the table values with the specified keys are the specified value
for _, k in ipairs(keys or {}) do
if tbl[k] ~= value then
return false
end
end
return true
end
function util.table.has_one_value(tbl, keys, value)
-- Whether one of table values with the specified keys is the specified value
for _, k in ipairs(keys or {}) do
if tbl[k] == value then
return true
end
end
return false
end
function util.table.find_in_nested_array(args)
-- Iterates thoguh the given nested array and finds the given value
--
-- ex.
-- data = {
-- {a=5}, {a=6}}
-- find_nested_array{arg=6, tbl=data, key='a'} -> 6
-- find_nested_array(arg=10, tbl=data, key='a'} -> nil
-- -> returns "6"
--
-- args: Table containing:
-- value: value of the argument
-- tbl: table of valid options
-- key: key or table of key of in tbl
-- rtrkey: if key is table, return this key instead of the value instead
-- rtrvalue: default: true
local rtr
if type(args.key) == 'table' then
for _, item in ipairs(args.tbl) do
for _, k in ipairs(args.key) do
if item[k] == args.value then
rtr = item
break
end
end
end
elseif args.key == nil then
for _, item in ipairs(args.tbl) do
if item == args.value then
rtr = item
break
end
end
else
for _, item in ipairs(args.tbl) do
if item[args.key] == args.value then
rtr = item
break
end
end
end
if rtr == nil then
return rtr
end
if args.rtrkey ~= nil then
return rtr[args.rtrkey]
elseif args.rtrvalue or args.rtrvalue == nil then
return args.value
else
return rtr
end
end
-- ----------------------------------------------------------------------------
return util