Path of Exile Wiki

Wiki поддерживается сообществом, поэтому подумайте над тем, чтобы внести свой вклад.

ПОДРОБНЕЕ

Path of Exile Wiki
Konigreich182@legacy41979339 (обсуждение | вклад)
(Новая страница: «-- Utility stuff local util = {} util.cast = {} util.cast.bool_false = {'false', '0', 'disabled', 'off', 'no', ''} function util.cast.boolean(value) -- Take…»)
 
Нет описания правки
(не показано 40 промежуточных версий 6 участников)
Строка 1: Строка 1:
  +
-------------------------------------------------------------------------------
-- Utility stuff
 
  +
--
  +
-- Module:Util
  +
--
  +
-- This meta module contains a number of utility functions
  +
-------------------------------------------------------------------------------
  +
  +
local xtable = require('Module:Table')
  +
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 = {}
  +
  +
-- ----------------------------------------------------------------------------
  +
-- util.cast
  +
-- ----------------------------------------------------------------------------
  +
 
util.cast = {}
 
util.cast = {}
util.cast.bool_false = {'false', '0', 'disabled', 'off', 'no', ''}
 
   
function util.cast.boolean(value)
+
function util.cast.text(value, args)
-- Takes an abitary value and casts it to a bool value
+
-- Takes an arbitary value and returns it as texts.
 
--
 
--
  +
-- Also strips any categories
-- for strings false will be according to util.cast.bool_false
 
  +
--
  +
-- 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
  +
  +
value = string.gsub(value, '%[%[Category:[%w_ ]+%]%]', '')
  +
return value
  +
end
  +
  +
function util.cast.boolean(value, args)
  +
-- Takes an abitary value and casts it to a bool value
  +
--
  +
-- 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
  +
if args.cast_nil == nil or args.cast_nil == true then
return false
 
  +
return false
  +
else
  +
return
  +
end
 
elseif t == 'boolean' then
 
elseif t == 'boolean' then
 
return value
 
return value
 
elseif t == 'number' then
 
elseif t == 'number' then
if t == 0 then return false end
+
if value == 0 then return false end
 
return true
 
return true
 
elseif t == 'string' then
 
elseif t == 'string' then
 
local tmp = string.lower(value)
 
local tmp = string.lower(value)
for _, v in ipairs(util.cast.bool_false) do
+
for _, v in ipairs(i18n.bool_false) do
 
if v == tmp then
 
if v == tmp then
 
return false
 
return false
Строка 26: Строка 80:
 
return true
 
return true
 
else
 
else
error('"' .. value .. ' is of uncaptured type "' .. t .. '"')
+
error(string.format(i18n.errors.not_a_boolean, tostring(value), t))
 
end
 
end
  +
 
 
end
 
end
   
function util.cast.number(value, default)
+
function util.cast.number(value, args)
 
-- Takes an abitary value and attempts to cast it to int
 
-- Takes an abitary value and attempts to cast it to int
 
--
 
--
  +
-- args:
-- For strings: if default is nil and the conversion fails, an error will be returned
 
  +
-- default: for strings, if default is nil and the conversion fails, an error will be returned
  +
-- min: error if <min
  +
-- max: error if >max
  +
if args == nil then
  +
args = {}
  +
end
  +
 
local t = type(value)
 
local t = type(value)
+
local val
  +
 
if t == 'nil' then
 
if t == 'nil' then
return 0
+
val = nil
 
elseif t == 'boolean' then
 
elseif t == 'boolean' then
 
if value then
 
if value then
return 1
+
val = 1
 
else
 
else
return 0
+
val = 0
 
end
 
end
 
elseif t == 'number' then
 
elseif t == 'number' then
return value
+
val = value
 
elseif t == 'string' then
 
elseif t == 'string' then
 
val = tonumber(value)
 
val = tonumber(value)
  +
end
if val == nil then
 
  +
if default ~= nil then
 
  +
if val == nil then
return default
 
  +
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.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
  +
  +
--
  +
-- 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.array_table(k, args)
  +
-- Arguments:
  +
-- tbl - table to check against
  +
-- errmsg - error message if no element was found; should accept 1 parameter
  +
args = args or {}
  +
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
  +
local r = util.table.find_in_nested_array{value=element, tbl=args.tbl, key='full'}
  +
if r == nil then
  +
error(string.format(args.errmsg or i18n.errors.missing_element, element))
  +
end
  +
end
  +
tpl_args[args.key_out or k] = xtable:new(elements)
  +
end
  +
end
  +
end
  +
  +
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
  +
-- frame: frame used to set subobjects; if not set dont set properties
  +
-- 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
 
else
error('"' .. value .. '" is not an integer')
+
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
 
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
 
else
  +
error(string.format(i18n.errors.improper_stat, args.prefix, i))
return val
 
 
end
 
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
  +
-- frame - if set, automtically set subobjects
  +
-- 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.frame and args.cargo_table then
  +
m_cargo.store(args.frame, {
  +
_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:
  +
-- frame: frame for queries
  +
-- set_properties: if defined, set properties on the page
  +
-- variables: table of prefixes
  +
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 argtbl[full_key] ~= nil then
  +
local value = util.cast.version(argtbl[full_key], {return_type = 'string'})
  +
argtbl[full_key] = value
  +
data.value = value
  +
if data.property ~= nil then
  +
version_ids[#version_ids+1] = value
  +
version_keys[value] = key
  +
end
  +
end
  +
end
  +
  +
-- no need to do a query if nothing was fetched
  +
if #version_ids > 0 then
  +
if args.frame == nil then
  +
error('Properties were set, but frame was not')
  +
end
  +
  +
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
  +
end
  +
  +
function util.args.from_cargo_map(args)
  +
-- Maps the arguments from a cargo argument table (i.e. the ones used in m_cargo.declare_factory)
  +
--
  +
-- It will expect/handle the following fields:
  +
-- map.order - REQUIRED - Array table for the order in which the arguments in map.fields will be parsed
  +
-- map.table - REQUIRED - Table name (for storage)
  +
-- map.fields[id].field - REQUIRED - Name of the field in cargo table
  +
-- map.fields[id].type - REQUIRED - Type of the field in cargo table
  +
-- map.fields[id].func - OPTIONAL - Function to handle the arguments. It will be passed tpl_args and frame.
  +
-- The function should return the parsed value.
  +
--
  +
-- If no function is specified, default handling depending on the cargo field type will be used
  +
-- map.fields[id].default - OPTIONAL - Default value if the value is not set or returned as nil
  +
-- If default is a function, the function will be called with (tpl_args, frame) and expected to return a default value for the field.
  +
-- map.fields[id].name - OPTIONAL - Name of the field in tpl_args if it differs from the id in map.fields. Used for i18n for example
  +
-- map.fields[id].required - OPTIONAL - Whether a value for the field is required or whether it can be left empty
  +
-- Note: With a default value the field will never be empty
  +
-- map.fields[id].skip - OPTIONAL - Skip field if missing from order
  +
--
  +
--
  +
-- Expects argument table.
  +
-- REQUIRED:
  +
-- tpl_args - arguments passed to template after preprecessing
  +
-- frame - frame object
  +
-- table_map - table mapping object
  +
-- rtr - if set return cargo props instead of storing them
  +
  +
m_cargo = m_cargo or require('Module:Cargo')
  +
  +
local tpl_args = args.tpl_args
  +
local frame = args.frame
  +
local map = args.table_map
  +
  +
local cargo_values = {_table = map.table}
  +
  +
-- for checking missing keys in order
  +
local available_fields = {}
  +
for key, field in pairs(map.fields) do
  +
if field.skip == nil then
  +
available_fields[key] = true
  +
end
  +
end
  +
  +
-- main loop
  +
for _, key in ipairs(map.order) do
  +
local field = map.fields[key]
  +
if field == nil then
  +
error(string.format(i18n.errors.missing_key_in_fields, key, map.table))
  +
else
  +
available_fields[key] = nil
  +
end
  +
-- key in argument mapping
  +
local args_key
  +
if field.name then
  +
args_key = field.name
  +
else
  +
args_key = key
  +
end
  +
-- Retrieve value
  +
local value
  +
-- automatic handling only works if the field type is set
  +
if field.type ~= nil then
  +
value = tpl_args[args_key]
  +
  +
local cfield = m_cargo.parse_field{field=field.type}
  +
local handler
  +
if cfield.type == 'Integer' or cfield.type == 'Float' then
  +
handler = tonumber
  +
elseif cfield.type == 'Boolean' then
  +
handler = function (value)
  +
return util.cast.boolean(value, {cast_nil=false})
  +
end
  +
end
  +
  +
if cfield.list and value ~= nil then
  +
-- ingore whitespace between separator and values
  +
value = util.string.split(value, cfield.list .. '%s*')
  +
if handler then
  +
for index, v in ipairs(value) do
  +
value[index] = handler(v)
  +
if value[index] == nil then
  +
error(string.format(i18n.errors.handler_returned_nil, map.table, args_key, v, field.type))
  +
end
  +
end
  +
end
  +
elseif handler and value ~= nil then
  +
value = handler(value)
  +
if value == nil then
  +
error(string.format(i18n.errors.handler_returned_nil, map.table, args_key, tpl_args[args_key], field.type))
  +
end
  +
end
  +
-- Don't need special handling: String, Text, Wikitext, Searchtext
  +
-- Consider: Page, Date, Datetime, Coordinates, File, URL, Email
  +
end
  +
if field.func ~= nil then
  +
value = field.func(tpl_args, frame, value)
  +
end
  +
-- Check defaults
  +
if value == nil and field.default ~= nil then
  +
if type(field.default) == 'function' then
  +
value = field.default(tpl_args, frame)
  +
elseif type(field.default) == 'table' then
  +
mw.logObject(string.format(i18n.errors.table_object_as_default, key, map.table))
  +
value = mw.clone(field.default)
  +
else
  +
value = field.default
  +
end
  +
end
  +
-- Add value to arguments and cargo data
  +
if value ~= nil then
  +
-- key will be used here since the value will be used internally from here on in english
  +
tpl_args[key] = value
  +
if field.field ~= nil then
  +
cargo_values[field.field] = value
  +
end
  +
elseif field.required == true then
  +
error(string.format(i18n.errors.argument_required, args_key))
  +
end
  +
end
  +
  +
-- check for missing keys and return error if any are missing
  +
local missing = {}
  +
for key, _ in pairs(available_fields) do
  +
missing[#missing+1] = key
  +
end
  +
if #missing > 0 then
  +
error(string.format(i18n.errors.missing_key_in_order, map.table, table.concat(missing, '\n')))
  +
end
  +
  +
-- finally store data in DB
  +
if args.rtr ~= nil then
  +
return cargo_values
 
else
 
else
  +
m_cargo.store(frame, cargo_values)
error('"' .. value .. ' is of uncaptured type "' .. t .. '"')
 
 
end
 
end
 
end
 
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 - message
  +
if args == nil then
  +
args = {}
  +
end
  +
  +
local err = mw.html.create('strong')
  +
err
  +
: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
  +
class = class and (' ' .. class) or ''
  +
return tostring(mw.html.create('em')
  +
:attr('class', 'tc -' .. label .. class)
  +
:wikitext(text))
  +
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
  +
  +
util.html.td = {}
  +
function util.html.td.na(options)
  +
--
  +
-- options:
  +
-- as_tag
  +
-- output: set to mw.html to return a mw.html node instead of a string
  +
options = options or {}
  +
-- N/A table row, requires mw.html.create instance to be passed
  +
local td = mw.html.create('td')
  +
td
  +
:attr('class', 'table-na')
  +
:wikitext(i18n.na)
  +
:done()
  +
if options.as_tag or options.output == mw.html then
  +
return td
  +
end
  +
return tostring(td)
  +
end
  +
  +
function util.html.format_value(tpl_args, frame, 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 the value range. Default: '(%s-%s)'
  +
-- color: poe_color code to use for the value range. False for no color. Default: 'mod'
  +
-- 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: 'default'
  +
-- 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)
  +
if options.color ~= false and options.no_color == nil then
  +
if options.color then
  +
value.color = options.color
  +
elseif value.base ~= value.min or value.base ~= value.max then
  +
value.color = 'mod'
  +
else
  +
value.color = 'value'
  +
end
  +
end
  +
if options.func then
  +
value.min = options.func(tpl_args, frame, value.min)
  +
value.max = options.func(tpl_args, frame, value.max)
  +
end
  +
options.fmt = options.fmt or '%s'
  +
if type(options.fmt) == 'function' then -- Function that returns the format string
  +
options.fmt = options.fmt(tpl_args, frame, value)
  +
end
  +
if value.min == value.max then -- Static value
  +
value.out = string.format(options.fmt, value.min)
  +
else -- Range value
  +
options.fmt_range = options.fmt_range or i18n.range
  +
value.out = string.format(
  +
string.format(options.fmt_range, options.fmt, options.fmt),
  +
value.min,
  +
value.max
  +
)
  +
end
  +
if value.color then
  +
value.out = util.html.poe_color(value.color, value.out, options.class)
  +
end
  +
if type(options.inline) == 'function' then
  +
options.inline = options.inline(tpl_args, frame, value)
  +
end
  +
if options.inline and options.inline ~= '' then
  +
value.out = string.format(options.inline, value.out)
  +
if options.inline_color ~= false then
  +
options.inline_color = options.inline_color or 'default'
  +
value.out = util.html.poe_color(options.inline_color, value.out, options.inline_class)
  +
end
  +
end
  +
local return_color = options.return_color and value.color or nil
  +
if return_color then
  +
return value.out, return_color
  +
end
  +
return value.out, value
  +
end
  +
  +
-- ----------------------------------------------------------------------------
  +
-- util.misc
  +
-- ----------------------------------------------------------------------------
  +
  +
util.misc = {}
  +
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)
  +
if util.misc.is_frame(frame) then
  +
return frame
  +
end
  +
return mw.getCurrentFrame()
  +
end
  +
  +
function util.misc.maybe_sandbox(frame)
  +
-- Did {{#invoke:}} call sandbox version?
  +
frame = util.misc.get_frame(frame)
  +
if string.find(frame: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
  +
-- ingore_blacklist: set to non-nil to ingore 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.ingore_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
  +
  +
function util.misc.raise_error_or_return(args)
  +
--
  +
-- Arguments:
  +
-- args: table of arguments to this function (must be set)
  +
-- One required:
  +
-- raise_required: Don't raise errors and return html errors instead unless raisae is set in arguments
  +
-- no_raise_required: Don't return html errors and raise errors insetad unless no_raise is set in arguments
  +
--
  +
-- Optional:
  +
-- msg: error message to raise or return, default: nil
  +
-- args: argument directory to validate against (e.x. template args), default: {}
  +
args.args = args.args or {}
  +
args.msg = args.msg or ''
  +
if args.raise_required ~= nil then
  +
if args.args.raise ~= nil then
  +
error(args.msg)
  +
else
  +
return util.html.error{msg=args.msg}
  +
end
  +
elseif args.no_raise_required ~= nil then
  +
if args.args.no_raise ~= nil then
  +
return util.html.error{msg=args.msg}
  +
else
  +
error(args.msg)
  +
end
  +
else
  +
error(i18n.errors.invalid_raise_error_or_return_usage)
  +
end
  +
end
  +
  +
-- ----------------------------------------------------------------------------
  +
-- util.smw
  +
-- ----------------------------------------------------------------------------
  +
  +
util.smw = {}
  +
  +
util.smw.data = {}
  +
util.smw.data.rejected_namespaces = xtable:new({'User'})
  +
  +
function util.smw.safeguard(args)
  +
-- Used for safeguarding data entry so it doesn't get added on user space stuff
  +
--
  +
-- Args:
  +
-- smw_ingore_safeguard - ingore safeguard and return true
  +
if args == nil then
  +
args = {}
  +
end
  +
  +
if args.smw_ingore_safeguard then
  +
return true
  +
end
  +
  +
local namespace = mw.site.namespaces[mw.title.getCurrentTitle().namespace].name
  +
if util.smw.data.rejected_namespaces:contains(namespace) then
  +
return false
  +
end
  +
  +
return true
  +
end
  +
  +
-- ----------------------------------------------------------------------------
  +
-- util.string
  +
-- ----------------------------------------------------------------------------
  +
  +
util.string = {}
  +
  +
function util.string.strip(str, pattern)
  +
pattern = pattern or '%s'
  +
return string.gsub(str, "^" .. pattern .. "*(.-)" .. pattern .. "*$", "%1")
  +
end
  +
  +
function util.string.split(str, pattern)
  +
-- Splits string into a table
  +
--
  +
-- str: string to split
  +
-- pattern: pattern to use for splitting
  +
local out = {}
  +
local i = 1
  +
local split_start, split_end = string.find(str, pattern, i)
  +
while split_start do
  +
out[#out+1] = string.sub(str, i, split_start-1)
  +
i = split_end+1
  +
split_start, split_end = string.find(str, pattern, i)
  +
end
  +
out[#out+1] = string.sub(str, i)
  +
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(str, vars)
  +
--[[
  +
Allow string replacement using named arguments.
  +
  +
TODO:
  +
* Support %d ?
  +
* Support 0.2f ?
  +
  +
Parameters
  +
----------
  +
str : String to replace.
  +
vars : Table of arguments.
  +
  +
Examples
  +
--------
  +
= util.string.format('{foo} is {bar}.', {foo='Dinner', bar='nice'})
  +
  +
References
  +
----------
  +
http://lua-users.org/wiki/StringInterpolation
  +
]]
  +
  +
if not vars then
  +
vars = str
  +
str = vars[1]
  +
end
  +
  +
return (string.gsub(str, "({([^}]+)})",
  +
function(whole, i)
  +
return vars[i] or whole
  +
end))
  +
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.assoc_to_array(tbl, args)
  +
-- Turn associative array into an array, discarding the values
  +
local out = {}
  +
for key, _ in pairs(tbl) do
  +
out[#out+1] = key
  +
end
  +
return out
  +
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
  +
  +
-- ----------------------------------------------------------------------------
  +
-- util.Struct
  +
-- ----------------------------------------------------------------------------
  +
  +
util.Struct = function(map)
  +
local this = {map = map}
  +
  +
  +
-- sets a value to a field
  +
function this:set(field, value)
  +
if not field or not value then
  +
error('One or more arguments are nils')
  +
end
  +
  +
local _ = self.map[field]
  +
  +
if not _ then
  +
error(string.format('Field "%s" doesn\'t exist', field))
  +
end
  +
  +
if _.validate then
  +
_.value = _.validate(value)
  +
else
  +
_.value = value
  +
end
  +
  +
-- this happen if 'validate' returns nil
  +
if _.required == true and _.value == nil then
  +
error(string.format('Field "%s" is required but has been set to nil', field))
  +
end
  +
end
  +
  +
  +
-- adds a new prop to a field
  +
function this:set_prop(field, prop, value)
  +
if not field or not prop or not value then
  +
error('One or more arguments are nils')
  +
end
  +
  +
local _ = self.map[field]
  +
  +
if not _ then
  +
error(string.format('Field "%s" doesn\'t exist', field))
  +
end
  +
  +
_[prop] = value
  +
end
  +
  +
  +
-- gets a value from a field
  +
function this:get(field)
  +
if not field then
  +
error('Argument field is nil')
  +
end
  +
  +
local _ = self.map[field]
  +
  +
if not _ then
  +
error(string.format('Field "%s" doesn\'t exist', field))
  +
end
  +
  +
return _.value
  +
end
  +
  +
  +
-- gets a value from a prop field
  +
function this:get_prop(field, prop)
  +
if not field or not prop then
  +
error('One or more arguments are nils')
  +
end
  +
  +
local _ = self.map[field]
  +
  +
if not _ then
  +
error(string.format('Field "%s" doesn\'t exist', field))
  +
end
  +
  +
return _[prop]
  +
end
  +
  +
  +
-- shows a value from a field
  +
function this:show(field)
  +
if not field then
  +
error('Argument field is nil')
  +
end
  +
  +
local _ = self.map[field]
  +
  +
if not _ then
  +
error(string.format('Field "%s" doesn\'t exist', field))
  +
end
  +
  +
if _.show then
  +
return _.show(_)
  +
else
  +
return _.value
  +
end
  +
end
  +
  +
  +
return this
  +
end
  +
  +
-- ----------------------------------------------------------------------------
   
 
return util
 
return util

Версия от 06:26, 11 июня 2021

Template info icon Документация модуля[просмотр] [править] [история] [очистить]

Описание

Предоставляет служебные функции для модулей программирования.

Структура

Группа Описание
util.cast утилиты для приведения значений (т. е. из аргументов)
util.html простые функции для создания HTML-тегов
util.misc прочие функции

Использование

Этот модуль должен быть загружен с помощью require().

-------------------------------------------------------------------------------
-- 
--                              Module:Util
-- 
-- This meta module contains a number of utility functions
-------------------------------------------------------------------------------

local xtable = require('Module:Table')
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 returns it as texts. 
    -- 
    -- 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
    
    value = string.gsub(value, '%[%[Category:[%w_ ]+%]%]', '')
    return value
end

function util.cast.boolean(value, args)
    -- Takes an abitary value and casts it to a bool value
    --
    -- 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 abitary value and attempts to cast it to int
    --
    -- 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
    if args == nil then
        args = {}
    end

    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.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

--
-- 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.array_table(k, args)
    -- Arguments:
    --  tbl - table to check against
    --  errmsg - error message if no element was found; should accept 1 parameter
    args = args or {}
    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 
                local r = util.table.find_in_nested_array{value=element, tbl=args.tbl, key='full'}
                if r == nil then
                    error(string.format(args.errmsg or i18n.errors.missing_element, element))
                end
            end
            tpl_args[args.key_out or k] = xtable:new(elements)
        end
    end
end

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
    --  frame: frame used to set subobjects; if not set dont set properties
    --  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
    --  frame - if set, automtically set subobjects
    --  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.frame and args.cargo_table then
                m_cargo.store(args.frame, {
                    _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:
    --  frame: frame for queries
    --  set_properties: if defined, set properties on the page
    --  variables: table of prefixes
    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 argtbl[full_key] ~= nil then
            local value = util.cast.version(argtbl[full_key], {return_type = 'string'})
            argtbl[full_key] = value
            data.value = value
            if data.property ~= nil then
                version_ids[#version_ids+1] = value
                version_keys[value] = key
            end
        end
    end

    -- no need to do a query if nothing was fetched
    if #version_ids > 0 then
        if args.frame == nil then
            error('Properties were set, but frame was not')
        end
        
        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
end

function util.args.from_cargo_map(args)
    -- Maps the arguments from a cargo argument table (i.e. the ones used in m_cargo.declare_factory)
    --
    -- It will expect/handle the following fields:
    -- map.order               - REQUIRED - Array table for the order in which the arguments in map.fields will be parsed
    -- map.table               - REQUIRED - Table name (for storage)
    -- map.fields[id].field    - REQUIRED - Name of the field in cargo table
    -- map.fields[id].type     - REQUIRED - Type of the field in cargo table
    -- map.fields[id].func     - OPTIONAL - Function to handle the arguments. It will be passed tpl_args and frame.
    --                                      The function should return the parsed value.
    --                                     
    --                                      If no function is specified, default handling depending on the cargo field type will be used
    -- map.fields[id].default  - OPTIONAL - Default value if the value is not set or returned as nil
    --                                      If default is a function, the function will be called with (tpl_args, frame) and expected to return a default value for the field.
    -- map.fields[id].name     - OPTIONAL - Name of the field in tpl_args if it differs from the id in map.fields. Used for i18n for example
    -- map.fields[id].required - OPTIONAL - Whether a value for the field is required or whether it can be left empty
    --                                      Note: With a default value the field will never be empty
    -- map.fields[id].skip     - OPTIONAL - Skip field if missing from order
    -- 
    --
    -- Expects argument table.
    -- REQUIRED:
    --  tpl_args  - arguments passed to template after preprecessing
    --  frame     - frame object
    --  table_map - table mapping object 
    --  rtr       - if set return cargo props instead of storing them

    m_cargo = m_cargo or require('Module:Cargo')

    local tpl_args = args.tpl_args
    local frame = args.frame
    local map = args.table_map
    
    local cargo_values = {_table = map.table}
    
    -- for checking missing keys in order
    local available_fields = {}
    for key, field in pairs(map.fields) do
        if field.skip == nil then
            available_fields[key] = true
        end
    end
    
    -- main loop
    for _, key in ipairs(map.order) do
        local field = map.fields[key]
        if field == nil then
            error(string.format(i18n.errors.missing_key_in_fields, key, map.table))
        else
            available_fields[key] = nil
        end
        -- key in argument mapping
        local args_key
        if field.name then
            args_key = field.name
        else
            args_key = key
        end
        -- Retrieve value
        local value
        -- automatic handling only works if the field type is set
        if field.type ~= nil then
            value = tpl_args[args_key]
            
            local cfield = m_cargo.parse_field{field=field.type}
            local handler
            if cfield.type == 'Integer' or cfield.type == 'Float' then
                handler = tonumber
            elseif cfield.type == 'Boolean' then
                handler = function (value) 
                    return util.cast.boolean(value, {cast_nil=false})
                end
            end
            
            if cfield.list and value ~= nil then
                -- ingore whitespace between separator and values
                value = util.string.split(value, cfield.list .. '%s*')
                if handler then
                    for index, v in ipairs(value) do
                        value[index] = handler(v)
                        if value[index] == nil then
                            error(string.format(i18n.errors.handler_returned_nil, map.table, args_key, v, field.type))
                        end
                    end
                end
            elseif handler and value ~= nil then
                value = handler(value)
                if value == nil then
                    error(string.format(i18n.errors.handler_returned_nil, map.table, args_key, tpl_args[args_key], field.type))
                end
            end
            -- Don't need special handling: String, Text, Wikitext, Searchtext
            -- Consider: Page, Date, Datetime, Coordinates, File, URL, Email
        end
        if field.func ~= nil then
            value = field.func(tpl_args, frame, value)
        end
        -- Check defaults
        if value == nil and field.default ~= nil then
            if type(field.default) == 'function' then
                value = field.default(tpl_args, frame)
            elseif type(field.default) == 'table' then
                mw.logObject(string.format(i18n.errors.table_object_as_default, key, map.table))
                value = mw.clone(field.default)
            else
                value = field.default
            end
        end
        -- Add value to arguments and cargo data
        if value ~= nil then
            -- key will be used here since the value will be used internally from here on in english
            tpl_args[key] = value
            if field.field ~= nil then
                cargo_values[field.field] = value
            end
        elseif field.required == true then
            error(string.format(i18n.errors.argument_required, args_key))
        end
    end
    
    -- check for missing keys and return error if any are missing
    local missing = {}
    for key, _ in pairs(available_fields) do
        missing[#missing+1] = key
    end
    if #missing > 0 then
        error(string.format(i18n.errors.missing_key_in_order, map.table, table.concat(missing, '\n')))
    end
    
    -- finally store data in DB
    if args.rtr ~= nil then
        return cargo_values
    else
        m_cargo.store(frame, cargo_values)
    end
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 - message
    if args == nil then
        args = {}
    end

    local err = mw.html.create('strong')
    err
        :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
    class = class and (' ' .. class) or ''
    return tostring(mw.html.create('em')
        :attr('class', 'tc -' .. label .. class)
        :wikitext(text))
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

util.html.td = {}
function util.html.td.na(options)
    --
    -- options:
    --   as_tag
    --   output: set to mw.html to return a mw.html node instead of a string
    options = options or {}
    -- N/A table row, requires mw.html.create instance to be passed
    local td = mw.html.create('td')
    td
        :attr('class', 'table-na')
        :wikitext(i18n.na)
        :done()
    if options.as_tag or options.output == mw.html then
        return td
    end
    return tostring(td)
end

function util.html.format_value(tpl_args, frame, 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 the value range. Default: '(%s-%s)'
    --  color: poe_color code to use for the value range. False for no color. Default: 'mod'
    --  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: 'default'
    --  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)
    if options.color ~= false and options.no_color == nil then
        if options.color then
            value.color = options.color
        elseif value.base ~= value.min or value.base ~= value.max then
            value.color = 'mod'
        else
            value.color = 'value'
        end
    end
    if options.func then
        value.min = options.func(tpl_args, frame, value.min)
        value.max = options.func(tpl_args, frame, value.max)
    end
    options.fmt = options.fmt or '%s'
    if type(options.fmt) == 'function' then -- Function that returns the format string
        options.fmt = options.fmt(tpl_args, frame, value)
    end
    if value.min == value.max then -- Static value
        value.out = string.format(options.fmt, value.min)
    else -- Range value
        options.fmt_range = options.fmt_range or i18n.range
        value.out = string.format(
            string.format(options.fmt_range, options.fmt, options.fmt),
            value.min,
            value.max
        )
    end
    if value.color then
        value.out = util.html.poe_color(value.color, value.out, options.class)
    end
    if type(options.inline) == 'function' then
        options.inline = options.inline(tpl_args, frame, value)
    end
    if options.inline and options.inline ~= '' then
        value.out = string.format(options.inline, value.out)
        if options.inline_color ~= false then
            options.inline_color = options.inline_color or 'default'
            value.out = util.html.poe_color(options.inline_color, value.out, options.inline_class)
        end
    end
    local return_color = options.return_color and value.color or nil
    if return_color then
        return value.out, return_color
    end
    return value.out, value
end

-- ----------------------------------------------------------------------------
-- util.misc
-- ----------------------------------------------------------------------------

util.misc = {}
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)
    if util.misc.is_frame(frame) then
        return frame
    end
    return mw.getCurrentFrame()
end

function util.misc.maybe_sandbox(frame)
    -- Did {{#invoke:}} call sandbox version?
    frame = util.misc.get_frame(frame)
    if string.find(frame: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
    --  ingore_blacklist: set to non-nil to ingore 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.ingore_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

function util.misc.raise_error_or_return(args)
    --
    -- Arguments:
    -- args: table of arguments to this function (must be set)
    --  One required:
    --  raise_required: Don't raise errors and return html errors instead unless raisae is set in arguments
    --  no_raise_required: Don't return html errors and raise errors insetad unless no_raise is set in arguments
    --
    --  Optional:
    --  msg: error message to raise or return, default: nil
    --  args: argument directory to validate against (e.x. template args), default: {}
    args.args = args.args or {}
    args.msg = args.msg or ''
    if args.raise_required ~= nil then
        if args.args.raise ~= nil then
            error(args.msg)
        else
            return util.html.error{msg=args.msg}
        end
    elseif args.no_raise_required ~= nil then
        if args.args.no_raise ~= nil then
            return util.html.error{msg=args.msg}
        else
            error(args.msg)
        end
    else
        error(i18n.errors.invalid_raise_error_or_return_usage)
    end
end

-- ----------------------------------------------------------------------------
-- util.smw
-- ----------------------------------------------------------------------------

util.smw = {}

util.smw.data = {}
util.smw.data.rejected_namespaces = xtable:new({'User'})

function util.smw.safeguard(args)
    -- Used for safeguarding data entry so it doesn't get added on user space stuff
    --
    -- Args:
    --  smw_ingore_safeguard - ingore safeguard and return true
    if args == nil then
        args = {}
    end

    if args.smw_ingore_safeguard then
        return true
    end

    local namespace = mw.site.namespaces[mw.title.getCurrentTitle().namespace].name
    if util.smw.data.rejected_namespaces:contains(namespace) then
        return false
    end

    return true
end

-- ----------------------------------------------------------------------------
-- util.string
-- ----------------------------------------------------------------------------

util.string = {}

function util.string.strip(str, pattern)
    pattern = pattern or '%s'
    return string.gsub(str, "^" .. pattern .. "*(.-)" .. pattern .. "*$", "%1")
end

function util.string.split(str, pattern)
    -- Splits string into a table
    --
    -- str: string to split
    -- pattern: pattern to use for splitting
    local out = {}
    local i = 1
    local split_start, split_end = string.find(str, pattern, i)
    while split_start do
        out[#out+1] = string.sub(str, i, split_start-1)
        i = split_end+1
        split_start, split_end = string.find(str, pattern, i)
    end
    out[#out+1] = string.sub(str, i)
    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(str, vars)
    --[[
    Allow string replacement using named arguments.
    
    TODO: 
    * Support %d ?
    * Support 0.2f ?

    Parameters
    ----------
    str : String to replace. 
    vars : Table of arguments.

    Examples
    --------
    = util.string.format('{foo} is {bar}.', {foo='Dinner', bar='nice'})

    References
    ----------
    http://lua-users.org/wiki/StringInterpolation
    ]]

    if not vars then
        vars = str
        str = vars[1]
    end
    
    return (string.gsub(str, "({([^}]+)})",
        function(whole, i)
          return vars[i] or whole
        end))
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.assoc_to_array(tbl, args)
    -- Turn associative array into an array, discarding the values
    local out = {}
    for key, _ in pairs(tbl) do
        out[#out+1] = key
    end
    return out
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

-- ----------------------------------------------------------------------------
-- util.Struct
-- ----------------------------------------------------------------------------

util.Struct = function(map)
    local this = {map = map}


    -- sets a value to a field
    function this:set(field, value)
        if not field or not value then
            error('One or more arguments are nils')
        end

        local _ = self.map[field]

        if not _ then
            error(string.format('Field "%s" doesn\'t exist', field))
        end

        if _.validate then
            _.value = _.validate(value)
        else
            _.value = value
        end

        -- this happen if 'validate' returns nil
        if _.required == true and _.value == nil then
            error(string.format('Field "%s" is required but has been set to nil', field))
        end
    end


    -- adds a new prop to a field
    function this:set_prop(field, prop, value)
        if not field or not prop or not value then
            error('One or more arguments are nils')
        end

        local _ = self.map[field]

        if not _ then
            error(string.format('Field "%s" doesn\'t exist', field))
        end

        _[prop] = value
    end


    -- gets a value from a field
    function this:get(field)
        if not field then
            error('Argument field is nil')
        end

        local _ = self.map[field]

        if not _ then
            error(string.format('Field "%s" doesn\'t exist', field))
        end

        return _.value
    end


    -- gets a value from a prop field
    function this:get_prop(field, prop)
        if not field or not prop then
            error('One or more arguments are nils')
        end

        local _ = self.map[field]

        if not _ then
            error(string.format('Field "%s" doesn\'t exist', field))
        end

        return _[prop]
    end


    -- shows a value from a field
    function this:show(field)
        if not field then
            error('Argument field is nil')
        end

        local _ = self.map[field]

        if not _ then
            error(string.format('Field "%s" doesn\'t exist', field))
        end

        if _.show then
            return _.show(_)
        else
            return _.value
        end
    end


    return this
end

-- ----------------------------------------------------------------------------

return util