Module:Util/sandbox: Difference between revisions
Jump to navigation
Jump to search
KickahaOta (talk | contribs) m (Test) |
KickahaOta (talk | contribs) m (Finally, a minor success) |
||
Line 574: | Line 574: | ||
args.version_ids = version_ids | args.version_ids = version_ids | ||
args.version_keys = version_keys | args.version_keys = version_keys | ||
end | end | ||
end | end |
Revision as of 06:52, 13 December 2021
![](https://upload.wikimedia.org/wikipedia/commons/thumb/7/7b/Edit_In_Sandbox_Icon_-_Color.svg/45px-Edit_In_Sandbox_Icon_-_Color.svg.png)
This is the module sandbox page for Module:Util (diff).
See also the companion subpage for test cases (run).
![](/images/thumb/4/4a/Meta_module.svg/45px-Meta_module.svg.png)
This is a meta module.
This module is meant to be used only by other modules. It should not be invoked in wikitext.
Overview
Provides utility functions for programming modules.
Structure
Group | Description |
---|---|
util.cast | utilities for casting values (i.e. from arguments) |
util.html | shorthand functions for creating some html tags |
util.misc | miscellaneous functions |
Usage
This module should be loaded with require()
.
The above documentation is transcluded from Module:Util/doc.
Editors can experiment in this module's sandbox and testcases pages.
Please add categories to the /doc subpage. Subpages of this module.
Editors can experiment in this module's sandbox and testcases pages.
Please add categories to the /doc subpage. Subpages of this module.
-------------------------------------------------------------------------------
--
-- Module:Util
--
-- This meta module contains a number of utility functions
-------------------------------------------------------------------------------
local xtable = require('Module:Table')
local m_cargo -- Lazy load require('Module:Cargo')
-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = mw.loadData('Module:Util/config')
local i18n = cfg.i18n
local util = {}
-- ----------------------------------------------------------------------------
-- util.cast
-- ----------------------------------------------------------------------------
util.cast = {}
function util.cast.text(value, args)
-- Takes an arbitary value and 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
value = string.gsub(value, '%[%[Category:[%w_ ]+%]%]', '')
return value
end
function util.cast.boolean(value, args)
-- Takes an abitary 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 abitary 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 abitary value and attempts to convert it to a table.
--
-- args
-- pattern The pattern to split strings by. Default: ',%s*'
-- callback A callback function to call on each value
args = args or {}
local tbl
if type(value) == 'string' then
tbl = util.string.split(value, args.pattern or ',%s*')
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.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
args = args or {}
return function (tpl_args, frame)
local elements
if tpl_args[k] ~= nil then
elements = util.string.split(tpl_args[k], ',%s*')
for _, element in ipairs(elements) do
local r = util.table.find_in_nested_array{value=element, tbl=args.tbl, key='full'}
if r == nil then
error(string.format(args.errmsg or i18n.errors.missing_element, element))
end
end
tpl_args[args.key_out or k] = xtable:new(elements)
end
end
end
function util.cast.factory.table(k, args)
args = args or {}
return function (tpl_args, frame)
args.value = tpl_args[k]
if args.value == nil then
return
end
local value = util.table.find_in_nested_array(args)
if value == nil then
error(string.format(args.errmsg or i18n.errors.missing_element, k))
end
tpl_args[args.key_out or k] = value
end
end
function util.cast.factory.assoc_table(k, args)
-- Arguments:
--
-- tbl
-- errmsg
-- key_out
return function (tpl_args, frame)
local elements
if tpl_args[k] ~= nil then
elements = util.string.split(tpl_args[k], ',%s*')
for _, element in ipairs(elements) do
if args.tbl[element] == nil then
error(util.html.error{msg=string.format(args.errmsg or i18n.errors.missing_element, element)})
end
end
tpl_args[args.key_out or k] = elements
end
end
end
function util.cast.factory.number(k, args)
args = args or {}
return function (tpl_args, frame)
tpl_args[args.key_out or k] = tonumber(tpl_args[k])
end
end
function util.cast.factory.boolean(k, args)
args = args or {}
return function(tpl_args, frame)
if tpl_args[k] ~= nil then
tpl_args[args.key_out or k] = util.cast.boolean(tpl_args[k])
end
end
end
function util.cast.factory.percentage(k, args)
args = args or {}
return function (tpl_args, frame)
local v = tonumber(tpl_args[k])
if v == nil then
return util.html.error{msg=string.format(i18n.errors.invalid_argument, k)}
end
if v < 0 or v > 100 then
return util.html.error{msg=string.format(i18n.errors.not_a_percentage, k)}
end
tpl_args[args.key_out or k] = v
end
end
-- ----------------------------------------------------------------------------
-- util.args
-- ----------------------------------------------------------------------------
util.args = {}
function util.args.stats(argtbl, args)
-- in any prefix spaces should be included
--
-- argtbl: argument table to work with
-- args:
-- prefix: prefix if any
-- frame: frame used to set subobjects; if not set dont set properties
-- property_prefix: property prefix if any
-- subobject_prefix: subobject prefix if any
-- properties: table of properties to add if any
args = args or {}
args.prefix = args.prefix or ''
local i = 0
local stats = {}
repeat
i = i + 1
local prefix = string.format('%s%s%s_%s', args.prefix, i18n.args.stat_infix, i, '%s')
local id = {
id = string.format(prefix, i18n.args.stat_id),
min = string.format(prefix, i18n.args.stat_min),
max = string.format(prefix, i18n.args.stat_max),
value = string.format(prefix, i18n.args.stat_value),
}
local value = {}
for key, args_key in pairs(id) do
value[key] = argtbl[args_key]
end
if value.id ~= nil and ((value.min ~= nil and value.max ~= nil and value.value == nil) or (value.min == nil and value.max == nil and value.value ~= nil)) then
if value.value then
value.value = util.cast.number(value.value)
argtbl[id.value] = value.value
else
value.min = util.cast.number(value.min)
argtbl[id.min] = value.min
value.max = util.cast.number(value.max)
argtbl[id.max] = value.max
-- Also set average value
value.avg = (value.min + value.max)/2
argtbl[string.format('%sstat%s_avg', args.prefix, i)] = value.avg
end
argtbl[string.format('%sstat%s', args.prefix, i)] = value
stats[#stats+1] = value
elseif util.table.has_all_value(value, {'id', 'min', 'max', 'value'}, nil) then
value = nil
-- all other cases should be improperly set value
else
error(string.format(i18n.errors.improper_stat, args.prefix, i))
end
until value == nil
argtbl[string.format('%sstats', args.prefix)] = stats
end
function util.args.spawn_weight_list(argtbl, args)
args = args or {}
args.input_argument = i18n.args.spawn_weight_prefix
args.output_argument = 'spawn_weights'
args.cargo_table = 'spawn_weights'
util.args.weight_list(argtbl, args)
end
function util.args.generation_weight_list(argtbl, args)
args = args or {}
args.input_argument = i18n.args.generation_weight_prefix
args.output_argument = 'generation_weights'
args.cargo_table = 'generation_weights'
util.args.weight_list(argtbl, args)
end
function util.args.weight_list(argtbl, args)
-- Parses a weighted pair of lists and sets properties
--
-- argtbl: argument table to work with
-- args:
-- output_argument - if set, set arguments to this value
-- frame - if set, automtically set subobjects
-- input_argument - input prefix for parsing the arguments from the argtbl
-- subobject_name - name of the subobject
m_cargo = m_cargo or require('Module:Cargo')
args = args or {}
args.input_argument = args.input_argument or 'spawn_weight'
local i = 0
local id = nil
local value = nil
if args.output_argument then
argtbl[args.output_argument] = {}
end
repeat
i = i + 1
id = {
tag = string.format('%s%s_tag', args.input_argument, i),
value = string.format('%s%s_value', args.input_argument, i),
}
value = {
tag = argtbl[id.tag],
value = argtbl[id.value],
}
if value.tag ~= nil and value.value ~= nil then
if args.output_argument then
argtbl[args.output_argument][i] = value
end
if args.frame and args.cargo_table then
m_cargo.store(args.frame, {
_table = args.cargo_table,
ordinal = i,
tag = value.tag,
weight = util.cast.number(value.value, {min=0}),
})
end
elseif not (value.tag == nil and value.value == nil) then
error(string.format(i18n.errors.invalid_weight, id.tag, id.value))
end
until value.tag == nil
end
function util.args.version(argtbl, args)
-- in any prefix spaces should be included
--
-- argtbl: argument table to work with
-- args:
-- frame: frame for queries
-- set_properties: if defined, set properties on the page
-- variables: table of prefixes
-- 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 = {},
}
if args.noquery == nil then
error('Noquery not set')
elseif args.return_ids_and_keys == nil then
error('Return_ids_and_keys not set')
end
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
if args.frame == nil then
error('Properties were set, but frame was not')
end
for i, id in ipairs(version_ids) do
version_ids[i] = string.format('Versions.version="%s"', id)
end
local results = m_cargo.query(
{'Versions'},
{'release_date', 'version'},
{
where = table.concat(version_ids, ' OR '),
}
)
if #results ~= #version_ids then
error(string.format(i18n.too_many_versions, #results, #version_ids))
end
for _, row in ipairs(results) do
local key = version_keys[row.version]
argtbl[string.format('%s_date', key)] = row.release_date
end
end
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)
-- Maps the arguments from a cargo argument table (i.e. the ones used in m_cargo.declare_factory)
--
-- It will expect/handle the following fields:
-- map.order - REQUIRED - Array table for the order in which the arguments in map.fields will be parsed
-- map.table - REQUIRED - Table name (for storage)
-- map.fields[id].field - REQUIRED - Name of the field in cargo table
-- map.fields[id].type - REQUIRED - Type of the field in cargo table
-- map.fields[id].func - OPTIONAL - Function to handle the arguments. It will be passed tpl_args and frame.
-- The function should return the parsed value.
--
-- If no function is specified, default handling depending on the cargo field type will be used
-- map.fields[id].default - OPTIONAL - Default value if the value is not set or returned as nil
-- If default is a function, the function will be called with (tpl_args, frame) and expected to return a default value for the field.
-- map.fields[id].name - OPTIONAL - Name of the field in tpl_args if it differs from the id in map.fields. Used for i18n for example
-- map.fields[id].required - OPTIONAL - Whether a value for the field is required or whether it can be left empty
-- Note: With a default value the field will never be empty
-- map.fields[id].skip - OPTIONAL - Skip field if missing from order
--
--
-- Expects argument table.
-- REQUIRED:
-- tpl_args - arguments passed to template after preprecessing
-- frame - frame object
-- table_map - table mapping object
-- rtr - if set return cargo props instead of storing them
m_cargo = m_cargo or require('Module:Cargo')
local tpl_args = args.tpl_args
local frame = args.frame
local map = args.table_map
local cargo_values = {_table = map.table}
-- for checking missing keys in order
local available_fields = {}
for key, field in pairs(map.fields) do
if field.skip == nil then
available_fields[key] = true
end
end
-- main loop
for _, key in ipairs(map.order) do
local field = map.fields[key]
if field == nil then
error(string.format(i18n.errors.missing_key_in_fields, key, map.table))
else
available_fields[key] = nil
end
-- key in argument mapping
local args_key
if field.name then
args_key = field.name
else
args_key = key
end
-- Retrieve value
local value
-- automatic handling only works if the field type is set
if field.type ~= nil then
value = tpl_args[args_key]
local cfield = m_cargo.parse_field{field=field.type}
local handler
if cfield.type == 'Integer' or cfield.type == 'Float' then
handler = tonumber
elseif cfield.type == 'Boolean' then
handler = function (value)
return util.cast.boolean(value, {cast_nil=false})
end
end
if cfield.list and value ~= nil then
-- ignore whitespace between separator and values
value = util.string.split(value, cfield.list .. '%s*')
if handler then
for index, v in ipairs(value) do
value[index] = handler(v)
if value[index] == nil then
error(string.format(i18n.errors.handler_returned_nil, map.table, args_key, v, field.type))
end
end
end
elseif handler and value ~= nil then
value = handler(value)
if value == nil then
error(string.format(i18n.errors.handler_returned_nil, map.table, args_key, tpl_args[args_key], field.type))
end
end
-- Don't need special handling: String, Text, Wikitext, Searchtext
-- Consider: Page, Date, Datetime, Coordinates, File, URL, Email
end
if field.func ~= nil then
value = field.func(tpl_args, frame, value)
end
-- Check defaults
if value == nil and field.default ~= nil then
if type(field.default) == 'function' then
value = field.default(tpl_args, frame)
elseif type(field.default) == 'table' then
mw.logObject(string.format(i18n.errors.table_object_as_default, key, map.table))
value = mw.clone(field.default)
else
value = field.default
end
end
-- Add value to arguments and cargo data
if value ~= nil then
-- key will be used here since the value will be used internally from here on in english
tpl_args[key] = value
if field.field ~= nil then
cargo_values[field.field] = value
end
elseif field.required == true then
error(string.format(i18n.errors.argument_required, args_key))
end
end
-- check for missing keys and return error if any are missing
local missing = {}
for key, _ in pairs(available_fields) do
missing[#missing+1] = key
end
if #missing > 0 then
error(string.format(i18n.errors.missing_key_in_order, map.table, table.concat(missing, '\n')))
end
-- finally store data in DB
if args.rtr ~= nil then
return cargo_values
else
m_cargo.store(frame, cargo_values)
end
end
function util.args.template_to_lua(str)
--[[
Convert templates to lua format. Simplifes debugging and creating
examples.
Parameters
----------
str : string
The entire template wrapped into string. Tip: Use Lua's square
bracket syntax for defining string literals.
Returns
-------
out : table
out.template - Template name.
out.args - arguments in table format.
out.args_to_str - arguments in readable string format.
]]
local out = {}
-- Get the template name:
out.template = string.match(str, '{{(.-)%s*|')
-- Remove everything but the arguments:
str = string.gsub(str, '%s*{{.-|', '')
str = string.gsub(str, '%s*}}%s*', '')
-- Split up the arguments:
out.args = {}
for i, v in ipairs(util.string.split(str, '%s*|%s*')) do
local arg = util.string.split(v, '%s*=%s*')
out.args[arg[1]] = arg[2]
out.args[#out.args+1] = arg[1]
end
-- Concate for easy copy/pasting:
local tbl = {}
for i, v in ipairs(out.args) do
tbl[#tbl+1]= string.format("%s='%s'", v, out.args[v])
end
out.args_to_str = table.concat(tbl, ',\n')
return out
end
-- ----------------------------------------------------------------------------
-- util.html
-- ----------------------------------------------------------------------------
util.html = {}
function util.html.abbr(text, title, options)
-- Outputs html tag <abbr> as string or as mw.html node.
--
-- options
-- class: class attribute
-- output: set to mw.html to return a mw.html node instead of a string
if not title then
return text
end
options = options or {}
local abbr = mw.html.create('abbr')
abbr:attr('title', title)
local class
if type(options) == 'table' and options.class then
class = options.class
else
class = options
end
if type(class) == 'string' then
abbr:attr('class', class)
end
abbr:wikitext(text)
if options.output == mw.html then
return abbr
end
return tostring(abbr)
end
function util.html.error(args)
-- Create an error message box
--
-- Args:
-- msg - message
if args == nil then
args = {}
end
local err = mw.html.create('strong')
err
:addClass('error')
:tag('span')
:addClass('module-error')
:wikitext(i18n.errors.module_error .. (args.msg or ''))
:done()
return tostring(err)
end
function util.html.poe_color(label, text, class)
if text == nil or text == '' then
return nil
end
class = class and (' ' .. class) or ''
return tostring(mw.html.create('em')
:attr('class', 'tc -' .. label .. class)
:wikitext(text))
end
util.html.poe_colour = util.html.poe_color
function util.html.tooltip(abbr, text, class)
return string.format('<span class="hoverbox c-tooltip %s"><span class="hoverbox__activator c-tooltip__activator">%s</span><span class="hoverbox__display c-tooltip__display">%s</span></span>', class or '', abbr or '', text or '')
end
util.html.td = {}
function util.html.td.na(options)
--
-- options:
-- as_tag
-- output: set to mw.html to return a mw.html node instead of a string
options = options or {}
-- N/A table row, requires mw.html.create instance to be passed
local td = mw.html.create('td')
td
:attr('class', 'table-na')
:wikitext(i18n.na)
:done()
if options.as_tag or options.output == mw.html then
return td
end
return tostring(td)
end
function util.html.format_value(tpl_args, frame, value, options)
-- value: table
-- min:
-- max:
-- options: table
-- func: Function to transform the value retrieved from the database
-- fmt: Format string (or function that returns format string) to use for the value. Default: '%s'
-- fmt_range: Format string to use for the value range. Default: '(%s-%s)'
-- color: poe_color code to use for the value range. False for no color. Default: 'mod'
-- class: Additional css class added to color tag
-- inline: Format string to use for the output
-- inline_color: poe_color code to use for the output. False for no color. Default: 'default'
-- inline_class: Additional css class added to inline color tag
-- no_color: (Deprecated; use color=false instead)
-- return_color: (Deprecated; returns both value.out and value without this)
if options.color ~= false and options.no_color == nil then
local base = {
min = value.base_min or value.base,
max = value.base_max or value.base,
}
if options.color then
value.color = options.color
elseif value.min ~= base.min or value.max ~= base.max then
value.color = 'mod'
else
value.color = 'value'
end
end
if options.func then
value.min = options.func(tpl_args, frame, value.min)
value.max = options.func(tpl_args, frame, value.max)
end
options.fmt = options.fmt or '%s'
if type(options.fmt) == 'function' then -- Function that returns the format string
options.fmt = options.fmt(tpl_args, frame, value)
end
if value.min == value.max then -- Static value
value.out = string.format(options.fmt, value.min)
else -- Range value
options.fmt_range = options.fmt_range or i18n.range
value.out = string.format(
string.format(options.fmt_range, options.fmt, options.fmt),
value.min,
value.max
)
end
if value.color then
value.out = util.html.poe_color(value.color, value.out, options.class)
end
if type(options.inline) == 'function' then
options.inline = options.inline(tpl_args, frame, value)
end
if options.inline and options.inline ~= '' then
value.out = string.format(options.inline, value.out)
if options.inline_color ~= false then
options.inline_color = options.inline_color or 'default'
value.out = util.html.poe_color(options.inline_color, value.out, options.inline_class)
end
end
local return_color = options.return_color and value.color or nil
if return_color then
return value.out, return_color
end
return value.out, value
end
-- ----------------------------------------------------------------------------
-- util.misc
-- ----------------------------------------------------------------------------
util.misc = {}
function util.misc.is_frame(frame)
-- the type of the frame is a table containing the functions, so check whether some of these exist
-- should be enough to avoid collisions.
return not(frame == nil or type(frame) ~= 'table' or (frame.argumentPairs == nil and frame.callParserFunction == nil))
end
function util.misc.get_frame(frame)
if util.misc.is_frame(frame) then
return frame
end
return mw.getCurrentFrame()
end
function util.misc.maybe_sandbox(module_name)
-- Did we load or {{#invoke:}} a module sandbox?
local frame = mw.getCurrentFrame()
if module_name and package.loaded[string.format('Module:%s/sandbox', module_name)] ~= nil or string.find(frame:getTitle(), 'sandbox', 1, true) then
return true
end
return false
end
function util.misc.add_category(categories, args)
-- categories: table of categories
-- args: table of extra arguments
-- namespace: id of namespace to validate against
-- 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
function util.misc.raise_error_or_return(args)
--
-- Arguments:
-- args: table of arguments to this function (must be set)
-- One required:
-- raise_required: Don't raise errors and return html errors instead unless raisae is set in arguments
-- no_raise_required: Don't return html errors and raise errors insetad unless no_raise is set in arguments
--
-- Optional:
-- msg: error message to raise or return, default: nil
-- args: argument directory to validate against (e.x. template args), default: {}
args.args = args.args or {}
args.msg = args.msg or ''
if args.raise_required ~= nil then
if args.args.raise ~= nil then
error(args.msg)
else
return util.html.error{msg=args.msg}
end
elseif args.no_raise_required ~= nil then
if args.args.no_raise ~= nil then
return util.html.error{msg=args.msg}
else
error(args.msg)
end
else
error(i18n.errors.invalid_raise_error_or_return_usage)
end
end
-- ----------------------------------------------------------------------------
-- util.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 a string into a table
This does essentially the same thing as mw.text.split, but with better
performance. However, it is not Unicode-aware.
str String to split
pattern Pattern to use for splitting
--]]
local out = {}
local i = 1
local split_start, split_end = string.find(str, pattern, i)
while split_start do
out[#out+1] = string.sub(str, i, split_start-1)
i = split_end+1
split_start, split_end = string.find(str, pattern, i)
end
out[#out+1] = string.sub(str, i)
return out
end
function util.string.split_outer(str, pattern, outer)
--[[
Split a string into a table according to the pattern, ignoring
matching patterns inside the outer patterns.
Parameters
----------
str : string
String to split.
pattern : string
Pattern to split on.
outer : table of strings where #outer = 2.
Table with 2 strings that defines the opening and closing patterns
to match, for example parantheses or brackets.
Returns
-------
out : table
table of split strings.
Examples
--------
-- Nesting at the end:
str = 'mods.id, CONCAT(mods.id, mods.name)'
mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'}))
table#1 {
"mods.id",
"CONCAT(mods.id, mods.name)",
}
-- Nesting in the middle:
str = 'mods.id, CONCAT(mods.id, mods.name), mods.required_level'
mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'}))
table#1 {
"mods.id",
"CONCAT(mods.id, mods.name)",
"mods.required_level",
}
]]
local out = {}
local nesting_level = 0
local i = 0
local pttrn = '(.-)' .. '(' .. pattern .. ')'
for v, sep in string.gmatch(str, pttrn) do
if nesting_level == 0 then
-- No nesting is occuring:
out[#out+1] = v
else
-- Nesting is occuring:
out[#out] = (out[math.max(#out, 1)] or '') .. v
end
-- Increase nesting level:
if string.find(v, outer[1]) then -- Multiple matches?
nesting_level = nesting_level + 1
end
if string.find(v, outer[2]) then
nesting_level = nesting_level - 1
end
-- Add back the separator if nesting is occuring:
if nesting_level ~= 0 then
out[#out] = out[#out] .. sep
end
-- Get the last index value:
i = i + #v + #sep
end
-- Complement with the last part of the string:
if nesting_level == 0 then
out[#out+1] = string.sub(str, math.max(i+1, 1))
else
out[#out] = out[#out] .. string.sub(str, math.max(i+1, 1))
-- TODO: Check if nesting level is zero?
end
return out
end
function util.string.split_args(str, args)
-- Splits arguments string into a table
--
-- str: String of arguments to split
-- args: table of extra arguments
-- sep: separator to use (default: ,)
-- kvsep: separator to use for key value pairs (default: =)
local out = {}
if args == nil then
args = {}
end
args.sep = args.sep or ','
args.kvsep = args.kvsep or '='
if str ~= nil then
local row
for _, str in ipairs(util.string.split(str, args.sep)) do
row = util.string.split(str, args.kvsep)
if #row == 1 then
out[#out+1] = row[1]
elseif #row == 2 then
out[row[1]] = row[2]
else
error(string.format(i18n.number_of_arguments_too_large, #row))
end
end
end
return out
end
function util.string.format(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
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.keys(tbl)
-- Returns the keys of a table
local keys = {}
for k, _ in pairs(tbl) do
keys[#keys+1] = k
end
return keys
end
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.assoc_to_array(tbl, args)
-- Turn associative array into an array, discarding the values
local out = {}
for key, _ in pairs(tbl) do
out[#out+1] = key
end
return out
end
function util.table.has_all_value(tbl, keys, value)
-- Whether all the table values with the specified keys are the specified value
for _, k in ipairs(keys or {}) do
if tbl[k] ~= value then
return false
end
end
return true
end
function util.table.has_one_value(tbl, keys, value)
-- Whether one of table values with the specified keys is the specified value
for _, k in ipairs(keys or {}) do
if tbl[k] == value then
return true
end
end
return false
end
function util.table.find_in_nested_array(args)
-- Iterates thoguh the given nested array and finds the given value
--
-- ex.
-- data = {
-- {a=5}, {a=6}}
-- find_nested_array{arg=6, tbl=data, key='a'} -> 6
-- find_nested_array(arg=10, tbl=data, key='a'} -> nil
-- -> returns "6"
--
-- args: Table containing:
-- value: value of the argument
-- tbl: table of valid options
-- key: key or table of key of in tbl
-- rtrkey: if key is table, return this key instead of the value instead
-- rtrvalue: default: true
local rtr
if type(args.key) == 'table' then
for _, item in ipairs(args.tbl) do
for _, k in ipairs(args.key) do
if item[k] == args.value then
rtr = item
break
end
end
end
elseif args.key == nil then
for _, item in ipairs(args.tbl) do
if item == args.value then
rtr = item
break
end
end
else
for _, item in ipairs(args.tbl) do
if item[args.key] == args.value then
rtr = item
break
end
end
end
if rtr == nil then
return rtr
end
if args.rtrkey ~= nil then
return rtr[args.rtrkey]
elseif args.rtrvalue or args.rtrvalue == nil then
return args.value
else
return rtr
end
end
-- ----------------------------------------------------------------------------
-- util.Struct
-- ----------------------------------------------------------------------------
util.Struct = function(map)
local this = {map = map}
-- sets a value to a field
function this:set(field, value)
if not field or not value then
error('One or more arguments are nils')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
if _.validate then
_.value = _.validate(value)
else
_.value = value
end
-- this happen if 'validate' returns nil
if _.required == true and _.value == nil then
error(string.format('Field "%s" is required but has been set to nil', field))
end
end
-- adds a new prop to a field
function this:set_prop(field, prop, value)
if not field or not prop or not value then
error('One or more arguments are nils')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
_[prop] = value
end
-- gets a value from a field
function this:get(field)
if not field then
error('Argument field is nil')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
return _.value
end
-- gets a value from a prop field
function this:get_prop(field, prop)
if not field or not prop then
error('One or more arguments are nils')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
return _[prop]
end
-- shows a value from a field
function this:show(field)
if not field then
error('Argument field is nil')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
if _.show then
return _.show(_)
else
return _.value
end
end
return this
end
-- ----------------------------------------------------------------------------
return util