Path of Exile Wiki

Please consider helping keep the wiki up to date. Check the to-do list of updates needed for version 3.14.0.

Game data exports will becoming later as the technical changes in addition to regular changes take some more time.

READ MORE

Path of Exile Wiki
(Return empty result set if id_array is empty)
mNo edit summary
(43 intermediate revisions by 3 users not shown)
Line 1: Line 1:
  +
-------------------------------------------------------------------------------
-- Utility stuff
 
  +
--
  +
-- Module:Util
  +
--
  +
-- This meta module contains a number of utility functions
  +
-------------------------------------------------------------------------------
   
 
local xtable = require('Module:Table')
 
local xtable = require('Module:Table')
  +
local m_cargo -- Lazy load require('Module:Cargo')
local util = {}
 
   
  +
-- The cfg table contains all localisable strings and configuration, to make it
local string_format = string.format
 
  +
-- easier to port this module to another wiki.
local infinity = math.huge
 
  +
local cfg = mw.loadData('Module:Util/config')
   
local mw = mw
+
local i18n = cfg.i18n
local cargo = mw.ext.cargo
 
   
  +
local util = {}
 
local i18n = {
 
bool_false = {'false', '0', 'disabled', 'off', 'no', '', 'deactivated'},
 
range = '(%s to %s)',
 
args = {
 
-- util.args.stat
 
stat_infix = 'stat',
 
stat_id = 'id',
 
stat_min = 'min',
 
stat_max = 'max',
 
stat_value = 'value',
 
 
-- util.args.weight_list
 
spawn_weight_prefix = 'spawn_weight',
 
generation_weight_prefix = 'generation_weight',
 
},
 
 
errors = {
 
-- util.cast.factory.*
 
missing_element = 'Element "%s" not found',
 
 
-- util.cast.factory.percentage
 
invalid_argument = 'Argument "%s" is invalid. Please check the documentation for acceptable values.',
 
not_a_percentage = '%s must be a percentage (in range 0 to 100).',
 
 
-- util.cast.boolean
 
not_a_boolean = 'value "%s" of type "%s" is not a boolean',
 
 
-- util.cast.number
 
not_a_number = 'value "%s" of type "%s" is not an integer',
 
number_too_small = '"%i" is too small. Minimum: "%i"',
 
number_too_large = '"%i" is too large. Maximum: "%i"',
 
 
-- util.cast.version
 
malformed_version_string = 'Malformed version string "%s"',
 
non_number_version_component = '"%s" has an non-number component',
 
unrecognized_version_number = '"%s" is not a recognized version number',
 
 
-- util.args.stats
 
improper_stat = '%sstat%s is improperly set; id and either value or min/max must be specified.',
 
 
-- util.args.weight_list
 
invalid_weight = 'Both %s and %s must be specified',
 
 
-- util.args.version
 
too_many_versions = 'The number of results (%s) does not match the number version arguments (%s)',
 
 
-- util.html.error
 
module_error = 'Module Error: ',
 
 
-- util.misc.raise_error_or_return
 
invalid_raise_error_or_return_usage = 'Invalid usage of raise_error_or_return.',
 
 
-- util.cargo.array_query
 
duplicate_ids = 'Found duplicates for field "%s":\n %s',
 
missing_ids = 'Missing results for "%s" field with values: \n%s',
 
 
-- util.smw.array_query
 
duplicate_ids_found = 'Found multiple pages for id property "%s" with value "%s": %s, %s',
 
missing_ids_found = 'No results were found for id property "%s" with the following values: %s',
 
 
-- util.string.split_args
 
number_of_arguments_too_large = 'Number of arguments near = is too large (%s).',
 
},
 
}
 
   
 
-- ----------------------------------------------------------------------------
 
-- ----------------------------------------------------------------------------
Line 82: Line 23:
 
util.cast = {}
 
util.cast = {}
   
function util.cast.boolean(value)
+
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
 
-- Takes an abitary value and casts it to a bool value
 
--
 
--
 
-- for strings false will be according to i18n.bool_false
 
-- 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
Line 226: Line 203:
 
local r = util.table.find_in_nested_array{value=element, tbl=args.tbl, key='full'}
 
local r = util.table.find_in_nested_array{value=element, tbl=args.tbl, key='full'}
 
if r == nil then
 
if r == nil then
error(util.html.error{msg=string.format(args.errmsg or i18n.errors.missing_element, element)})
+
error(string.format(args.errmsg or i18n.errors.missing_element, element))
 
end
 
end
 
end
 
end
Line 238: Line 215:
 
return function (tpl_args, frame)
 
return function (tpl_args, frame)
 
args.value = tpl_args[k]
 
args.value = tpl_args[k]
  +
if args.value == nil then
  +
return
  +
end
 
local value = util.table.find_in_nested_array(args)
 
local value = util.table.find_in_nested_array(args)
 
if value == nil then
 
if value == nil then
error(util.html.error{msg=string.format(args.errmsg or i18n.errors.missing_element, element)})
+
error(string.format(args.errmsg or i18n.errors.missing_element, k))
 
end
 
end
 
tpl_args[args.key_out or k] = value
 
tpl_args[args.key_out or k] = value
Line 382: Line 362:
 
end
 
end
   
function util.args.weight_list (argtbl, args)
+
function util.args.weight_list(argtbl, args)
 
-- Parses a weighted pair of lists and sets properties
 
-- Parses a weighted pair of lists and sets properties
 
--
 
--
Line 390: Line 370:
 
-- frame - if set, automtically set subobjects
 
-- frame - if set, automtically set subobjects
 
-- input_argument - input prefix for parsing the arguments from the argtbl
 
-- input_argument - input prefix for parsing the arguments from the argtbl
-- subobject_name - name of the subobject
+
-- subobject_name - name of the subobject
  +
  +
m_cargo = m_cargo or require('Module:Cargo')
  +
 
args = args or {}
 
args = args or {}
 
args.input_argument = args.input_argument or 'spawn_weight'
 
args.input_argument = args.input_argument or 'spawn_weight'
Line 420: Line 403:
 
 
 
if args.frame and args.cargo_table then
 
if args.frame and args.cargo_table then
util.cargo.store(args.frame, {
+
m_cargo.store(args.frame, {
 
_table = args.cargo_table,
 
_table = args.cargo_table,
 
ordinal = i,
 
ordinal = i,
Line 433: Line 416:
 
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
 
--
 
--
Line 473: Line 456:
 
end
 
end
   
local results = mw.ext.cargo.query(
+
local results = m_cargo.query(
'Versions',
+
{'Versions'},
'release_date, version',
+
{'release_date', 'version'},
 
{
 
{
where=table.concat(version_ids, ' OR '),
+
where = table.concat(version_ids, ' OR '),
 
}
 
}
 
)
 
)
Line 490: Line 473:
 
end
 
end
 
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
 
end
   
Line 497: Line 662:
   
 
util.html = {}
 
util.html = {}
  +
function util.html.abbr(abbr, text, class)
 
  +
function util.html.abbr(text, title, options)
return string.format('<abbr title="%s" class="%s">%s</abbr>', text or '', class or '', abbr or '')
 
  +
-- 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 510: Line 700:
 
end
 
end
   
local err = mw.html.create('span')
+
local err = mw.html.create('strong')
 
err
 
err
:attr('class', 'module-error')
+
:addClass('error')
  +
:tag('span')
:wikitext(i18n.errors.module_error .. (args.msg or ''))
 
:done()
+
: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
  +
class = class and (' ' .. class) or ''
 
return tostring(mw.html.create('em')
 
return tostring(mw.html.create('em')
:attr('class', 'tc -' .. label)
+
:attr('class', 'tc -' .. label .. class)
 
:wikitext(text))
 
:wikitext(text))
 
end
 
end
  +
util.html.poe_colour = util.html.poe_color
   
 
function util.html.tooltip(abbr, text, class)
 
function util.html.tooltip(abbr, text, class)
return string.format('<span class="tooltip-activator %s">%s<span class="tooltip-content">%s</span></span>', class or '', abbr or '', text or '')
+
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
 
end
   
 
util.html.td = {}
 
util.html.td = {}
function util.html.td.na(args)
+
function util.html.td.na(options)
 
--
 
--
-- Args:
+
-- options:
-- as_tag
+
-- as_tag
  +
-- output: set to mw.html to return a mw.html node instead of a string
args = args or {}
 
  +
options = options or {}
 
-- N/A table row, requires mw.html.create instance to be passed
 
-- N/A table row, requires mw.html.create instance to be passed
 
local td = mw.html.create('td')
 
local td = mw.html.create('td')
 
td
 
td
 
:attr('class', 'table-na')
 
:attr('class', 'table-na')
:wikitext('N/A')
+
:wikitext(i18n.na)
 
:done()
 
:done()
if args.as_tag then
+
if options.as_tag or options.output == mw.html then
 
return td
 
return td
else
 
return tostring(td)
 
 
end
 
end
  +
return tostring(td)
 
end
 
end
   
Line 556: Line 750:
 
-- max:
 
-- max:
 
-- options: table
 
-- options: table
-- fmt: formatter to use for the value instead of valfmt
+
-- func: Function to transform the value retrieved from the database
-- fmt_range: formatter to use for the range values. Default: (%s to %s)
+
-- fmt: Format string (or function that returns format string) to use for the value. Default: '%s'
-- inline: Use this format string to insert value
+
-- fmt_range: Format string to use for the value range. Default: '(%s-%s)'
-- inline_color: colour to use for the inline value; false to disable colour
+
-- color: poe_color code to use for the value range. False for no color. Default: 'mod'
-- func: Function to adjust the value with before output
+
-- class: Additional css class added to color tag
-- color: colour code for util.html.poe_color, overrides mod colour
+
-- inline: Format string to use for the output
  +
-- inline_color: poe_color code to use for the output. False for no color. Default: 'default'
-- no_color: set to true to ingore colour entirely
 
  +
-- inline_class: Additional css class added to inline color tag
-- return_color: also return colour
 
  +
-- no_color: (Deprecated; use color=false instead)
 
  +
-- return_color: (Deprecated; returns both value.out and value without this)
if options.no_color == nil then
 
  +
if options.color ~= false and options.no_color == nil then
 
if options.color then
 
if options.color then
 
value.color = options.color
 
value.color = options.color
Line 574: Line 769:
 
end
 
end
 
end
 
end
  +
if options.func then
 
if options.func ~= nil then
 
 
value.min = options.func(tpl_args, frame, value.min)
 
value.min = options.func(tpl_args, frame, value.min)
 
value.max = options.func(tpl_args, frame, value.max)
 
value.max = options.func(tpl_args, frame, value.max)
 
end
 
end
  +
options.fmt = options.fmt or '%s'
 
if options.fmt == nil then
+
if type(options.fmt) == 'function' then -- Function that returns the format string
options.fmt = '%s'
+
options.fmt = options.fmt(tpl_args, frame, value)
elseif type(options.fmt) == 'function' then
 
options.fmt = options.fmt(tpl_args, frame)
 
 
end
 
end
  +
if value.min == value.max then -- Static value
 
if value.min == value.max then
 
 
value.out = string.format(options.fmt, value.min)
 
value.out = string.format(options.fmt, value.min)
else
+
else -- Range value
value.out = string.format(string.format(options.fmt_range or i18n.range, options.fmt, options.fmt), value.min, value.max)
+
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
 
end
  +
if value.color then
 
  +
value.out = util.html.poe_color(value.color, value.out, options.class)
if options.no_color == nil then
 
value.out = util.html.poe_color(value.color, value.out)
 
 
end
 
end
  +
if type(options.inline) == 'function' then
 
  +
options.inline = options.inline(tpl_args, frame, value)
local return_color
 
if options.return_color ~= nil then
 
return_color = value.color
 
 
end
 
end
  +
if options.inline and options.inline ~= '' then
 
local text = options.inline
+
value.out = string.format(options.inline, value.out)
  +
if options.inline_color ~= false then
 
  +
options.inline_color = options.inline_color or 'default'
if type(text) == 'string' then
 
  +
value.out = util.html.poe_color(options.inline_color, value.out, options.inline_class)
elseif type(text) == 'function' then
 
text = text(tpl_args, frame)
 
else
 
text = nil
 
end
 
 
if text and text ~= '' then
 
local color
 
if options.inline_color == nil then
 
color = 'default'
 
elseif options.inline_color ~= false then
 
color = color.inline_color
 
end
 
 
if color ~= nil then
 
text = util.html.poe_color(color, text)
 
 
end
 
end
 
return string.format(text, value.out), return_color
 
 
end
 
end
  +
local return_color = options.return_color and value.color or nil
 
  +
if return_color then
-- If we didn't return before, return here
 
return value.out, return_color
+
return value.out, return_color
  +
end
  +
return value.out, value
 
end
 
end
   
Line 647: Line 825:
 
end
 
end
   
util.misc.category_blacklist = {}
+
function util.misc.maybe_sandbox(frame)
  +
-- Did {{#invoke:}} call sandbox version?
util.misc.category_blacklist.sub_pages = {
 
  +
frame = util.misc.get_frame(frame)
doc = true,
 
sandbox = true,
+
if string.find(frame:getTitle(), 'sandbox', 1, true) then
sandbox2 = true,
+
return true
  +
end
testcases = true,
 
  +
return false
}
 
  +
end
 
util.misc.category_blacklist.namespaces = {
 
Template = true,
 
Template_talk = true,
 
Module = true,
 
Module_talk = true,
 
User = true,
 
User_talk = true,
 
}
 
   
 
function util.misc.add_category(categories, args)
 
function util.misc.add_category(categories, args)
Line 678: Line 848:
 
args = {}
 
args = {}
 
end
 
end
 
   
 
local title = mw.title.getCurrentTitle()
 
local title = mw.title.getCurrentTitle()
local sub_blacklist = args.sub_page_blacklist or util.misc.category_blacklist.sub_pages
+
local sub_blacklist = args.sub_page_blacklist or cfg.misc.category_blacklist.sub_pages
local ns_blacklist = args.namespace_blacklist or util.misc.category_blacklist.namespaces
+
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
Line 761: Line 930:
   
 
-- ----------------------------------------------------------------------------
 
-- ----------------------------------------------------------------------------
-- util.cargo
+
-- util.string
 
-- ----------------------------------------------------------------------------
 
-- ----------------------------------------------------------------------------
   
util.cargo = {}
+
util.string = {}
   
function util.cargo.declare(frame, args)
+
function util.string.strip(str, pattern)
  +
pattern = pattern or '%s'
return frame:callParserFunction('#cargo_declare:', args)
 
  +
return string.gsub(str, "^" .. pattern .. "*(.-)" .. pattern .. "*$", "%1")
 
end
 
end
   
function util.cargo.attach(frame, args)
+
function util.string.split(str, pattern)
  +
-- Splits string into a table
return frame:callParserFunction('#cargo_attach:', args)
 
end
 
 
function util.cargo.store(frame, values, args)
 
-- Calls the cargo_store parser function and ensures the values passed are casted properly
 
 
--
 
--
-- Value handling:
+
-- str: string to split
-- tables - automatically concat
+
-- pattern: pattern to use for splitting
  +
local out = {}
-- booleans - automatically casted to 1 or 0 to ensure they're stored properly
 
--
+
local i = 1
  +
local split_start, split_end = string.find(str, pattern, i)
-- Arguments:
 
  +
while split_start do
-- frame - frame object
 
  +
out[#out+1] = string.sub(str, i, split_start-1)
-- values - table of field/value pairs to store
 
  +
i = split_end+1
-- args - any additional arguments
 
  +
split_start, split_end = string.find(str, pattern, i)
-- sep - separator to use for concat
 
-- store_empty - if specified, allow storing empty rows
 
-- debug - send the converted values to the lua debug log
 
args = args or {}
 
args.sep = args.sep or {}
 
local i = 0
 
for k, v in pairs(values) do
 
i = i + 1
 
if type(v) == 'table' then
 
values[k] = table.concat(v, args.sep[k] or ',')
 
elseif type(v) == 'boolean' then
 
if v == true then
 
v = '1'
 
elseif v == false then
 
v = '0'
 
end
 
values[k] = v
 
end
 
end
 
-- i must be greater then 1 since we at least expect the _table argument to be set, even if no values are set
 
if i > 1 or args.store_empty then
 
if args.debug ~= nil then
 
mw.logObject(values)
 
end
 
return frame:callParserFunction('#cargo_store:', values)
 
 
end
 
end
  +
out[#out+1] = string.sub(str, i)
  +
return out
 
end
 
end
   
function util.cargo.declare_factory(args)
+
function util.string.split_outer(str, pattern, outer)
  +
--[[
-- Returns a function that can be called by templates to declare cargo tables
 
  +
Split a string into a table according to the pattern, ignoring
--
 
  +
matching patterns inside the outer patterns.
-- args
 
-- data: data table
 
-- table: name of cargo table
 
-- fields: associative table with:
 
-- field: name of the field to declare
 
-- type: type of the field
 
return function (frame)
 
frame = util.misc.get_frame(frame)
 
 
 
local dcl_args = {}
+
Parameters
  +
----------
dcl_args._table = args.data.table
 
  +
str : string
for k, field_data in pairs(args.data.fields) do
 
if field_data.field then
+
String to split.
  +
pattern : string
dcl_args[field_data.field] = field_data.type
 
end
+
Pattern to split on.
  +
outer : table of strings where #outer = 2.
end
 
  +
Table with 2 strings that defines the opening and closing patterns
  +
to match, for example parantheses or brackets.
 
 
  +
Returns
return util.cargo.declare(frame, dcl_args)
 
end
+
-------
  +
out : table
end
 
  +
table of split strings.
 
  +
function util.cargo.attach_factory(args)
 
  +
Examples
-- Returns a function that can be called by templates to attach cargo tables
 
--
+
--------
-- args
+
-- Nesting at the end:
  +
str = 'mods.id, CONCAT(mods.id, mods.name)'
-- data: data table
 
  +
mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'}))
-- table: name of cargo table
 
-- fields: associative table with:
+
table#1 {
-- field: name of the field to declare
+
"mods.id",
-- type: type of the field
+
"CONCAT(mods.id, mods.name)",
return function (frame)
+
}
frame = util.misc.get_frame(frame)
 
 
 
local attach_args = {}
+
-- Nesting in the middle:
  +
str = 'mods.id, CONCAT(mods.id, mods.name), mods.required_level'
attach_args._table = args.data.table
 
  +
mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'}))
 
  +
table#1 {
return util.cargo.attach(frame, attach_args)
 
  +
"mods.id",
end
 
  +
"CONCAT(mods.id, mods.name)",
end
 
  +
"mods.required_level",
 
  +
}
--mw.logObject(p.cargo.map_results_to_id{results=mw.ext.cargo.query('mods,spawn_weights', 'mods._pageID,spawn_weights.tag', {where='mods.id="Strength1"', join='mods._pageID=spawn_weights._pageID'}), table_name='mods'})
 
  +
]]
function util.cargo.map_results_to_id(args)
 
-- Maps the results passed to a table containing the specified field as key and a table of rows for the particular page as values.
 
--
 
-- args
 
-- results : table of results returned from mw.ext.cargo.query to map to the specified id field
 
-- field : name of the id field to map results to
 
-- the field has to be in the fields list of the original query or it will cause errors
 
-- keep_id_field : if set, don't delete _pageID
 
--
 
-- return
 
-- table
 
-- key : the specified field
 
-- value : array containing the found rows (in the order that they were found)
 
 
local out = {}
 
local out = {}
  +
local nesting_level = 0
for _, row in ipairs(args.results) do
 
local pid = row[args.field]
+
local i = 0
  +
local pttrn = '(.-)' .. '(' .. pattern .. ')'
if out[pid] then
 
  +
for v, sep in string.gmatch(str, pttrn) do
out[pid][#out[pid]+1] = row
 
  +
if nesting_level == 0 then
  +
-- No nesting is occuring:
  +
out[#out+1] = v
 
else
 
else
out[pid] = {row}
+
-- Nesting is occuring:
  +
out[#out] = (out[math.max(#out, 1)] or '') .. v
 
end
 
end
  +
-- discard the pageID, don't need this any longer in most cases
 
if args.keep_id_field == nil then
+
-- Increase nesting level:
  +
if string.find(v, outer[1]) then -- Multiple matches?
row[args.field] = nil
 
  +
nesting_level = nesting_level + 1
 
end
 
end
  +
if string.find(v, outer[2]) then
end
 
  +
nesting_level = nesting_level - 1
 
return out
 
end
 
 
function util.cargo.query(tables, fields, query, args)
 
-- Wrapper for mw.ext.cargo.query that helps to work around some bugs
 
--
 
-- Current workarounds:
 
-- field names will be "aliased" to themselves
 
--
 
-- Takes 3 arguments:
 
-- tables - array containing tables
 
-- fields - array containing fields; these will automatically be renamed to the way they are specified to work around bugs when results are returned
 
-- query - array containing cargo sql clauses
 
-- args
 
-- args.keep_empty
 
 
-- Cargo bug workaround
 
args = args or {}
 
for i, field in ipairs(fields) do
 
-- already has some alternate name set, so do not do this.
 
if string.find(field, '=') == nil then
 
fields[i] = string.format('%s=%s', field, field)
 
end
 
end
 
local results = cargo.query(table.concat(tables, ','), table.concat(fields, ','), query)
 
if args.keep_empty == nil then
 
for _, row in ipairs(results) do
 
for k, v in pairs(row) do
 
if v == "" then
 
row[k] = nil
 
end
 
end
 
end
 
end
 
return results
 
end
 
 
function util.cargo.array_query(args)
 
-- Performs a long "OR" query from the given array and field validating that there is only exactly one match returned
 
--
 
-- args:
 
-- tables - array of tables (see util.cargo.query)
 
-- fields - array of fields (see util.cargo.query)
 
-- query - array containing cargo sql clauses [optional] (see util.cargo.query)
 
-- id_array - list of ids to query for
 
-- id_field - name of the id field, will be automatically added to fields
 
--
 
-- RETURN:
 
-- table - results as given by mw.ext.cargo.query
 
--
 
args.query = args.query or {}
 
 
args.fields[#args.fields+1] = args.id_field
 
 
if #args.id_array == 0 then
 
return {}
 
end
 
 
local id_array = {}
 
for i, id in ipairs(args.id_array) do
 
id_array[i] = string.format('%s="%s"', args.id_field, id)
 
end
 
if args.query.where then
 
args.query.where = string.format('(%s) AND (%s)', args.query.where, table.concat(id_array, ' OR '))
 
else
 
args.query.where = table.concat(id_array, ' OR ')
 
end
 
 
--
 
-- Check for duplicates
 
--
 
 
-- The usage of distinct should elimate duplicates here from cargo being bugged while still showing actual data duplicates.
 
local results = util.cargo.query(
 
args.tables,
 
{
 
string.format('COUNT(DISTINCT %s._pageID)=count', args.tables[1]),
 
args.id_field,
 
},
 
{
 
join=args.query.join,
 
where=args.query.where,
 
groupBy=args.id_field,
 
having=string.format('COUNT(DISTINCT %s._pageID) > 1', args.tables[1]),
 
}
 
)
 
if #results > 0 then
 
out = {}
 
for _, row in ipairs(results) do
 
out[#out+1] = string.format('%s (%s pages found)', row[args.id_field], row['count'])
 
end
 
error(string.format(i18n.errors.duplicate_ids, args.id_field, table.concat(out, '\n')))
 
end
 
 
--
 
-- Prepare query
 
--
 
if args.query.groupBy then
 
args.query.groupBy = string.format('%s._pageID,%s', args.tables[1], args.query.groupBy)
 
else
 
args.query.groupBy = string.format('%s._pageID', args.tables[1])
 
end
 
 
local results = util.cargo.query(
 
args.tables,
 
args.fields,
 
args.query
 
)
 
 
--
 
-- Check missing results
 
--
 
if #results ~= #args.id_array then
 
local missing = {}
 
for _, id in ipairs(args.id_array) do
 
missing[id] = true
 
end
 
for _, row in ipairs(results) do
 
missing[row[args.id_field]] = nil
 
 
end
 
end
 
 
  +
-- Add back the separator if nesting is occuring:
local missing_ids = {}
 
for k, _ in pairs(missing) do
+
if nesting_level ~= 0 then
missing_ids[#missing_ids+1] = k
+
out[#out] = out[#out] .. sep
end
+
end
 
 
  +
-- Get the last index value:
error(string.format(i18n.errors.missing_ids, args.id_field, table.concat(missing_ids, '\n')))
 
  +
i = i + #v + #sep
 
end
 
end
 
 
  +
-- Complement with the last part of the string:
return results
 
  +
if nesting_level == 0 then
end
 
  +
out[#out+1] = string.sub(str, math.max(i+1, 1))
 
  +
else
-- ----------------------------------------------------------------------------
 
  +
out[#out] = out[#out] .. string.sub(str, math.max(i+1, 1))
-- util.string
 
  +
-- TODO: Check if nesting level is zero?
-- ----------------------------------------------------------------------------
 
 
util.string = {}
 
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
 
end
out[#out+1] = string.sub(str, i)
 
 
return out
 
return out
 
end
 
end
Line 1,069: Line 1,067:
   
 
return out
 
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
 
end
   
Line 1,077: Line 1,116:
 
util.table = {}
 
util.table = {}
 
function util.table.length(tbl)
 
function util.table.length(tbl)
-- Get length of a table when # doesn't work (usually when a table has a metatable)
+
-- Get number of elements in a table. Counts both numerically indexed elements and associative elements. Does not count nil elements.
for i = 1, infinity do
+
local count = 0
if tbl[i] == nil then
+
for _ in pairs(tbl) do
return i - 1
+
count = count + 1
end
 
 
end
 
end
  +
return count
 
end
 
end
  +
util.table.count = util.table.length
   
 
function util.table.assoc_to_array(tbl, args)
 
function util.table.assoc_to_array(tbl, args)
Line 1,189: Line 1,229:
   
 
if not _ then
 
if not _ then
error(string_format('Field "%s" doesn\'t exist', field))
+
error(string.format('Field "%s" doesn\'t exist', field))
 
end
 
end
   
Line 1,200: Line 1,240:
 
-- this happen if 'validate' returns nil
 
-- this happen if 'validate' returns nil
 
if _.required == true and _.value == nil then
 
if _.required == true and _.value == nil then
error(string_format('Field "%s" is required but has been set to nil', field))
+
error(string.format('Field "%s" is required but has been set to nil', field))
 
end
 
end
 
end
 
end
Line 1,214: Line 1,254:
   
 
if not _ then
 
if not _ then
error(string_format('Field "%s" doesn\'t exist', field))
+
error(string.format('Field "%s" doesn\'t exist', field))
 
end
 
end
   
Line 1,230: Line 1,270:
   
 
if not _ then
 
if not _ then
error(string_format('Field "%s" doesn\'t exist', field))
+
error(string.format('Field "%s" doesn\'t exist', field))
 
end
 
end
   
Line 1,246: Line 1,286:
   
 
if not _ then
 
if not _ then
error(string_format('Field "%s" doesn\'t exist', field))
+
error(string.format('Field "%s" doesn\'t exist', field))
 
end
 
end
   
Line 1,262: Line 1,302:
   
 
if not _ then
 
if not _ then
error(string_format('Field "%s" doesn\'t exist', field))
+
error(string.format('Field "%s" doesn\'t exist', field))
 
end
 
end
   

Revision as of 03:22, 11 June 2021

Template info icon Module documentation[view] [edit] [history] [purge]

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

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