Module:Util: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
>OmegaK2
(fixed error when using a section without a field type)
No edit summary
 
(76 intermediate revisions by 5 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 -- Lazy load require('Module:Table')
local util = {}
local getArgs -- Lazy load require('Module:Arguments').getArgs
local m_cargo -- Lazy load require('Module:Cargo')


local string_format = string.format
-- The cfg table contains all localisable strings and configuration, to make it
local infinity = math.huge
-- easier to port this module to another wiki.
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.args.from_cargo_map
        missing_key_in_fields = 'Key "%s" not found in the fields mapping of table "%s"',
        table_object_as_default = 'Warning: table object as default value on key "%s" in mapping of table "%s"',
        missing_key_in_order = 'Fields mapping of table "%s" has the following extra keys that are not handled by order:\n%s',
        handler_returned_nil = 'Handler for "%s.fields.%s" returned nil for argument "%s". Check whether the value is correct for the given field type "%s".',
        argument_required = 'Argument "%s" is required',
       
        -- 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 88: Line 23:


util.cast = {}
util.cast = {}
function util.cast.text(value, args)
    -- Takes an arbitary value and converts it to text.
    --
    -- Also strips any categories
    --
    -- args
    --  cast_nil      - Cast lua nil value to "nil" string
    --                  Default: false
    --  discard_empty - if the string is empty, return nil rather then empty string
    --                  Default: true
    args = args or {}
    if args.discard_empty == nil then
        args.discard_empty = true
    end
   
    if value == nil and not args.cast_nil then
        return
    end
   
    value = tostring(value)
    if value == '' and args.discard_empty then
        return
    end
   
    -- Reassign to variable before returning since string.gsub returns two values
    value = string.gsub(value, '%[%[Category:[%w_ ]+%]%]', '')
    return value
end


function util.cast.boolean(value, args)
function util.cast.boolean(value, args)
     -- Takes an abitary value and casts it to a bool value
     -- Takes an arbitrary value and attempts to convert it to a boolean.
     --
     --
     -- for strings false will be according to i18n.bool_false
     -- for strings false will be according to i18n.bool_false
     --
     --
     -- args:
     -- args
     --  cast_nil - if set to false, it will not cast nil values
     --  cast_nil if set to false, it will not cast nil values
     args = args or {}
     args = args or {}
     local t = type(value)
     local t = type(value)
Line 124: Line 88:


function util.cast.number(value, args)
function util.cast.number(value, args)
     -- Takes an abitary value and attempts to cast it to int
     -- Takes an arbitrary value and attempts to convert it to a number.
     --
     --
     -- args:
     -- args
     --  default: for strings, if default is nil and the conversion fails, an error will be returned
     --  default for strings, if default is nil and the conversion fails, an error will be returned
     --  min: error if <min
     --  min error if <min
     --  max: error if >max
     --  max error if >max
     if args == nil then
     args = args or {}
        args = {}
    end


     local t = type(value)
     local t = type(value)
Line 168: Line 130:


     return val
     return val
end
function util.cast.table(value, args)
    -- Takes an arbitrary value and attempts to convert it to a table.
    --
    -- args
    --  split_args  If true, create an association table (rather than an array)
    --  pattern  The pattern to split strings by. Default: ',%s*'
    --  split_args_pattern  The pattern to split keys from values by. Ignored if split_args is not true.
    --      Default: '%s*=%s*'
    --  callback  A callback function to call on each value
    args = args or {}
    local pattern = args.pattern or ',%s*'
    local split_args_pattern = args.split_args_pattern or '%s*=%s*'
    local tbl
    if type(value) == 'string' then
        if args.split_args then
            tbl = util.string.split_args(value, { sep = pattern, kvsep = split_args_pattern } )
        else
            tbl = util.string.split(value, pattern)
        end
    elseif type(value) ~= 'table' then
        tbl = {value}
    else
        tbl = value
    end
    if args.callback then
        for k, v in ipairs(tbl) do
            tbl[k] = args.callback(v)
        end
    end
    return tbl
end
end


Line 210: Line 204:


     return result
     return result
end
function util.cast.replace_if_match(value, args)
    -- Returns a function that returns its input unchanged, unless the string value
    -- matches the 'pattern' argument, in which case the 'replacewith' value is returned.
    if ((args == nil) or (args.pattern == nil) or (value == nil)) then
        return value
    elseif string.find(tostring(value),args.pattern) then
        return args.replacewith
    else
        return value
    end
end
-- ----------------------------------------------------------------------------
-- util.validate
-- ----------------------------------------------------------------------------
util.validate = {}
util.validate.factory = {}
function util.validate.factory.number_in_range(args)
    -- Returns a function that validates whether a number is within a range of
    -- values. An error is thrown if the value is not a number or if it is not
    -- within the specified range.
    args = args or {}
    args.min = args.min or -math.huge
    args.max = args.max or math.huge
    return function (value)
        if type(value) ~= 'number' then
            error(string.format(i18n.errors.not_a_number, tostring(value), type(value)))
        end
        if value < args.min or value > args.max then
            error(string.format(args.errmsg or i18n.errors.number_out_of_range, tostring(value), tostring(args.min), tostring(args.max)), args.errlvl or 2)
        end
        return value
    end
end
function util.validate.factory.string_length(args)
    -- Returns a function that validates whether a string has has the correct
    -- length. An error is thrown if the value is not a string or if its length
    -- restrictions are not met.
    args = args or {}
    args.min = args.min or 0
    args.max = args.max or math.huge
    return function (value)
        if type(value) ~= 'string' then
            error(string.format(i18n.errors.not_a_string, tostring(value), type(value)))
        end
        local length = mw.ustring.len(value)
        if length < args.min or length > args.max then
            error(string.format(args.errmsg or i18n.errors.string_length_incorrect, tostring(value), tostring(args.min), tostring(args.max)), args.errlvl or 2)
        end
        return value
    end
end
function util.validate.factory.in_table(args)
    -- Returns a function that validates whether a table contains a value.
    -- An error is thrown if the value is not found.
    args = args or {}
    return function (value)
        if not util.table.contains(args.tbl or {}, value) then
            error(string.format(args.errmsg or i18n.errors.value_not_in_table, tostring(value)), args.errlvl or 2)
        end
        return value
    end
end
function util.validate.factory.in_table_keys(args)
    -- Returns a function that validates whether a table has a value as one of
    -- its keys. An error is thrown if the key does not exist.
    args = args or {}
    return function (value)
        if not util.table.has_key(args.tbl or {}, value) then
            error(string.format(args.errmsg or i18n.errors.value_not_in_table_keys, tostring(value)), args.errlvl or 2)
        end
        return value
    end
end
end


Line 232: Line 306:
     --  tbl - table to check against
     --  tbl - table to check against
     --  errmsg - error message if no element was found; should accept 1 parameter
     --  errmsg - error message if no element was found; should accept 1 parameter
    xtable = xtable or require('Module:Table')
     args = args or {}
     args = args or {}
     return function (tpl_args, frame)
     return function (tpl_args, frame)
Line 330: Line 405:
     -- args:
     -- args:
     --  prefix: prefix if any
     --  prefix: prefix if any
    --  frame: frame used to set subobjects; if not set dont set properties
     --  property_prefix: property prefix if any
     --  property_prefix: property prefix if any
     --  subobject_prefix: subobject prefix if any
     --  subobject_prefix: subobject prefix if any
Line 400: Line 474:
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 406: Line 480:
     -- args:
     -- args:
     --  output_argument - if set, set arguments to this value
     --  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
     --  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 437: Line 513:
             end
             end
              
              
             if args.frame and args.cargo_table then
             if args.cargo_table then
                 util.cargo.store(args.frame, {
                 m_cargo.store({
                     _table = args.cargo_table,
                     _table = args.cargo_table,
                     ordinal = i,
                     ordinal = i,
Line 451: Line 527:
end
end


function util.args.version (argtbl, args)
function util.args.version(argtbl, args)
     -- in any prefix spaces should be included
     -- in any prefix spaces should be included
     --
     --
     -- argtbl: argument table to work with
     -- argtbl: argument table to work with
     -- args:
     -- args:
    --  frame: frame for queries
     --  set_properties: if defined, set properties on the page
     --  set_properties: if defined, set properties on the page
     --  variables: table of prefixes
     --  variables: table of prefixes
    --  ignore_unknowns: if defined, treat a version number of '?' as if it
    --    were not present
    --  noquery: For testing; if defined, skips the query
    --  return_ids_and_keys: For testing; on return, args.version_ids and
    --    args.versionkeys are set to the IDs and keys found
     args = args or {}
     args = args or {}
     args.variables = args.variables or {
     args.variables = args.variables or {
Line 465: Line 545:
     }
     }


     local version_ids = {}
     local version_ids={}
     local version_keys = {}
     local version_keys={}


     for key, data in pairs(args.variables) do
     for key, data in pairs(args.variables) do
         local full_key = string.format('%s_version', key)
         local full_key = string.format('%s_version', key)
         if argtbl[full_key] ~= nil then
         if args.ignore_unknowns and (argtbl[full_key] == '?') then
            argtbl[full_key] = nil
        elseif argtbl[full_key] ~= nil then
             local value = util.cast.version(argtbl[full_key], {return_type = 'string'})
             local value = util.cast.version(argtbl[full_key], {return_type = 'string'})
             argtbl[full_key] = value
             argtbl[full_key] = value
             data.value = value
             if value ~= nil then
            if data.property ~= nil then
                data.value = value
                version_ids[#version_ids+1] = value
                if data.property ~= nil then
                version_keys[value] = key
                    version_ids[#version_ids+1] = value
                    version_keys[value] = key
                end
             end
             end
         end
         end
Line 482: Line 566:


     -- no need to do a query if nothing was fetched
     -- no need to do a query if nothing was fetched
     if #version_ids > 0 then
     if (args.noquery == nil) and (#version_ids > 0) then
        if args.frame == nil then
            error('Properties were set, but frame was not')
        end
       
         for i, id in ipairs(version_ids) do
         for i, id in ipairs(version_ids) do
             version_ids[i] = string.format('Versions.version="%s"', id)
             version_ids[i] = string.format('Versions.version="%s"', id)
         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 507: Line 587:
             argtbl[string.format('%s_date', key)] = row.release_date
             argtbl[string.format('%s_date', key)] = row.release_date
         end
         end
    end
    if args.return_ids_and_keys ~= nil then
        args.version_ids = version_ids
        args.version_keys = version_keys
     end
     end
end
end


function util.args.from_cargo_map(args)
function util.args.from_cargo_map(args)
     -- Maps the arguments from a cargo argument table (i.e. the ones used in util.cargo.declare_factory)
     m_cargo = m_cargo or require('Module:Cargo')
    --
    return m_cargo.store_mapped_args(args)
    -- It will expect/handle the following fields:
end
    -- 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)
function util.args.template_to_lua(str)
     -- map.fields[id].field    - REQUIRED - Name of the field in cargo table
     --[[
    -- map.fields[id].type    - REQUIRED - Type of the field in cargo table
     Convert templates to lua format. Simplifes debugging and creating
     -- map.fields[id].func    - OPTIONAL - Function to handle the arguments. It will be passed tpl_args and frame.
     examples.
     --                                      The function should return the parsed value.
      
     --                                   
     Parameters
     --                                      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
     str : string
    --                                     If default is a function, the function will be called with (tpl_args, frame) and expected to return a default value for the field.
        The entire template wrapped into string. Tip: Use Lua's square
    -- 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
        bracket syntax for defining string literals.
     -- 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
     Returns
     --  
     -------
    --
     out : table
    -- Expects argument table.
        out.template - Template name.
     -- REQUIRED:
        out.args - arguments in table format.
    --  tpl_args  - arguments passed to template after preprecessing
        out.args_to_str - arguments in readable string format.
    --  frame    - frame object
     ]]
    --  table_map - table mapping object
     local out = {}
    --  rtr      - if set return cargo props instead of storing them
    local tpl_args = args.tpl_args
     local frame = args.frame
     local map = args.table_map
      
      
     local cargo_values = {_table = map.table}
     -- Get the template name:
    out.template = string.match(str, '{{(.-)%s*|')
      
      
     -- for checking missing keys in order
     -- Remove everything but the arguments:
     local available_fields = {}
     str = string.gsub(str, '%s*{{.-|', '')
     for key, _ in ipairs(map.fields) do
     str = string.gsub(str, '%s*}}%s*', '')
        available_fields[key] = true
    end
      
      
     -- main loop
     -- Split up the arguments:
     for _, key in ipairs(map.order) do
    out.args = {}
        local field = map.fields[key]
     for i, v in ipairs(util.string.split(str, '%s*|%s*')) do
        if field == nil then
         local arg = util.string.split(v, '%s*=%s*')
            error(string.format(i18n.errors.missing_key_in_fields, key, map.table))
        out.args[arg[1]] = arg[2]
        else
         out.args[#out.args+1] = arg[1]
            available_fields[key] = nil
     end  
        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
        if field.func ~= nil then
            value = field.func(tpl_args, frame)
        -- automatic handling only works if the field type is set
        elseif field.type ~= nil then
            value = tpl_args[args_key]
           
            local cfield = util.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
        -- 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
     -- Concate for easy copy/pasting:
     local missing = {}
     local tbl = {}
     for key, _ in pairs(available_fields) do
     for i, v in ipairs(out.args) do  
         missing[#missing+1] = key
         tbl[#tbl+1]= string.format("%s='%s'", v, out.args[v])
     end
     end  
     if #missing > 0 then
     out.args_to_str = table.concat(tbl, ',\n')
        error(string.format(i18n.errors.missing_key_in_order, map.table, table.concat('missing', '\n')))
    end
      
      
     -- finally store data in DB
     return out
    if args.rtr ~= nil then
        return cargo_values
    else
        util.cargo.store(frame, cargo_values)
    end
end
end


Line 645: Line 650:


util.html = {}
util.html = {}
function util.html.abbr(abbr, text, class)
 
     return string.format('<abbr title="%s" class="%s">%s</abbr>', text or '', class or '', abbr or '')
function util.html.abbr(text, title, options)
    -- Outputs html tag <abbr> as string or as mw.html node.
    --
    -- options
    --  class: class attribute
     --  output: set to mw.html to return a mw.html node instead of a string
    if not title then
        return text
    end
    options = options or {}
    local abbr = mw.html.create('abbr')
    abbr:attr('title', title)
    local class
    if type(options) == 'table' and options.class then
        class = options.class
    else
        class = options
    end
    if type(class) == 'string' then
        abbr:attr('class', class)
    end
    abbr:wikitext(text)
    if options.output == mw.html then
        return abbr
    end
    return tostring(abbr)
end
end


Line 652: Line 682:
     -- Create an error message box
     -- Create an error message box
     --
     --
     -- Args:
     -- args
     --  msg - message
     --   msg str  The error message
     if args == nil then
     args = args or {}
        args = {}
     local err = mw.html.create('strong')
    end
        :addClass('error')
 
         :tag('span')
     local err = mw.html.create('span')
            :addClass('module-error')
    err
            :wikitext(i18n.errors.module_error .. (args.msg or ''))
         :attr('class', 'module-error')
            :done()
        :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
     return tostring(mw.html.create('em')
     local em = mw.html.create('em')
         :attr('class', 'tc -' .. label)
         :addClass('tc -' .. label)
         :wikitext(text))
        :addClass(class or '')
         :wikitext(text)
    return tostring(em)
end
end
util.html.poe_colour = util.html.poe_color
util.html.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
     args = args or {}
     --  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
     -- 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


function util.html.format_value(tpl_args, frame, value, options)
function util.html.format_value(tpl_args, value, options)
     -- value: table
     -- value: table
     --  min:
     --  min:
     --  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.
     --  inline: Use this format string to insert value
    --    Default: '%s'
     -- inline_color: colour to use for the inline value; false to disable colour
     --  fmt_range: Format string to use for range value.
     --  func: Function to adjust the value with before output
    --    Default: '(%s-%s)'
     --  color: colour code for util.html.poe_color, overrides mod colour
     --  color: poe_color code to use for the value. False for no color.
     --  no_color: set to true to ingore colour entirely
     --   Default: 'value' if value is unmodified; 'mod' if modified
     --  return_color: also return colour
    --  class: Additional css class added to color tag
     --  inline: Format string to use for the output
     --  inline_color: poe_color code to use for the output. False for no color.
    --    Default: Inherits from value color
    --  inline_class: Additional css class added to inline color tag
     --  no_color: (Deprecated; use color=false instead)
     --  return_color: (Deprecated; returns both value.out and value without this)


     if options.no_color == nil then
     -- Make shallow copy to avoid modifying the original table
        if options.color then
    local value_copy = {}
            value.color = options.color
    for k, v in pairs(value) do
         elseif value.base ~= value.min or value.base ~= value.max then
        value_copy[k] = v
            value.color = 'mod'
    end
        else
    local default_color = 'value'
            value.color = 'value'
    local base = {
        end
        min = value_copy.base_min or value_copy.base,
         max = value_copy.base_max or value_copy.base,
    }
    if value_copy.min ~= base.min or value_copy.max ~= base.max then
        default_color = 'mod'
     end
     end
   
     if options.color ~= false and options.no_color == nil then
     if options.func ~= nil then
         value_copy.color = options.color or default_color
         value.min = options.func(tpl_args, frame, value.min)
        value.max = options.func(tpl_args, frame, value.max)
     end
     end
   
     if options.func then
     if options.fmt == nil then
         value_copy.min = options.func(tpl_args, value_copy.min)
         options.fmt = '%s'
         value_copy.max = options.func(tpl_args, value_copy.max)
    elseif type(options.fmt) == 'function' then
         options.fmt = options.fmt(tpl_args, frame)
     end
     end
      
     local fmt = options.fmt or '%s'
     if value.min == value.max then
     if type(fmt) == 'function' then -- Function that returns the format string
         value.out = string.format(options.fmt, value.min)
         fmt = fmt(tpl_args, value_copy)
    else
        value.out = string.format(string.format(options.fmt_range or i18n.range, options.fmt, options.fmt), value.min, value.max)
     end
     end
   
     if value_copy.min == value_copy.max then -- Static value
     if options.no_color == nil then
         value_copy.out = string.format(fmt, value_copy.min)
         value.out = util.html.poe_color(value.color, value.out)
    else -- Range value
        local fmt_range = options.fmt_range or i18n.range
        value_copy.out = string.format(
            string.format(fmt_range, fmt, fmt),
            value_copy.min,
            value_copy.max
        )
     end
     end
   
     local inline = options.inline
     local return_color
     if type(inline) == 'function' then
     if options.return_color ~= nil then
         inline = inline(tpl_args, value_copy)
         return_color = value.color
     end
     end
      
     inline = inline ~= '' and inline or nil -- TODO: Eliminate the need for this?
     local text = options.inline
     local inline_color = options.inline_color
   
     if value_copy.color and (not inline or inline_color ~= nil) then
     if type(text) == 'string' then
         value_copy.out = util.html.poe_color(value_copy.color, value_copy.out, options.class)
    elseif type(text) == 'function' then
         text = text(tpl_args, frame)
    else
        text = nil
     end
     end
   
     if inline then
     if text and text ~= '' then
         value_copy.out = string.format(inline, value_copy.out)
         local color
         if inline_color or inline_color == nil and options.color ~= false then
         if options.inline_color == nil then
             inline_color = inline_color or value_copy.color or default_color
            color = 'default'
             value_copy.out = util.html.poe_color(inline_color, value_copy.out, options.inline_class)
        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
      
     if options.return_color then
     -- If we didn't return before, return here
        return value_copy.out, value_copy.color
     return value.out, return_color
    end
    return value_copy.out, value_copy
end
 
function util.html.wikilink(page, text)
     if text then
        return string.format('[[%s|%s]]', page, text)
    end
    return string.format('[[%s]]', page)
end
 
function util.html.url_link(url, text)
     return string.format('[%s %s]', url, text)
end
end


Line 783: Line 821:


util.misc = {}
util.misc = {}
function util.misc.invoker_factory(func, options)
    -- Returns a function that can be called directly or with #invoke.
    return function (frame)
        frame = frame or {}
        local args
        if type(frame.args) == 'table' then
            -- Called via #invoke, so use getArgs().
            getArgs = getArgs or require('Module:Arguments').getArgs
            args = getArgs(frame, options)
        else
            -- Called from another module or from the debug console, so assume args
            -- are passed in directly.
            args = frame
        end
        return func(args)
    end
end
function util.misc.is_frame(frame)
function util.misc.is_frame(frame)
     -- the type of the frame is a table containing the functions, so check whether some of these exist
     -- the type of the frame is a table containing the functions, so check whether some of these exist
Line 790: Line 847:


function util.misc.get_frame(frame)
function util.misc.get_frame(frame)
    -- OBSOLETE. Use mw.getCurrentFrame() instead.
    return mw.getCurrentFrame()
end
function util.misc.get_args_raw(frame)
    -- Simple method for getting arguments. Use this instead of Module:Arguments
    -- when the extra options provided by the latter would be overkill.
     if util.misc.is_frame(frame) then
     if util.misc.is_frame(frame) then
         return frame
        -- Called via {{#invoke:}}, so use the args that were passed into the
        -- template.
         return frame.args
     end
     end
     return mw.getCurrentFrame()
    -- Called from another module or from the debug console, so assume args
    -- are passed in directly.
     return frame
end
end


util.misc.category_blacklist = {}
function util.misc.maybe_sandbox(module_name)
util.misc.category_blacklist.sub_pages = {
    -- Did we load or {{#invoke:}} a module sandbox?
    doc = true,
    if module_name and package.loaded[string.format('Module:%s/sandbox', module_name)] ~= nil or string.find(mw.getCurrentFrame():getTitle(), 'sandbox', 1, true) then
    sandbox = true,
        return true
    sandbox2 = 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 817: Line 876:
     -- args: table of extra arguments
     -- args: table of extra arguments
     --  namespace: id of namespace to validate against
     --  namespace: id of namespace to validate against
     --  ingore_blacklist: set to non-nil to ingore the blacklist
     --  ignore_blacklist: set to non-nil to ignore the blacklist
     --  sub_page_blacklist: blacklist of subpages to use (if empty, use default)
     --  sub_page_blacklist: blacklist of subpages to use (if empty, use default)
     --  namespace_blacklist: blacklist of namespaces to use (if empty, use default)
     --  namespace_blacklist: blacklist of namespaces to use (if empty, use default)
Line 827: Line 886:
         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 837: Line 895:
     end
     end


     if args.ingore_blacklist == nil and (sub_blacklist[title.subpageText] or ns_blacklist[title.subjectNsText]) then
     if args.ignore_blacklist == nil and (sub_blacklist[title.subpageText] or ns_blacklist[title.subjectNsText]) then
         return ''
         return ''
     end
     end
Line 847: Line 905:
     end
     end
     return table.concat(cats)
     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
end


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


util.smw = {}
-- Prototype error object
 
local Error_prototype = {
util.smw.data = {}
    message = i18n.errors.unspecified,
util.smw.data.rejected_namespaces = xtable:new({'User'})
    code = 'module_error',
    issue = true, -- Whether to issue error
    level = 2,
}
Error_prototype.__index = Error_prototype


function util.smw.safeguard(args)
function Error_prototype:throw(force)
    -- Used for safeguarding data entry so it doesn't get added on user space stuff
     if force or self.issue then
    --
         error(self.message, self.level)
    -- Args:
    --  smw_ingore_safeguard - ingore safeguard and return true
     if args == nil then
         args = {}
     end
     end
    return self
end


     if args.smw_ingore_safeguard then
function Error_prototype:get_html()
        return true
     return util.html.error{msg=self.message}
    end
end


    local namespace = mw.site.namespaces[mw.title.getCurrentTitle().namespace].name
function Error_prototype:get_category(args)
     if util.smw.data.rejected_namespaces:contains(namespace) then
     return util.misc.add_category(self.category, args)
        return false
end
    end


     return true
function util.Error(obj)
    -- Create a new error object
    obj = obj or {}
    setmetatable(obj, Error_prototype)
     return obj
end
end


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


util.cargo = {}
util.string = {}
 
function util.string.trim(str, charset)
    --[[
    Trims leading and trailing characters in charset from a string.
    Charset is '%s' by default, which matches whitespace characters


function util.cargo.declare(frame, args)
    This works much like mw.text.trim, using the string library instead
     return frame:callParserFunction('#cargo_declare:', args)
     of the ustring library. This function may return erroneous results
    if the charset needs to be Unicode-aware.
    --]]
    charset = charset or '%s'
    str = string.gsub(str, '^[' .. charset .. ']*(.-)[' .. charset .. ']*$', '%1')
    return str
end
end


function util.cargo.attach(frame, args)
function util.string.strip_wikilinks(str)
     return frame:callParserFunction('#cargo_attach:', args)
    --[[
    Removes wikilinks from a string, leaving the plain text
    --]]
    str = mw.ustring.gsub(str, '%[%[:?([^%]|]+)%]%]', '%1')
     str = mw.ustring.gsub(str, '%[%[:?[^|]+|([^%]|]+)%]%]', '%1')
    return str
end
end


function util.cargo.store(frame, values, args)
function util.string.strip_html(str)
     -- Calls the cargo_store parser function and ensures the values passed are casted properly
     --[[
    --
     Removes html tags from a string, leaving the plain text
    -- Value handling:
     --]]
    --  tables  - automatically concat
     str = mw.ustring.gsub(str, '<[^>]*>', '')
    --  booleans - automatically casted to 1 or 0 to ensure they're stored properly
     return str
    --
    -- Arguments:
    --  frame        - frame object
    --  values      - table of field/value pairs to store
     --  args        - any additional arguments
    --  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
end


function util.cargo.declare_factory(args)
function util.string.split(str, pattern, plain)
     -- Returns a function that can be called by templates to declare cargo tables
     --[[
     --
    Splits a string into a table
     -- args
      
     --  data: data table
     This does essentially the same thing as mw.text.split, but with
     --  table: name of cargo table
     significantly better performance. This function may return erroneous
     --  fields: associative table with:
     results if the pattern needs to be Unicode-aware.
     --    field: name of the field to declare
      
     --    type: type of the field
     str  String to split
     return function (frame)
     pattern Pattern to use for splitting
        frame = util.misc.get_frame(frame)
     plain  If true, pattern is interpreted as a literal string
       
    --]]
        local dcl_args = {}
    local out = {}
        dcl_args._table = args.data.table
    local init = 1
        for k, field_data in pairs(args.data.fields) do
    local split_start, split_end = string.find(str, pattern, init, plain)
            if field_data.field then
    while split_start do
                dcl_args[field_data.field] = field_data.type
        out[#out+1] = string.sub(str, init, split_start-1)
            end
         init = split_end+1
         end
         split_start, split_end = string.find(str, pattern, init, plain)
          
        return util.cargo.declare(frame, dcl_args)
     end
     end
    out[#out+1] = string.sub(str, init)
    return out
end
end


function util.cargo.attach_factory(args)
function util.string.split_outer(str, pattern, outer)
     -- Returns a function that can be called by templates to attach cargo tables
     --[[
    --
        Split a string into a table according to the pattern, ignoring
    -- args
        matching patterns inside the outer patterns.
    -- data: data table
       
    --   table: name of cargo table
        Parameters
    --   fields: associative table with:
        ----------
    --    field: name of the field to declare
        str : string
    --    type: type of the  field
            String to split.
    return function (frame)
        pattern : string
         frame = util.misc.get_frame(frame)
            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.
          
          
         local attach_args = {}
         Returns
         attach_args._table = args.data.table
        -------
        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)",
        }
          
          
         return util.cargo.attach(frame, attach_args)
         -- Nesting in the middle:
    end
        str = 'mods.id, CONCAT(mods.id, mods.name), mods.required_level'
end
        mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'}))
 
        table#1 {
--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'})
          "mods.id",
function util.cargo.map_results_to_id(args)
          "CONCAT(mods.id, mods.name)",
    -- Maps the results passed to a table containing the specified field as key and a table of rows for the particular page as values.
          "mods.required_level",
    --
         }
    -- 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 = {}
     for _, row in ipairs(args.results) do
    local nesting_level = 0
         local pid = row[args.field]
    local i = 0
        if out[pid] then
    local pttrn = '(.-)' .. '(' .. pattern .. ')'
             out[pid][#out[pid]+1] = row
     for v, sep in string.gmatch(str, pttrn) do
         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:
             row[args.field] = nil
         if string.find(v, outer[1]) then -- Multiple matches?
             nesting_level = nesting_level + 1
         end
         end
    end
         if string.find(v, outer[2]) then  
   
             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
          
          
         local missing_ids = {}
         -- Add back the separator if nesting is occuring:
         for k, _ in pairs(missing) do
         if nesting_level ~= 0 then
             missing_ids[#missing_ids+1] = k
             out[#out] = out[#out] .. sep
         end
         end  
          
          
         error(string.format(i18n.errors.missing_ids, args.id_field, table.concat(missing_ids, '\n')))
         -- Get the last index value: 
        i = i + #v + #sep
     end
     end
      
      
    return results
     -- Complement with the last part of the string:
end
     if nesting_level == 0 then  
 
         out[#out+1] = string.sub(str, math.max(i+1, 1))
function util.cargo.replace_holds(args)
     -- Replaces a holds query with a like or regexp equivalent.
    --
    -- required args:
    --  string: string to replace
    --
    -- optional args:
    --  mode    : Either "like" or "regex"; default "regex"
    --              like: Replaces the holds query with a LIKE equivalent
    --              regex: Replaces the holds query with a REGEXP equivalent
    --  field    : Field pattern to use. Can be used to restrict the hold replacement to specific fields.
     --            Default: all fields are matched.
    --  separator: Separator for field entries to use in the REGEXP mode.
    --            Default: ,
    --
    -- Returns the replaced query
    local args = args or {}
    -- if the field is not specified, replace any holds query
    args.field = args.field or '[%w_\.]+'
    local pattern = string.format('(%s) HOLDS ([NOT ]*)"([^"]+)"', args.field)
    if args.mode == 'like' or args.mode == nil then
         return string.gsub(args.string, pattern, '%1__full %2LIKE "%%%3%%"')
    elseif args.mode == 'regex' then
        args.separator = args.separator or ','
        return string.gsub(
            args.string,  
            pattern,
            string.format('%%1__full %%2REGEXP "(%s|^)%%3(%s|$)"', args.separator, args.separator)
        )
     else
     else
         error('Invalid mode specified. Acceptable values are like or regex.')
         out[#out] = out[#out] .. string.sub(str, math.max(i+1, 1))
        -- TODO: Check if nesting level is zero?
     end
     end
end
function util.cargo.parse_field(args)
    -- Parse a cargo field declaration and returns a table containing the results
    --
    -- required args:
    --  field: field to parse
    --
    -- Return
    --  type      - Type of the field
    --  list      - Separator of the list if it is a list type field
    --  parameters - any parameters to the field itself
    local field = args.field
    local results = {}
    local match
   
    match = { string.match(field, 'List %(([^%(%)]+)%) of (.*)') }
    if #match > 0 then
        results.list = match[1]
        field = match[2]
    end
   
    match = { string.match(field, '%s*(%a+)%s*%(([^%(%)]+)%)') }
    if #match > 0 then
        results.type = match[1]
        field = match[2]
        results.parameters = {}
        for _, param_string in ipairs(util.string.split(field, ';')) do
            local index = { string.find(param_string, '=') }
            local key
            local value
            if #index > 0 then
                key = string.sub(param_string, 0, index[1]-1)
                value = util.string.strip(string.sub(param_string, index[1]+1))
            else
                key = param_string
                value = true
            end
            results.parameters[util.string.strip(key)] = value
        end
    else
        -- the reminder must be the type since there is no extra declarations
        results.type = string.match(field, '%s*(%a+)%s*')
    end
   
    return results
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
     return out
end
end
Line 1,303: Line 1,113:


     return out
     return out
end
function util.string.format(format, ...)
    --[[
    String replacement with support for numbered argument conversion
    specifications. This is useful for i18n, as translating can sometimes
    change the order of words around.
    The format can contain either numbered argument conversion specifications
    (i.e., "%n$"), or unnumbered argument conversion specifications (i.e., "%"),
    but not both.
    If numbered argument conversion specifications are not needed, consider
    using string.format() from the Lua string library instead.
    Example:
        local format = 'Bubba ate %2$d %1$s. That\'s a lot of %1$s!'
        util.string.format(format, 'hotdogs', 26)
            -> Bubba ate 26 hotdogs. That's a lot of hotdogs!
    ]]
    local values = {}
    for v in string.gmatch(format, '%%(%d+)%$') do
        values[#values+1] = select(v, ...)
    end
    if #values == 0 then
        -- Using unnumbered argument conversion specifications, so just pass
        -- args to string.format().
        return string.format(format, ...)
    end
    format = string.gsub(format, '%%%d+%$', '%%')
    return string.format(format, unpack(values))
end
function util.string.first_to_upper(str)
    --[[
        Converts the first letter of a string to uppercase
    --]]
    -- Reassign to variable before returning since string.gsub returns two values
    str = str:gsub('^%l', string.upper)
    return str
end
util.string.pattern = {}
function util.string.pattern.valid_var_name()
    --[[
        Get a pattern for a valid variable name.
    ]]
    return '%A?([%a_]+[%w_]*)[^%w_]?'
end
end


Line 1,310: Line 1,170:


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
     for i = 1, infinity do
    -- elements and associative elements. Does not count nil elements.
         if tbl[i] == nil then
    local count = 0
             return i - 1
    for _ in pairs(tbl) do
        count = count + 1
    end
    return count
end
util.table.count = util.table.length
 
function util.table.contains(tbl, value)
    -- Checks whether a table contains a value
     for _, v in pairs(tbl) do
         if v == value then
             return true
         end
         end
     end
     end
    return false
end
function util.table.has_key(tbl, key)
    -- Checks whether a table has a key
    return tbl[key] ~= nil
end
end


function util.table.assoc_to_array(tbl, args)
function util.table.has_any_key(tbl, keys)
     -- Turn associative array into an array, discarding the values
    -- Checks whether a table has at least one of the keys
     local out = {}
    for _, key in ipairs(keys or {}) do
     for key, _ in pairs(tbl) do
        if tbl[key] ~= nil then
         out[#out+1] = key
            return true
        end
    end
    return false
end
 
function util.table.has_all_keys(tbl, keys)
    -- Checks whether a table has all of the keys
    for _, key in ipairs(keys or {}) do
        if tbl[key] == nil then
            return false
        end
    end
    return true
end
 
function util.table.keys(tbl)
    -- Returns the keys of a table
    local keys = {}
    for k, _ in pairs(tbl) do
        keys[#keys+1] = k
    end
    return keys
end
util.table.assoc_to_array = util.table.keys
 
function util.table.column(tbl, colkey, idxkey)
     --[[
    Returns the values of one column of a multi-dimensional table
 
    tbl  A multi-dimensional table
    colkey  The column key from the inner tables
    idxkey  If provided, the column from the inner tables to index the
            returned values by. Default: nil
    --]]
     local col = {}
     for _, row in pairs(tbl) do
         if type(row) == 'table' and row[colkey] ~= nil then
            if idxkey ~= nil and row[idxkey] ~= nil then
                col[row[idxkey]] = row[colkey]
            else
                col[#col+1] = row[colkey]
            end
        end
    end
    return col
end
 
function util.table.merge(...)
    --[[
    Merges the keys and values of multiple tables into a single table. If
    the input tables share non-numerical keys, then the later values for those
    keys will overwrite the previous ones. Numerical keys are instead appended
    and renumbered, incrementing from 1.
    --]]
    local tbl = {}
    for _, t in ipairs({...}) do
        for k, v in pairs(t) do
            if type(k) == 'number' then
                table.insert(tbl, v)
            else
                tbl[k] = v
            end
        end
     end
     end
     return out
     return tbl
end
end


Line 1,423: Line 1,364:


         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,434: Line 1,375:
         -- 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,448: Line 1,389:


         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,464: Line 1,405:


         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,480: Line 1,421:


         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,496: Line 1,437:


         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



Latest revision as of 18:57, 21 April 2024

Module documentation[view] [edit] [history] [purge]


This is a meta module.

This module is meant to be used only by other modules. It should not be invoked in wikitext.

Lua logo

This module depends on the following other modules:

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

ru:Модуль:Util

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

local xtable -- Lazy load require('Module:Table')
local getArgs -- Lazy load require('Module:Arguments').getArgs
local m_cargo -- Lazy load require('Module:Cargo')

-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = mw.loadData('Module:Util/config')

local i18n = cfg.i18n

local util = {}

-- ----------------------------------------------------------------------------
-- util.cast
-- ----------------------------------------------------------------------------

util.cast = {}

function util.cast.text(value, args)
    -- Takes an arbitary value and converts it to text.
    -- 
    -- Also strips any categories
    --
    -- args
    --  cast_nil      - Cast lua nil value to "nil" string
    --                  Default: false
    --  discard_empty - if the string is empty, return nil rather then empty string
    --                  Default: true
    args = args or {}
    if args.discard_empty == nil then
        args.discard_empty = true
    end
    
    if value == nil and not args.cast_nil then
        return 
    end
    
    value = tostring(value)
    if value == '' and args.discard_empty then
        return
    end
    
    -- Reassign to variable before returning since string.gsub returns two values
    value = string.gsub(value, '%[%[Category:[%w_ ]+%]%]', '')
    return value
end

function util.cast.boolean(value, args)
    -- Takes an arbitrary value and attempts to convert it to a boolean.
    --
    -- for strings false will be according to i18n.bool_false
    --
    -- args
    --  cast_nil  if set to false, it will not cast nil values
    args = args or {}
    local t = type(value)
    if t == 'nil' then
        if args.cast_nil == nil or args.cast_nil == true then
            return false
        else
            return
        end
    elseif t == 'boolean' then
        return value
    elseif t == 'number' then
        if value == 0 then return false end
        return true
    elseif t == 'string' then
        local tmp = string.lower(value)
        for _, v in ipairs(i18n.bool_false) do
            if v == tmp then
                return false
            end
        end
        return true
    else
        error(string.format(i18n.errors.not_a_boolean, tostring(value), t))
    end

end

function util.cast.number(value, args)
    -- Takes an arbitrary value and attempts to convert it to a number.
    --
    -- args
    --  default  for strings, if default is nil and the conversion fails, an error will be returned
    --  min  error if <min
    --  max  error if >max
    args = args or {}

    local t = type(value)
    local val

    if t == 'nil' then
        val = nil
    elseif t == 'boolean' then
        if value then
            val = 1
        else
            val = 0
        end
    elseif t == 'number' then
        val = value
    elseif t == 'string' then
        val = tonumber(value)
    end

    if val == nil then
        if args.default ~= nil then
            val = args.default
        else
            error(string.format(i18n.errors.not_a_number, tostring(value), t))
        end
    end

    if args.min ~= nil and val < args.min then
        error(string.format(i18n.errors.number_too_small, val, args.min))
    end

    if args.max ~= nil and val > args.max then
        error(string.format(i18n.errors.number_too_large, val, args.max))
    end

    return val
end

function util.cast.table(value, args)
    -- Takes an arbitrary value and attempts to convert it to a table.
    -- 
    -- args
    --  split_args  If true, create an association table (rather than an array)
    --  pattern  The pattern to split strings by. Default: ',%s*'
    --  split_args_pattern  The pattern to split keys from values by. Ignored if split_args is not true.
    --      Default: '%s*=%s*'
    --  callback  A callback function to call on each value
    args = args or {}
    local pattern = args.pattern or ',%s*'
    local split_args_pattern = args.split_args_pattern or '%s*=%s*'
    local tbl
    if type(value) == 'string' then
        if args.split_args then
            tbl = util.string.split_args(value, { sep = pattern, kvsep = split_args_pattern } )
        else
            tbl = util.string.split(value, pattern)
        end
    elseif type(value) ~= 'table' then
        tbl = {value}
    else
        tbl = value
    end
    if args.callback then
        for k, v in ipairs(tbl) do
            tbl[k] = args.callback(v)
        end
    end
    return tbl
end

function util.cast.version(value, args)
    -- Takes a string value and returns as version number
    -- If the version number is invalid an error is raised
    --
    -- args:
    --  return_type: defaults to "table"
    --   table  - Returns the version number broken down into sub versions as a table
    --   string - Returns the version number as string
    --
    if args == nil then
        args = {}
    end

    local result
    if args.return_type == 'table' or args.return_type == nil then
        result = util.string.split(value, '%.')

        if #result ~= 3 then
            error(string.format(i18n.errors.malformed_version_string, value))
        end

        result[4] = string.match(result[3], '%a+')
        result[3] = string.match(result[3], '%d+')

        for i=1,3 do
            local v = tonumber(result[i])
            if v == nil then
                error(string.format(i18n.errors.non_number_version_component, value))
            end
            result[i] = v
        end
    elseif args.return_type == 'string' then
        result = string.match(value, '%d+%.%d+%.%d+%a*')
    end

    if result == nil then
        error(string.format(i18n.errors.unrecognized_version_number, value))
    end

    return result
end

function util.cast.replace_if_match(value, args)
    -- Returns a function that returns its input unchanged, unless the string value
    -- matches the 'pattern' argument, in which case the 'replacewith' value is returned.
    if ((args == nil) or (args.pattern == nil) or (value == nil)) then
        return value
    elseif string.find(tostring(value),args.pattern) then
        return args.replacewith
    else
        return value
    end
end

-- ----------------------------------------------------------------------------
-- util.validate
-- ----------------------------------------------------------------------------

util.validate = {}
util.validate.factory = {}

function util.validate.factory.number_in_range(args)
    -- Returns a function that validates whether a number is within a range of 
    -- values. An error is thrown if the value is not a number or if it is not 
    -- within the specified range.
    args = args or {}
    args.min = args.min or -math.huge
    args.max = args.max or math.huge
    return function (value)
        if type(value) ~= 'number' then
            error(string.format(i18n.errors.not_a_number, tostring(value), type(value)))
        end
        if value < args.min or value > args.max then
            error(string.format(args.errmsg or i18n.errors.number_out_of_range, tostring(value), tostring(args.min), tostring(args.max)), args.errlvl or 2)
        end
        return value
    end
end

function util.validate.factory.string_length(args)
    -- Returns a function that validates whether a string has has the correct 
    -- length. An error is thrown if the value is not a string or if its length 
    -- restrictions are not met.
    args = args or {}
    args.min = args.min or 0
    args.max = args.max or math.huge
    return function (value)
        if type(value) ~= 'string' then
            error(string.format(i18n.errors.not_a_string, tostring(value), type(value)))
        end
        local length = mw.ustring.len(value)
        if length < args.min or length > args.max then
            error(string.format(args.errmsg or i18n.errors.string_length_incorrect, tostring(value), tostring(args.min), tostring(args.max)), args.errlvl or 2)
        end
        return value
    end
end

function util.validate.factory.in_table(args)
    -- Returns a function that validates whether a table contains a value.
    -- An error is thrown if the value is not found.
    args = args or {}
    return function (value)
        if not util.table.contains(args.tbl or {}, value) then
            error(string.format(args.errmsg or i18n.errors.value_not_in_table, tostring(value)), args.errlvl or 2)
        end
        return value
    end
end

function util.validate.factory.in_table_keys(args)
    -- Returns a function that validates whether a table has a value as one of 
    -- its keys. An error is thrown if the key does not exist.
    args = args or {}
    return function (value)
        if not util.table.has_key(args.tbl or {}, value) then
            error(string.format(args.errmsg or i18n.errors.value_not_in_table_keys, tostring(value)), args.errlvl or 2)
        end
        return value
    end
end

--
-- util.cast.factory
--

-- This section is used to generate new functions for common argument parsing tasks based on specific options
--
-- All functions return a function which accepts two arguments:
--  tpl_args - arguments from the template 
--  frame - current frame object
--
-- All factory functions accept have two arguments on creation:
--  k - the key in the tpl_args to retrive the value from
--  args - any addtional arguments (see function for details)

util.cast.factory = {}

function util.cast.factory.array_table(k, args)
    -- Arguments:
    --  tbl - table to check against
    --  errmsg - error message if no element was found; should accept 1 parameter
    xtable = xtable or require('Module:Table')
    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
    --  property_prefix: property prefix if any
    --  subobject_prefix: subobject prefix if any
    --  properties: table of properties to add if any
    args = args or {}
    args.prefix = args.prefix or ''

    local i = 0
    local stats = {}
    repeat
        i = i + 1
        local prefix = string.format('%s%s%s_%s', args.prefix, i18n.args.stat_infix, i, '%s')
        local id = {
            id = string.format(prefix, i18n.args.stat_id),
            min = string.format(prefix, i18n.args.stat_min),
            max = string.format(prefix, i18n.args.stat_max),
            value = string.format(prefix, i18n.args.stat_value),
        }

        local value = {}
        for key, args_key in pairs(id) do
            value[key] = argtbl[args_key]
        end


        if value.id ~= nil and ((value.min ~= nil and value.max ~= nil and value.value == nil) or (value.min == nil and value.max == nil and value.value ~= nil)) then
            if value.value then
                value.value = util.cast.number(value.value)
                argtbl[id.value] = value.value
            else
                value.min = util.cast.number(value.min)
                argtbl[id.min] = value.min
                value.max = util.cast.number(value.max)
                argtbl[id.max] = value.max

                -- Also set average value
                value.avg = (value.min + value.max)/2
                argtbl[string.format('%sstat%s_avg', args.prefix, i)] = value.avg
            end
            argtbl[string.format('%sstat%s', args.prefix, i)] = value
            stats[#stats+1] = value
        elseif util.table.has_all_value(value, {'id', 'min', 'max', 'value'}, nil) then
            value = nil
        -- all other cases should be improperly set value
        else
            error(string.format(i18n.errors.improper_stat, args.prefix, i))
        end
    until value == nil

    argtbl[string.format('%sstats', args.prefix)] = stats
end

function util.args.spawn_weight_list(argtbl, args)
    args = args or {}
    args.input_argument = i18n.args.spawn_weight_prefix
    args.output_argument = 'spawn_weights'
    args.cargo_table = 'spawn_weights'
    
    util.args.weight_list(argtbl, args)
end

function util.args.generation_weight_list(argtbl, args)
    args = args or {}
    args.input_argument = i18n.args.generation_weight_prefix
    args.output_argument = 'generation_weights'
    args.cargo_table = 'generation_weights'
    
    util.args.weight_list(argtbl, args)
end

function util.args.weight_list(argtbl, args)
    -- Parses a weighted pair of lists and sets properties
    --
    -- argtbl: argument table to work with
    -- args:
    --  output_argument - if set, set arguments to this value
    --  input_argument - input prefix for parsing the arguments from the argtbl
    --  subobject_name - name of the subobject

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

    args = args or {}
    args.input_argument = args.input_argument or 'spawn_weight'

    local i = 0
    local id = nil
    local value = nil
    
    if args.output_argument then
        argtbl[args.output_argument] = {}
    end

    repeat
        i = i + 1
        id = {
            tag = string.format('%s%s_tag', args.input_argument, i),
            value = string.format('%s%s_value', args.input_argument, i),
        }
    
        value = {
            tag = argtbl[id.tag],
            value = argtbl[id.value],
        }
        
        if value.tag ~= nil and value.value ~= nil then
            if args.output_argument then
                argtbl[args.output_argument][i] = value
            end
            
            if args.cargo_table then
                m_cargo.store({
                    _table = args.cargo_table,
                    ordinal = i,
                    tag = value.tag,
                    weight = util.cast.number(value.value, {min=0}),
                })
            end
        elseif not (value.tag == nil and value.value == nil) then
            error(string.format(i18n.errors.invalid_weight, id.tag, id.value))
        end
    until value.tag == nil
end

function util.args.version(argtbl, args)
    -- in any prefix spaces should be included
    --
    -- argtbl: argument table to work with
    -- args:
    --  set_properties: if defined, set properties on the page
    --  variables: table of prefixes
    --  ignore_unknowns: if defined, treat a version number of '?' as if it
    --    were not present 
    --  noquery: For testing; if defined, skips the query
    --  return_ids_and_keys: For testing; on return, args.version_ids and
    --    args.versionkeys are set to the IDs and keys found
    args = args or {}
    args.variables = args.variables or {
        release = {},
        removal = {},
    }

    local version_ids={}
    local version_keys={}

    for key, data in pairs(args.variables) do
        local full_key = string.format('%s_version', key)
        if args.ignore_unknowns and (argtbl[full_key] == '?') then
            argtbl[full_key] = nil
        elseif argtbl[full_key] ~= nil then
            local value = util.cast.version(argtbl[full_key], {return_type = 'string'})
            argtbl[full_key] = value
            if value ~= nil then
                data.value = value
                if data.property ~= nil then
                    version_ids[#version_ids+1] = value
                    version_keys[value] = key
                end
            end
        end
    end

    -- no need to do a query if nothing was fetched
    if (args.noquery == nil) and (#version_ids > 0) then
        for i, id in ipairs(version_ids) do
            version_ids[i] = string.format('Versions.version="%s"', id)
        end

        local results = m_cargo.query(
            {'Versions'},
            {'release_date', 'version'},
            {
                where = table.concat(version_ids, ' OR '),
            }
        )

        if #results ~= #version_ids then
            error(string.format(i18n.too_many_versions, #results, #version_ids))
        end

        for _, row in ipairs(results) do
            local key = version_keys[row.version]
            argtbl[string.format('%s_date', key)] = row.release_date
        end
    end

    if args.return_ids_and_keys ~= nil then
        args.version_ids = version_ids
        args.version_keys = version_keys
    end
end

function util.args.from_cargo_map(args)
    m_cargo = m_cargo or require('Module:Cargo')
    return m_cargo.store_mapped_args(args)
end

function util.args.template_to_lua(str)
    --[[
    Convert templates to lua format. Simplifes debugging and creating 
    examples.
    
    Parameters
    ----------
    str : string 
        The entire template wrapped into string. Tip: Use Lua's square 
        bracket syntax for defining string literals.
       
    Returns
    -------
    out : table
        out.template - Template name.
        out.args - arguments in table format.
        out.args_to_str - arguments in readable string format.
    ]]
    local out = {}
    
    -- Get the template name:
    out.template = string.match(str, '{{(.-)%s*|')
    
    -- Remove everything but the arguments:
    str = string.gsub(str, '%s*{{.-|', '')
    str = string.gsub(str, '%s*}}%s*', '')
    
    -- Split up the arguments:
    out.args = {}
    for i, v in ipairs(util.string.split(str, '%s*|%s*')) do 
        local arg = util.string.split(v, '%s*=%s*')
        out.args[arg[1]] = arg[2]
        out.args[#out.args+1] = arg[1]
    end    
    
    -- Concate for easy copy/pasting:
    local tbl = {}
    for i, v in ipairs(out.args) do 
        tbl[#tbl+1]= string.format("%s='%s'", v, out.args[v])
    end 
    out.args_to_str = table.concat(tbl, ',\n')
    
    return out
end

-- ----------------------------------------------------------------------------
-- util.html
-- ----------------------------------------------------------------------------

util.html = {}

function util.html.abbr(text, title, options)
    -- Outputs html tag <abbr> as string or as mw.html node.
    -- 
    -- options
    --   class: class attribute
    --   output: set to mw.html to return a mw.html node instead of a string
    if not title then
        return text
    end
    options = options or {}
    local abbr = mw.html.create('abbr')
    abbr:attr('title', title)
    local class
    if type(options) == 'table' and options.class then
        class = options.class
    else
        class = options
    end
    if type(class) == 'string' then
        abbr:attr('class', class)
    end
    abbr:wikitext(text)
    if options.output == mw.html then
        return abbr
    end
    return tostring(abbr)
end

function util.html.error(args)
    -- Create an error message box
    --
    -- args
    --   msg  str  The error message
    args = args or {}
    local err = mw.html.create('strong')
        :addClass('error')
        :tag('span')
            :addClass('module-error')
            :wikitext(i18n.errors.module_error .. (args.msg or ''))
            :done()
    return tostring(err)
end

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

function util.html.tooltip(abbr, text, class)
    return string.format('<span class="hoverbox c-tooltip %s"><span class="hoverbox__activator c-tooltip__activator">%s</span><span class="hoverbox__display c-tooltip__display">%s</span></span>', class or '', abbr or '', text or '')
end

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, value, options)
    -- value: table
    --  min:
    --  max:
    -- options: table
    --  func: Function to transform the value retrieved from the database
    --  fmt: Format string (or function that returns format string) to use for the value.
    --    Default: '%s'
    --  fmt_range: Format string to use for range value.
    --    Default: '(%s-%s)'
    --  color: poe_color code to use for the value. False for no color.
    --    Default: 'value' if value is unmodified; 'mod' if modified
    --  class: Additional css class added to color tag
    --  inline: Format string to use for the output
    --  inline_color: poe_color code to use for the output. False for no color.
    --    Default: Inherits from value color
    --  inline_class: Additional css class added to inline color tag
    --  no_color: (Deprecated; use color=false instead)
    --  return_color: (Deprecated; returns both value.out and value without this)

    -- Make shallow copy to avoid modifying the original table
    local value_copy = {}
    for k, v in pairs(value) do
        value_copy[k] = v
    end
    local default_color = 'value'
    local base = {
        min = value_copy.base_min or value_copy.base,
        max = value_copy.base_max or value_copy.base,
    }
    if value_copy.min ~= base.min or value_copy.max ~= base.max then
        default_color = 'mod'
    end
    if options.color ~= false and options.no_color == nil then
        value_copy.color = options.color or default_color
    end
    if options.func then
        value_copy.min = options.func(tpl_args, value_copy.min)
        value_copy.max = options.func(tpl_args, value_copy.max)
    end
    local fmt = options.fmt or '%s'
    if type(fmt) == 'function' then -- Function that returns the format string
        fmt = fmt(tpl_args, value_copy)
    end
    if value_copy.min == value_copy.max then -- Static value
        value_copy.out = string.format(fmt, value_copy.min)
    else -- Range value
        local fmt_range = options.fmt_range or i18n.range
        value_copy.out = string.format(
            string.format(fmt_range, fmt, fmt),
            value_copy.min,
            value_copy.max
        )
    end
    local inline = options.inline
    if type(inline) == 'function' then
        inline = inline(tpl_args, value_copy)
    end
    inline = inline ~= '' and inline or nil -- TODO: Eliminate the need for this?
    local inline_color = options.inline_color
    if value_copy.color and (not inline or inline_color ~= nil) then
        value_copy.out = util.html.poe_color(value_copy.color, value_copy.out, options.class)
    end
    if inline then
        value_copy.out = string.format(inline, value_copy.out)
        if inline_color or inline_color == nil and options.color ~= false then
            inline_color = inline_color or value_copy.color or default_color
            value_copy.out = util.html.poe_color(inline_color, value_copy.out, options.inline_class)
        end
    end
    if options.return_color then
        return value_copy.out, value_copy.color
    end
    return value_copy.out, value_copy
end

function util.html.wikilink(page, text)
    if text then
        return string.format('[[%s|%s]]', page, text)
    end
    return string.format('[[%s]]', page)
end

function util.html.url_link(url, text)
    return string.format('[%s %s]', url, text)
end

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

util.misc = {}

function util.misc.invoker_factory(func, options)
    -- Returns a function that can be called directly or with #invoke.
    return function (frame)
        frame = frame or {}
        local args
        if type(frame.args) == 'table' then
            -- Called via #invoke, so use getArgs().
            getArgs = getArgs or require('Module:Arguments').getArgs
            args = getArgs(frame, options)
        else
            -- Called from another module or from the debug console, so assume args 
            -- are passed in directly.
            args = frame
        end
        return func(args)
    end
end

function util.misc.is_frame(frame)
    -- the type of the frame is a table containing the functions, so check whether some of these exist
    -- should be enough to avoid collisions.
    return not(frame == nil or type(frame) ~= 'table' or (frame.argumentPairs == nil and frame.callParserFunction == nil))
end

function util.misc.get_frame(frame)
    -- OBSOLETE. Use mw.getCurrentFrame() instead.
    return mw.getCurrentFrame()
end

function util.misc.get_args_raw(frame)
    -- Simple method for getting arguments. Use this instead of Module:Arguments
    -- when the extra options provided by the latter would be overkill.
    if util.misc.is_frame(frame) then
        -- Called via {{#invoke:}}, so use the args that were passed into the 
        -- template.
        return frame.args
    end
    -- Called from another module or from the debug console, so assume args 
    -- are passed in directly.
    return frame
end

function util.misc.maybe_sandbox(module_name)
    -- Did we load or {{#invoke:}} a module sandbox?
    if module_name and package.loaded[string.format('Module:%s/sandbox', module_name)] ~= nil or string.find(mw.getCurrentFrame():getTitle(), 'sandbox', 1, true) then
        return true
    end
    return false
end

function util.misc.add_category(categories, args)
    -- categories: table of categories
    -- args: table of extra arguments
    --  namespace: id of namespace to validate against
    --  ignore_blacklist: set to non-nil to ignore the blacklist
    --  sub_page_blacklist: blacklist of subpages to use (if empty, use default)
    --  namespace_blacklist: blacklist of namespaces to use (if empty, use default)
    if type(categories) == 'string' then
        categories = {categories}
    end

    if args == nil then
        args = {}
    end

    local title = mw.title.getCurrentTitle()
    local sub_blacklist = args.sub_page_blacklist or cfg.misc.category_blacklist.sub_pages
    local ns_blacklist = args.namespace_blacklist or cfg.misc.category_blacklist.namespaces

    if args.namespace ~= nil and title.namespace ~= args.namespace then
        return ''
    end

    if args.ignore_blacklist == nil and (sub_blacklist[title.subpageText] or ns_blacklist[title.subjectNsText]) then
        return ''
    end

    local cats = {}

    for i, cat in ipairs(categories) do
        cats[i] = string.format('[[Category:%s]]', cat)
    end
    return table.concat(cats)
end

-- ----------------------------------------------------------------------------
-- util.Error
-- ----------------------------------------------------------------------------

-- Prototype error object
local Error_prototype = {
    message = i18n.errors.unspecified,
    code = 'module_error',
    issue = true, -- Whether to issue error
    level = 2,
}
Error_prototype.__index = Error_prototype

function Error_prototype:throw(force)
    if force or self.issue then
        error(self.message, self.level)
    end
    return self
end

function Error_prototype:get_html()
    return util.html.error{msg=self.message}
end

function Error_prototype:get_category(args)
    return util.misc.add_category(self.category, args)
end

function util.Error(obj)
    -- Create a new error object
    obj = obj or {}
    setmetatable(obj, Error_prototype)
    return obj
end

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

util.string = {}

function util.string.trim(str, charset)
    --[[
    Trims leading and trailing characters in charset from a string. 
    Charset is '%s' by default, which matches whitespace characters

    This works much like mw.text.trim, using the string library instead 
    of the ustring library. This function may return erroneous results 
    if the charset needs to be Unicode-aware.
    --]]
    charset = charset or '%s'
    str = string.gsub(str, '^[' .. charset .. ']*(.-)[' .. charset .. ']*$', '%1')
    return str
end

function util.string.strip_wikilinks(str)
    --[[
    Removes wikilinks from a string, leaving the plain text
    --]]
    str = mw.ustring.gsub(str, '%[%[:?([^%]|]+)%]%]', '%1')
    str = mw.ustring.gsub(str, '%[%[:?[^|]+|([^%]|]+)%]%]', '%1')
    return str
end

function util.string.strip_html(str)
    --[[
    Removes html tags from a string, leaving the plain text
    --]]
    str = mw.ustring.gsub(str, '<[^>]*>', '')
    return str
end

function util.string.split(str, pattern, plain)
    --[[
    Splits a string into a table
    
    This does essentially the same thing as mw.text.split, but with 
    significantly better performance. This function may return erroneous 
    results if the pattern needs to be Unicode-aware.
    
    str  String to split
    pattern  Pattern to use for splitting
    plain  If true, pattern is interpreted as a literal string
    --]]
    local out = {}
    local init = 1
    local split_start, split_end = string.find(str, pattern, init, plain)
    while split_start do
        out[#out+1] = string.sub(str, init, split_start-1)
        init = split_end+1
        split_start, split_end = string.find(str, pattern, init, plain)
    end
    out[#out+1] = string.sub(str, init)
    return out
end

function util.string.split_outer(str, pattern, outer)
    --[[
        Split a string into a table according to the pattern, ignoring 
        matching patterns inside the outer patterns.
        
        Parameters
        ----------
        str : string
            String to split.
        pattern : string
            Pattern to split on.
        outer : table of strings where #outer = 2.
            Table with 2 strings that defines the opening and closing patterns 
            to match, for example parantheses or brackets.
        
        Returns
        -------
        out : table
            table of split strings.
            
        Examples
        --------
        -- Nesting at the end:
        str = 'mods.id, CONCAT(mods.id, mods.name)'
        mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'}))
        table#1 {
          "mods.id",
          "CONCAT(mods.id, mods.name)",
        }
        
        -- Nesting in the middle:
        str = 'mods.id, CONCAT(mods.id, mods.name), mods.required_level'
        mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'}))
        table#1 {
          "mods.id",
          "CONCAT(mods.id, mods.name)",
          "mods.required_level",
        }
    ]]
    local out = {}
    local nesting_level = 0
    local i = 0
    local pttrn = '(.-)' .. '(' .. pattern .. ')'
    for v, sep in string.gmatch(str, pttrn) do
        if nesting_level == 0 then
            -- No nesting is occuring:
            out[#out+1] = v
        else
            -- Nesting is occuring:
            out[#out] = (out[math.max(#out, 1)] or '') .. v
        end
        
        -- Increase nesting level:
        if string.find(v, outer[1]) then -- Multiple matches?
            nesting_level = nesting_level + 1
        end
        if string.find(v, outer[2]) then 
            nesting_level = nesting_level - 1
        end
        
        -- Add back the separator if nesting is occuring:
        if nesting_level ~= 0 then 
            out[#out] = out[#out] .. sep
        end 
        
        -- Get the last index value:  
        i = i + #v + #sep
    end
    
    -- Complement with the last part of the string:
    if nesting_level == 0 then 
        out[#out+1] = string.sub(str, math.max(i+1, 1))
    else
        out[#out] = out[#out] .. string.sub(str, math.max(i+1, 1))
        -- TODO: Check if nesting level is zero?
    end
    return out
end

function util.string.split_args(str, args)
    -- Splits arguments string into a table
    --
    -- str: String of arguments to split
    -- args: table of extra arguments
    --  sep: separator to use (default: ,)
    --  kvsep: separator to use for key value pairs (default: =)
    local out = {}

    if args == nil then
        args = {}
    end

    args.sep = args.sep or ','
    args.kvsep = args.kvsep or '='

    if str ~= nil then
        local row
        for _, str in ipairs(util.string.split(str, args.sep)) do
            row = util.string.split(str, args.kvsep)
            if #row == 1 then
                out[#out+1] = row[1]
            elseif #row == 2 then
                out[row[1]] = row[2]
            else
                error(string.format(i18n.number_of_arguments_too_large, #row))
            end
        end
    end

    return out
end

function util.string.format(format, ...)
    --[[
    String replacement with support for numbered argument conversion 
    specifications. This is useful for i18n, as translating can sometimes 
    change the order of words around.

    The format can contain either numbered argument conversion specifications 
    (i.e., "%n$"), or unnumbered argument conversion specifications (i.e., "%"), 
    but not both.

    If numbered argument conversion specifications are not needed, consider 
    using string.format() from the Lua string library instead.

    Example:
        local format = 'Bubba ate %2$d %1$s. That\'s a lot of %1$s!'
        util.string.format(format, 'hotdogs', 26)
            -> Bubba ate 26 hotdogs. That's a lot of hotdogs!
    ]]

    local values = {}
    for v in string.gmatch(format, '%%(%d+)%$') do
        values[#values+1] = select(v, ...)
    end
    if #values == 0 then
        -- Using unnumbered argument conversion specifications, so just pass 
        -- args to string.format().
        return string.format(format, ...)
    end
    format = string.gsub(format, '%%%d+%$', '%%')
    return string.format(format, unpack(values))
end

function util.string.first_to_upper(str)
    --[[
        Converts the first letter of a string to uppercase
    --]]

    -- Reassign to variable before returning since string.gsub returns two values
    str = str:gsub('^%l', string.upper)
    return str
end

util.string.pattern = {}
function util.string.pattern.valid_var_name()
    --[[
        Get a pattern for a valid variable name.
    ]]
    return '%A?([%a_]+[%w_]*)[^%w_]?'
end

-- ----------------------------------------------------------------------------
-- util.table
-- ----------------------------------------------------------------------------

util.table = {}

function util.table.length(tbl)
    -- Get number of elements in a table. Counts both numerically indexed 
    -- elements and associative elements. Does not count nil elements.
    local count = 0
    for _ in pairs(tbl) do
        count = count + 1
    end
    return count
end
util.table.count = util.table.length

function util.table.contains(tbl, value)
    -- Checks whether a table contains a value
    for _, v in pairs(tbl) do
        if v == value then
            return true
        end
    end
    return false
end

function util.table.has_key(tbl, key)
    -- Checks whether a table has a key
    return tbl[key] ~= nil
end

function util.table.has_any_key(tbl, keys)
    -- Checks whether a table has at least one of the keys
    for _, key in ipairs(keys or {}) do
        if tbl[key] ~= nil then
            return true
        end
    end
    return false
end

function util.table.has_all_keys(tbl, keys)
    -- Checks whether a table has all of the keys
    for _, key in ipairs(keys or {}) do
        if tbl[key] == nil then
            return false
        end
    end
    return true
end

function util.table.keys(tbl)
    -- Returns the keys of a table
    local keys = {}
    for k, _ in pairs(tbl) do
        keys[#keys+1] = k
    end
    return keys
end
util.table.assoc_to_array = util.table.keys

function util.table.column(tbl, colkey, idxkey)
    --[[
    Returns the values of one column of a multi-dimensional table

    tbl  A multi-dimensional table
    colkey  The column key from the inner tables
    idxkey  If provided, the column from the inner tables to index the 
            returned values by. Default: nil
    --]]
    local col = {}
    for _, row in pairs(tbl) do
        if type(row) == 'table' and row[colkey] ~= nil then
            if idxkey ~= nil and row[idxkey] ~= nil then
                col[row[idxkey]] = row[colkey]
            else
                col[#col+1] = row[colkey]
            end
        end
    end
    return col
end

function util.table.merge(...)
    --[[
    Merges the keys and values of multiple tables into a single table. If
    the input tables share non-numerical keys, then the later values for those
    keys will overwrite the previous ones. Numerical keys are instead appended
    and renumbered, incrementing from 1.
    --]]
    local tbl = {}
    for _, t in ipairs({...}) do
        for k, v in pairs(t) do
            if type(k) == 'number' then
                table.insert(tbl, v)
            else
                tbl[k] = v
            end
        end
    end
    return tbl
end

function util.table.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