Module:Util

-- Utility stuff

local xtable = require('Module:Table') local util = {}

local string_format = string.format local infinity = math.huge

local mw = mw

local i18n = { bool_false = {'false', '0', 'disabled', 'off', 'no', '', 'deactivated'}, 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.smw.array_query duplicate_ids_found = 'Found multiple pages for id property "%s" with value "%s": %s, %s', missing_ids = '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).', }, }

-- -- util.cast --

util.cast = {}

function util.cast.boolean(value) -- Takes an abitary value and casts it to a bool value --   -- for strings false will be according to i18n.bool_false local t = type(value) if t == 'nil' then return false 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, 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 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], ', ') 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(util.html.error{msg=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] local value = util.table.find_in_nested_array(args) if value == nil then error(util.html.error{msg=string.format(args.errmsg or i18n.errors.missing_element, element)}) 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], ', ') 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 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 = {}

util.args.stat_properties = { id = 'Has %sstat id', min = 'Has minimum %sstat value', max = 'Has maximum %sstat value', avg = 'Has average %sstat value', value = 'Has %sstat value', }

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 '' args.property_prefix = args.property_prefix or '' args.subobject_prefix = args.subobject_prefix or '' args.properties = args.properties or {}

local i = 0 local stats = {} repeat i = i + 1 local id = { id = string.format('%sstat%s_id', args.prefix, i), min = string.format('%sstat%s_min', args.prefix, i), max = string.format('%sstat%s_max', args.prefix, i), value = string.format('%sstat%s_value', args.prefix, i), }

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 local properties = {} 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

if args.frame ~= nil then local properties = {} for property, value in pairs(args.properties) do                   properties[property] = value end

for key, property in pairs(util.args.stat_properties) do                   properties[string.format(property, args.property_prefix)] = value[key] end

util.smw.subobject(args.frame, string.format('%sstat%s_%s', args.subobject_prefix, i, value.id), properties) end 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.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 -- weight_property - name of the weight property on the subobject -- subobject_name - name of the subobject args = args or {} args.input_argument = args.input_argument or 'spawn_weight' args.weight_property = args.weight_proprety or 'Has spawn weight' args.subobject_name = args.subobject_name 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 then properties = {} properties['Is tag number'] = i               properties['Has tag'] = value.tag properties[args.weight_property] = util.cast.number(value.value, {min=0}) util.smw.subobject(args.frame, string.format('%s %s', args.subobject_name, i), properties) 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 of    --   property: property; if not set skip fetching and setting release date args = args or {} args.variables = args.variables or { release = { property = 'Has release', },       removal = { property = 'Has 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

local query = {} query[#query+1] = string.format('Is version::%s', table.concat(version_ids, '||')) query[#query+1] = '?Has release date#' query[#query+1] = '?Is version'

local results = util.smw.query(query, args.frame)

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['Is version']] argtbl[string.format('%s_date', key)] = row['Has release date'] end end

if args.set_properties ~= nil then local properties = {} for key, data in pairs(args.variables) do           if data.property ~= nil then properties[string.format('%s version', data.property)] = argtbl[string.format('%s_version', key)] properties[string.format('%s date', data.property)] = argtbl[string.format('%s_date', key)] end end

util.smw.set(args.frame, properties) end end

-- -- util.html --

util.html = {} function util.html.abbr(abbr, text, class) return string.format('%s ', text or , class or , abbr or '') 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('span') err :attr('class', 'module-error') :wikitext(i18n.errors.module_error .. (args.msg or '')) :done

return tostring(err) end

function util.html.poe_color(label, text) if text == nil or text == '' then return nil end return tostring(mw.html.create('em')       :attr('class', 'tc -' .. label)       :wikitext(text)) end

function util.html.tooltip(abbr, text, class) return string.format(' %s %s ', class or , abbr or , text or '') end

util.html.td = {} function util.html.td.na(args) --   -- Args: -- as_tag args = args 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('N/A') :done if args.as_tag then return td   else return tostring(td) end 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

util.misc.category_blacklist = {} util.misc.category_blacklist.sub_pages = { doc = true, sandbox = true, sandbox2 = true, testcases = true, }

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) -- 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 util.misc.category_blacklist.sub_pages local ns_blacklist = args.namespace_blacklist or util.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('', 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._parser_function(frame, parser_function, args) -- Executes a semantic parser functions and sets the arguments args --   -- This function is a helper for handling tables since +sep= parameter -- appears to be broken. --   -- frame          : frame object -- parser_function: the whole parser function string -- args          : table of arguments for k, v in pairs(args) do       if type(v) == 'table' then for _, value in ipairs(v) do               frame:callParserFunction(parser_function, {[k] = value}) end args[k] = nil elseif type(v) == 'boolean' then args[k] = tostring(v) end end frame:callParserFunction(parser_function, args) end

function util.smw.set(frame, args) util.smw._parser_function(frame, '#set:', args) end

function util.smw.subobject(frame, id, args) util.smw._parser_function(frame, '#subobject:' .. id, args) end

function util.smw.query(query, frame) -- Executes a semantic media wiki #ask query and returns the result as an   -- array containing each row as table. --   -- query: table of query arguments to pass -- frame: current frame object

-- the characters here for sep/header/propsep are control characters; I'm farily certain they should not appear in regular text. query.sep = '�' query.propsep = '' query.headersep = '' query.format = 'array' query.headers = 'plain'

local result = frame:callParserFunction('#ask', query)

-- " Some subquery has no valid condition. " if mw.ustring.find(result, 'data%-title="Error"') ~= nil then error(mw.ustring.sub(result, mw.ustring.find(result, '<span class="smw-highlighter"', 1, true), -1)) end

local out = {}

for row_string in string.gmatch(result, '[^�]+') do       local row = {} for _, str in ipairs(util.string.split(row_string, query.propsep)) do           local kv = util.string.split(str, query.headersep) if #kv == 1 then row[#row+1] = kv[1] elseif #kv == 2 then row[kv[1]] = kv[2] end end out[#out+1] = row end

return out end

function util.smw.array_query(args) -- Performs a long "OR" query from the given array and properties and returns the results with the property as key. -- This function is neccessary because on the limit on query size, so multiple queries will be peformed. --    -- REQUIRED: -- frame: frame -- property: id property that will be used; will also be used to set the output table accordingly -- id_array: list of ids to perform queries for --   -- OPTIONAL: -- conditions: any extra conditions -- query: fields to pass to the query (like the fields you want to return, other smw query options, etc) -- result_key: override property in the output table -- max_array_size: Maximum elements from the array to insert into the property field --                 May need to be lowered for more complex queries --                 Defaults to 9 -- error_on_missing: If set to true, it will validate the number of results returned --   -- RETURN: -- assoc table - associative table with the property as key containing the query results args.conditions = args.conditions or '' args.query = args.query or {} args.max_array_size = args.max_array_size or 11 args.error_on_missing = args.error_on_missing or false args.result_key = args.result_key or args.property -- first field will hold the query table.insert(args.query, 1, '')

local qresults = {} for i=0,(#args.id_array-1)/args.max_array_size do       local query_ids_slice = {} for j=i*args.max_array_size+1, (i+1)*args.max_array_size do           query_ids_slice[#query_ids_slice+1] = args.id_array[j] end -- we can just reuse the query object here, no need to recreate it       args.query[1] = string.format('%s::%s', args.property, table.concat(query_ids_slice, '||')) .. args.conditions local results = util.smw.query(args.query, args.frame) for _, result in ipairs(results) do           if qresults[result[args.result_key]] ~= nil then error(string.format(i18n.errors.duplicate_ids_found, args.result_key, result[args.result_key], qresults[result[args.result_key]][1], result[1])) end qresults[result[args.result_key]] = result end end if args.error_on_missing then local missing = {} for _, id in ipairs(args.id_array) do           if qresults[id] == nil then missing[#missing+1] = id           end end if #missing > 0 then error(string.format(i18n.errors.missing_ids, args.property, table.concat(missing, ', '))) end end return qresults end

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

util.cargo = {}

function util.cargo.declare(frame, args) return frame:callParserFunction('#cargo_declare:', args) end

function util.cargo.store(frame, args) return frame:callParserFunction('#cargo_store:', args) end

-- -- util.string --

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 out[#out+1] = string.sub(str, i)   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

-- -- util.table --

util.table = {} function util.table.length(tbl) -- Get length of a table when # doesn't work (usually when a table has a metatable) for i = 1, infinity do       if tbl[i] == nil then return i - 1 end end 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