Нет описания правки |
Нет описания правки |
||
(не показано 17 промежуточных версий этого же участника) | |||
Строка 1: | Строка 1: | ||
+ | ------------------------------------------------------------------------------- |
||
− | -- Utility stuff |
||
+ | -- |
||
+ | -- Module:Util |
||
+ | -- |
||
+ | -- This meta module contains a number of utility functions |
||
+ | ------------------------------------------------------------------------------- |
||
local xtable = require('Module:Table') |
local xtable = require('Module:Table') |
||
+ | local m_cargo -- Lazy load require('Module:Cargo') |
||
− | local util = {} |
||
+ | -- The cfg table contains all localisable strings and configuration, to make it |
||
− | local string_format = string.format |
||
+ | -- easier to port this module to another wiki. |
||
− | local infinity = math.huge |
||
+ | local cfg = mw.loadData('Module:Util/config') |
||
− | local |
+ | local i18n = cfg.i18n |
− | local cargo = mw.ext.cargo |
||
+ | local util = {} |
||
− | |||
− | local i18n = { |
||
− | bool_false = {'false', '0', 'disabled', 'off', 'no', '', 'deactivated'}, |
||
− | range = '(%s-%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 = 'Элемент "%s" не найден', |
||
− | |||
− | -- util.cast.factory.percentage |
||
− | invalid_argument = 'Недопустимый аргумент "%s". Проверите документацию для допустимых значений значений', |
||
− | not_a_percentage = '%s должно быть в процентах (в диапазоне от 0 до 100).', |
||
− | |||
− | -- util.cast.boolean |
||
− | not_a_boolean = 'значение "%s" типа "%s" не является логическим', |
||
− | |||
− | -- util.cast.number |
||
− | not_a_number = 'значение "%s" типа "%s" не является целым числом', |
||
− | number_too_small = '"%i" слишком мало. Минимум: "%i"', |
||
− | number_too_large = '"%i" слишком много. Максимум: "%i"', |
||
− | |||
− | -- util.cast.version |
||
− | malformed_version_string = 'Malformed version string "%s"', |
||
− | non_number_version_component = '"%s" имеет нечисловой компонент', |
||
− | 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 = 'Должны быть указаны %s и %s', |
||
− | |||
− | -- 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 = 'Ошибка модуля: ', |
||
− | |||
− | -- util.misc.raise_error_or_return |
||
− | invalid_raise_error_or_return_usage = 'Недопустимое использование 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).', |
||
− | }, |
||
− | } |
||
-- ---------------------------------------------------------------------------- |
-- ---------------------------------------------------------------------------- |
||
Строка 428: | Строка 362: | ||
end |
end |
||
− | function util.args.weight_list |
+ | 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 |
||
-- |
-- |
||
Строка 436: | Строка 370: | ||
-- frame - if set, automtically set subobjects |
-- frame - if set, automtically set subobjects |
||
-- input_argument - input prefix for parsing the arguments from the argtbl |
-- input_argument - input prefix for parsing the arguments from the argtbl |
||
− | -- subobject_name - name of the subobject |
+ | -- subobject_name - name of the subobject |
+ | |||
+ | m_cargo = m_cargo or require('Module:Cargo') |
||
+ | |||
args = args or {} |
args = args or {} |
||
args.input_argument = args.input_argument or 'spawn_weight' |
args.input_argument = args.input_argument or 'spawn_weight' |
||
Строка 466: | Строка 403: | ||
if args.frame and args.cargo_table then |
if args.frame and args.cargo_table then |
||
− | + | m_cargo.store(args.frame, { |
|
_table = args.cargo_table, |
_table = args.cargo_table, |
||
ordinal = i, |
ordinal = i, |
||
Строка 479: | Строка 416: | ||
end |
end |
||
− | function util.args.version |
+ | function util.args.version(argtbl, args) |
-- in any prefix spaces should be included |
-- in any prefix spaces should be included |
||
-- |
-- |
||
Строка 519: | Строка 456: | ||
end |
end |
||
− | local results = |
+ | 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 '), |
} |
} |
||
) |
) |
||
Строка 539: | Строка 476: | ||
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 |
+ | -- 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: |
-- It will expect/handle the following fields: |
||
Строка 564: | Строка 501: | ||
-- table_map - table mapping object |
-- table_map - table mapping object |
||
-- rtr - if set return cargo props instead of storing them |
-- 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 tpl_args = args.tpl_args |
||
local frame = args.frame |
local frame = args.frame |
||
Строка 599: | Строка 539: | ||
value = tpl_args[args_key] |
value = tpl_args[args_key] |
||
− | local cfield = |
+ | local cfield = m_cargo.parse_field{field=field.type} |
local handler |
local handler |
||
if cfield.type == 'Integer' or cfield.type == 'Float' then |
if cfield.type == 'Integer' or cfield.type == 'Float' then |
||
Строка 668: | Строка 608: | ||
return cargo_values |
return cargo_values |
||
else |
else |
||
− | + | m_cargo.store(frame, cargo_values) |
|
end |
end |
||
+ | end |
||
+ | |||
+ | function util.args.template_to_lua(str) |
||
+ | --[[ |
||
+ | Convert templates to lua format. Simplifes debugging and creating |
||
+ | examples. |
||
+ | |||
+ | Parameters |
||
+ | ---------- |
||
+ | str : string |
||
+ | The entire template wrapped into string. Tip: Use Lua's square |
||
+ | bracket syntax for defining string literals. |
||
+ | |||
+ | Returns |
||
+ | ------- |
||
+ | out : table |
||
+ | out.template - Template name. |
||
+ | out.args - arguments in table format. |
||
+ | out.args_to_str - arguments in readable string format. |
||
+ | ]] |
||
+ | local out = {} |
||
+ | |||
+ | -- Get the template name: |
||
+ | out.template = string.match(str, '{{(.-)%s*|') |
||
+ | |||
+ | -- Remove everything but the arguments: |
||
+ | str = string.gsub(str, '%s*{{.-|', '') |
||
+ | str = string.gsub(str, '%s*}}%s*', '') |
||
+ | |||
+ | -- Split up the arguments: |
||
+ | out.args = {} |
||
+ | for i, v in ipairs(util.string.split(str, '%s*|%s*')) do |
||
+ | local arg = util.string.split(v, '%s*=%s*') |
||
+ | out.args[arg[1]] = arg[2] |
||
+ | out.args[#out.args+1] = arg[1] |
||
+ | end |
||
+ | |||
+ | -- Concate for easy copy/pasting: |
||
+ | local tbl = {} |
||
+ | for i, v in ipairs(out.args) do |
||
+ | tbl[#tbl+1]= string.format("%s='%s'", v, out.args[v]) |
||
+ | end |
||
+ | out.args_to_str = table.concat(tbl, ',\n') |
||
+ | |||
+ | return out |
||
end |
end |
||
Строка 677: | Строка 662: | ||
util.html = {} |
util.html = {} |
||
+ | |||
− | function util.html.abbr(abbr, text, class) |
||
+ | function util.html.abbr(text, title, options) |
||
− | return string.format('<abbr title="%s" class="%s">%s</abbr>', text or '', class or '', abbr or '') |
||
+ | -- Outputs html tag <abbr> as string or as mw.html node. |
||
+ | -- |
||
+ | -- options |
||
+ | -- class: class attribute |
||
+ | -- output: set to mw.html to return a mw.html node instead of a string |
||
+ | if not title then |
||
+ | return text |
||
+ | end |
||
+ | options = options or {} |
||
+ | local abbr = mw.html.create('abbr') |
||
+ | abbr:attr('title', title) |
||
+ | local class |
||
+ | if type(options) == 'table' and options.class then |
||
+ | class = options.class |
||
+ | else |
||
+ | class = options |
||
+ | end |
||
+ | if type(class) == 'string' then |
||
+ | abbr:attr('class', class) |
||
+ | end |
||
+ | abbr:wikitext(text) |
||
+ | if options.output == mw.html then |
||
+ | return abbr |
||
+ | end |
||
+ | return tostring(abbr) |
||
end |
end |
||
Строка 690: | Строка 700: | ||
end |
end |
||
− | local err = mw.html.create(' |
+ | local err = mw.html.create('strong') |
err |
err |
||
− | : |
+ | :addClass('error') |
+ | :tag('span') |
||
− | :wikitext(i18n.errors.module_error .. (args.msg or '')) |
||
− | : |
+ | :addClass('module-error') |
+ | :wikitext(i18n.errors.module_error .. (args.msg or '')) |
||
+ | :done() |
||
return tostring(err) |
return tostring(err) |
||
Строка 711: | Строка 723: | ||
function util.html.tooltip(abbr, text, class) |
function util.html.tooltip(abbr, text, class) |
||
− | return string.format('<span 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 |
end |
||
util.html.td = {} |
util.html.td = {} |
||
− | function util.html.td.na( |
+ | function util.html.td.na(options) |
-- |
-- |
||
− | -- |
+ | -- options: |
− | -- as_tag |
+ | -- as_tag |
+ | -- output: set to mw.html to return a mw.html node instead of a string |
||
− | args = args or {} |
||
+ | options = options or {} |
||
-- N/A table row, requires mw.html.create instance to be passed |
-- N/A table row, requires mw.html.create instance to be passed |
||
local td = mw.html.create('td') |
local td = mw.html.create('td') |
||
td |
td |
||
:attr('class', 'table-na') |
:attr('class', 'table-na') |
||
− | :wikitext( |
+ | :wikitext(i18n.na) |
:done() |
:done() |
||
− | if |
+ | 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 |
||
Строка 738: | Строка 750: | ||
-- max: |
-- max: |
||
-- options: table |
-- 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' |
||
− | -- no_color: set to true to ingore colour entirely |
||
+ | -- inline_class: Additional css class added to inline color tag |
||
− | -- return_color: also return colour |
||
− | -- |
+ | -- no_color: (Deprecated; use color=false instead) |
+ | -- return_color: (Deprecated; returns both value.out and value without this) |
||
− | |||
− | if options.no_color == nil then |
+ | if options.color ~= false and options.no_color == nil then |
if options.color then |
if options.color then |
||
value.color = options.color |
value.color = options.color |
||
Строка 757: | Строка 769: | ||
end |
end |
||
end |
end |
||
+ | if options.func then |
||
− | |||
− | if options.func ~= nil then |
||
value.min = options.func(tpl_args, frame, value.min) |
value.min = options.func(tpl_args, frame, value.min) |
||
value.max = options.func(tpl_args, frame, value.max) |
value.max = options.func(tpl_args, frame, value.max) |
||
end |
end |
||
+ | options.fmt = options.fmt or '%s' |
||
− | |||
− | if options.fmt == |
+ | if type(options.fmt) == 'function' then -- Function that returns the format string |
− | options.fmt = '%s' |
||
− | elseif type(options.fmt) == 'function' then |
||
options.fmt = options.fmt(tpl_args, frame, value) |
options.fmt = options.fmt(tpl_args, frame, value) |
||
end |
end |
||
+ | if value.min == value.max then -- Static value |
||
− | |||
− | if value.min == value.max then |
||
value.out = string.format(options.fmt, value.min) |
value.out = string.format(options.fmt, value.min) |
||
− | else |
+ | else -- Range value |
− | + | options.fmt_range = options.fmt_range or i18n.range |
|
+ | value.out = string.format( |
||
+ | string.format(options.fmt_range, options.fmt, options.fmt), |
||
+ | value.min, |
||
+ | value.max |
||
+ | ) |
||
end |
end |
||
+ | if value.color then |
||
− | |||
− | if options.no_color == nil then |
||
value.out = util.html.poe_color(value.color, value.out, options.class) |
value.out = util.html.poe_color(value.color, value.out, options.class) |
||
end |
end |
||
+ | if type(options.inline) == 'function' then |
||
− | |||
+ | options.inline = options.inline(tpl_args, frame, value) |
||
− | local return_color |
||
− | if options.return_color ~= nil then |
||
− | return_color = value.color |
||
end |
end |
||
+ | if options.inline and options.inline ~= '' then |
||
− | |||
− | + | value.out = string.format(options.inline, value.out) |
|
+ | if options.inline_color ~= false then |
||
− | |||
+ | options.inline_color = options.inline_color or 'default' |
||
− | if type(text) == 'string' then |
||
+ | value.out = util.html.poe_color(options.inline_color, value.out, options.inline_class) |
||
− | elseif type(text) == 'function' then |
||
− | text = text(tpl_args, frame, value) |
||
− | else |
||
− | text = nil |
||
− | end |
||
− | |||
− | if text and text ~= '' then |
||
− | local color |
||
− | if options.inline_color == nil then |
||
− | color = 'default' |
||
− | elseif options.inline_color ~= false then |
||
− | color = color.inline_color |
||
− | end |
||
− | |||
− | if color ~= nil then |
||
− | text = util.html.poe_color(color, text, options.class) |
||
end |
end |
||
− | |||
− | return string.format(text, value.out), return_color |
||
end |
end |
||
+ | local return_color = options.return_color and value.color or nil |
||
− | |||
+ | if return_color then |
||
− | -- If we didn't return before, return here |
||
− | return value.out, return_color |
+ | return value.out, return_color |
+ | end |
||
+ | return value.out, value |
||
end |
end |
||
Строка 830: | Строка 825: | ||
end |
end |
||
− | util.misc. |
+ | function util.misc.maybe_sandbox(frame) |
+ | -- Did {{#invoke:}} call sandbox version? |
||
− | util.misc.category_blacklist.sub_pages = { |
||
+ | frame = util.misc.get_frame(frame) |
||
− | doc = true, |
||
− | sandbox |
+ | if string.find(frame:getTitle(), 'sandbox', 1, true) then |
− | + | return true |
|
+ | end |
||
− | testcases = true, |
||
+ | return false |
||
− | } |
||
+ | end |
||
− | |||
− | util.misc.category_blacklist.namespaces = { |
||
− | Template = true, |
||
− | Template_talk = true, |
||
− | Module = true, |
||
− | Module_talk = true, |
||
− | User = true, |
||
− | User_talk = true, |
||
− | } |
||
function util.misc.add_category(categories, args) |
function util.misc.add_category(categories, args) |
||
Строка 861: | Строка 848: | ||
args = {} |
args = {} |
||
end |
end |
||
− | |||
local title = mw.title.getCurrentTitle() |
local title = mw.title.getCurrentTitle() |
||
− | local sub_blacklist = args.sub_page_blacklist or |
+ | local sub_blacklist = args.sub_page_blacklist or cfg.misc.category_blacklist.sub_pages |
− | local ns_blacklist = args.namespace_blacklist or |
+ | 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 |
||
Строка 941: | Строка 927: | ||
return true |
return true |
||
− | end |
||
− | |||
− | -- ---------------------------------------------------------------------------- |
||
− | -- util.cargo |
||
− | -- ---------------------------------------------------------------------------- |
||
− | |||
− | util.cargo = {} |
||
− | |||
− | function util.cargo.declare(frame, args) |
||
− | return frame:callParserFunction('#cargo_declare:', args) |
||
− | end |
||
− | |||
− | function util.cargo.attach(frame, args) |
||
− | return frame:callParserFunction('#cargo_attach:', args) |
||
− | end |
||
− | |||
− | function util.cargo.store(frame, values, args) |
||
− | -- Calls the cargo_store parser function and ensures the values passed are casted properly |
||
− | -- |
||
− | -- Value handling: |
||
− | -- tables - automatically concat |
||
− | -- booleans - automatically casted to 1 or 0 to ensure they're stored properly |
||
− | -- |
||
− | -- 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 |
||
− | |||
− | function util.cargo.declare_factory(args) |
||
− | -- Returns a function that can be called by templates to declare cargo tables |
||
− | -- |
||
− | -- args |
||
− | -- data: data table |
||
− | -- table: name of cargo table |
||
− | -- fields: associative table with: |
||
− | -- field: name of the field to declare |
||
− | -- type: type of the field |
||
− | return function (frame) |
||
− | frame = util.misc.get_frame(frame) |
||
− | |||
− | local dcl_args = {} |
||
− | dcl_args._table = args.data.table |
||
− | for k, field_data in pairs(args.data.fields) do |
||
− | if field_data.field then |
||
− | dcl_args[field_data.field] = field_data.type |
||
− | end |
||
− | end |
||
− | |||
− | return util.cargo.declare(frame, dcl_args) |
||
− | end |
||
− | end |
||
− | |||
− | function util.cargo.attach_factory(args) |
||
− | -- Returns a function that can be called by templates to attach cargo tables |
||
− | -- |
||
− | -- args |
||
− | -- data: data table |
||
− | -- table: name of cargo table |
||
− | -- fields: associative table with: |
||
− | -- field: name of the field to declare |
||
− | -- type: type of the field |
||
− | return function (frame) |
||
− | frame = util.misc.get_frame(frame) |
||
− | |||
− | local attach_args = {} |
||
− | attach_args._table = args.data.table |
||
− | |||
− | return util.cargo.attach(frame, attach_args) |
||
− | end |
||
− | end |
||
− | |||
− | --mw.logObject(p.cargo.map_results_to_id{results=mw.ext.cargo.query('mods,spawn_weights', 'mods._pageID,spawn_weights.tag', {where='mods.id="Strength1"', join='mods._pageID=spawn_weights._pageID'}), table_name='mods'}) |
||
− | function util.cargo.map_results_to_id(args) |
||
− | -- Maps the results passed to a table containing the specified field as key and a table of rows for the particular page as values. |
||
− | -- |
||
− | -- args |
||
− | -- results : table of results returned from mw.ext.cargo.query to map to the specified id field |
||
− | -- field : name of the id field to map results to |
||
− | -- the field has to be in the fields list of the original query or it will cause errors |
||
− | -- keep_id_field : if set, don't delete _pageID |
||
− | -- |
||
− | -- return |
||
− | -- table |
||
− | -- key : the specified field |
||
− | -- value : array containing the found rows (in the order that they were found) |
||
− | local out = {} |
||
− | for _, row in ipairs(args.results) do |
||
− | local pid = row[args.field] |
||
− | if out[pid] then |
||
− | out[pid][#out[pid]+1] = row |
||
− | else |
||
− | out[pid] = {row} |
||
− | end |
||
− | -- discard the pageID, don't need this any longer in most cases |
||
− | if args.keep_id_field == nil then |
||
− | row[args.field] = nil |
||
− | end |
||
− | end |
||
− | |||
− | 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: |
||
− | -- REQUIRED: |
||
− | -- tables - array of tables (see util.cargo.query) |
||
− | -- fields - array of fields (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 |
||
− | -- OPTIONAL: |
||
− | -- query - array containing cargo sql clauses [optional] (see util.cargo.query) |
||
− | -- ingore_missing - skip the check for missing fields entirely |
||
− | -- warning_on_missing - issue warning instead of error if missing values |
||
− | -- |
||
− | -- RETURN: |
||
− | -- table - results as given by mw.ext.cargo.query |
||
− | -- msg - any error messages if it was used as warning |
||
− | args.query = args.query or {} |
||
− | |||
− | args.fields[#args.fields+1] = args.id_field |
||
− | |||
− | if #args.id_array == 0 then |
||
− | return {} |
||
− | end |
||
− | |||
− | -- for error returning |
||
− | local msg = {} |
||
− | |||
− | 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 |
||
− | -- |
||
− | |||
− | local results = util.cargo.query( |
||
− | args.tables, |
||
− | args.fields, |
||
− | args.query |
||
− | ) |
||
− | |||
− | -- |
||
− | -- Check missing results |
||
− | -- |
||
− | if not args.ignore_missing and #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 |
||
− | |||
− | local missing_ids = {} |
||
− | for k, _ in pairs(missing) do |
||
− | missing_ids[#missing_ids+1] = k |
||
− | end |
||
− | |||
− | msg[#msg+1] = string.format(i18n.errors.missing_ids, args.id_field, table.concat(missing_ids, '\n')) |
||
− | if args.warning_on_missing == nil then |
||
− | error(msg[#msg]) |
||
− | else |
||
− | mw.logObject(msg[#msg]) |
||
− | end |
||
− | end |
||
− | |||
− | return results, msg |
||
− | end |
||
− | |||
− | 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_\.]+' |
||
− | if args.mode == 'like' or args.mode == nil then |
||
− | return string.gsub( |
||
− | args.string, |
||
− | string.format('(%s) HOLDS ([NOT ]*)([LIKE ]*)"([^"]+)"', args.field), |
||
− | '%1__full %2LIKE "%%%4%%"' |
||
− | ) |
||
− | elseif args.mode == 'regex' then |
||
− | args.separator = args.separator or ',' |
||
− | return string.gsub( |
||
− | args.string, |
||
− | string.format('(%s) HOLDS ([NOT ]*)"([^"]+)"', args.field), |
||
− | string.format('%%1__full %%2REGEXP "(%s|^)%%3(%s|$)"', args.separator, args.separator) |
||
− | ) |
||
− | else |
||
− | error('Invalid mode specified. Acceptable values are like or regex.') |
||
− | 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 |
end |
||
Строка 1294: | Строка 934: | ||
util.string = {} |
util.string = {} |
||
+ | |||
function util.string.strip(str, pattern) |
function util.string.strip(str, pattern) |
||
pattern = pattern or '%s' |
pattern = pattern or '%s' |
||
Строка 1313: | Строка 954: | ||
end |
end |
||
out[#out+1] = string.sub(str, i) |
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 |
return out |
||
end |
end |
||
Строка 1347: | Строка 1067: | ||
return out |
return out |
||
+ | end |
||
+ | |||
+ | function util.string.format(str, vars) |
||
+ | --[[ |
||
+ | Allow string replacement using named arguments. |
||
+ | |||
+ | TODO: |
||
+ | * Support %d ? |
||
+ | * Support 0.2f ? |
||
+ | |||
+ | Parameters |
||
+ | ---------- |
||
+ | str : String to replace. |
||
+ | vars : Table of arguments. |
||
+ | |||
+ | Examples |
||
+ | -------- |
||
+ | = util.string.format('{foo} is {bar}.', {foo='Dinner', bar='nice'}) |
||
+ | |||
+ | References |
||
+ | ---------- |
||
+ | http://lua-users.org/wiki/StringInterpolation |
||
+ | ]] |
||
+ | |||
+ | if not vars then |
||
+ | vars = str |
||
+ | str = vars[1] |
||
+ | end |
||
+ | |||
+ | return (string.gsub(str, "({([^}]+)})", |
||
+ | function(whole, i) |
||
+ | return vars[i] or whole |
||
+ | end)) |
||
+ | end |
||
+ | |||
+ | util.string.pattern = {} |
||
+ | function util.string.pattern.valid_var_name() |
||
+ | --[[ |
||
+ | Get a pattern for a valid variable name. |
||
+ | ]] |
||
+ | return '%A?([%a_]+[%w_]*)[^%w_]?' |
||
end |
end |
||
Строка 1355: | Строка 1116: | ||
util.table = {} |
util.table = {} |
||
function util.table.length(tbl) |
function util.table.length(tbl) |
||
− | -- Get |
+ | -- 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 |
||
end |
end |
||
+ | return count |
||
end |
end |
||
+ | util.table.count = util.table.length |
||
function util.table.assoc_to_array(tbl, args) |
function util.table.assoc_to_array(tbl, args) |
||
Строка 1467: | Строка 1229: | ||
if not _ then |
if not _ then |
||
− | error( |
+ | error(string.format('Field "%s" doesn\'t exist', field)) |
end |
end |
||
Строка 1478: | Строка 1240: | ||
-- 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( |
+ | error(string.format('Field "%s" is required but has been set to nil', field)) |
end |
end |
||
end |
end |
||
Строка 1492: | Строка 1254: | ||
if not _ then |
if not _ then |
||
− | error( |
+ | error(string.format('Field "%s" doesn\'t exist', field)) |
end |
end |
||
Строка 1508: | Строка 1270: | ||
if not _ then |
if not _ then |
||
− | error( |
+ | error(string.format('Field "%s" doesn\'t exist', field)) |
end |
end |
||
Строка 1524: | Строка 1286: | ||
if not _ then |
if not _ then |
||
− | error( |
+ | error(string.format('Field "%s" doesn\'t exist', field)) |
end |
end |
||
Строка 1540: | Строка 1302: | ||
if not _ then |
if not _ then |
||
− | error( |
+ | error(string.format('Field "%s" doesn\'t exist', field)) |
end |
end |
||
Версия от 06:26, 11 июня 2021
Это мета-модуль, который предназначен для использования только другими модулями. Он не должен вызываться в викитексте. |
Этот модуль зависит от следующих других модулей: |
Описание
Предоставляет служебные функции для модулей программирования.
Структура
Группа | Описание |
---|---|
util.cast | утилиты для приведения значений (т. е. из аргументов) |
util.html | простые функции для создания HTML-тегов |
util.misc | прочие функции |
Использование
Этот модуль должен быть загружен с помощью require()
.
Вышеприведенная документация извлекается из Модуль:Util/doc.
Редакторы могут экспериментировать в sandbox этого модуля и в testcases страницах.
Подстраницы этого модуля.
Редакторы могут экспериментировать в sandbox этого модуля и в testcases страницах.
Подстраницы этого модуля.
-------------------------------------------------------------------------------
--
-- Module:Util
--
-- This meta module contains a number of utility functions
-------------------------------------------------------------------------------
local xtable = require('Module:Table')
local m_cargo -- Lazy load require('Module:Cargo')
-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = mw.loadData('Module:Util/config')
local i18n = cfg.i18n
local util = {}
-- ----------------------------------------------------------------------------
-- util.cast
-- ----------------------------------------------------------------------------
util.cast = {}
function util.cast.text(value, args)
-- Takes an arbitary value and returns it as texts.
--
-- Also strips any categories
--
-- args:
-- cast_nil - Cast lua nil value to "nil" string
-- Default: false
-- discard_empty - if the string is empty, return nil rather then empty string
-- Default: true
args = args or {}
if args.discard_empty == nil then
args.discard_empty = true
end
if value == nil and not args.cast_nil then
return
end
value = tostring(value)
if value == '' and args.discard_empty then
return
end
value = string.gsub(value, '%[%[Category:[%w_ ]+%]%]', '')
return value
end
function util.cast.boolean(value, args)
-- Takes an abitary value and casts it to a bool value
--
-- for strings false will be according to i18n.bool_false
--
-- args:
-- cast_nil - if set to false, it will not cast nil values
args = args or {}
local t = type(value)
if t == 'nil' then
if args.cast_nil == nil or args.cast_nil == true then
return false
else
return
end
elseif t == 'boolean' then
return value
elseif t == 'number' then
if value == 0 then return false end
return true
elseif t == 'string' then
local tmp = string.lower(value)
for _, v in ipairs(i18n.bool_false) do
if v == tmp then
return false
end
end
return true
else
error(string.format(i18n.errors.not_a_boolean, tostring(value), t))
end
end
function util.cast.number(value, args)
-- Takes an abitary value and attempts to cast it to int
--
-- args:
-- default: for strings, if default is nil and the conversion fails, an error will be returned
-- min: error if <min
-- max: error if >max
if args == nil then
args = {}
end
local t = type(value)
local val
if t == 'nil' then
val = nil
elseif t == 'boolean' then
if value then
val = 1
else
val = 0
end
elseif t == 'number' then
val = value
elseif t == 'string' then
val = tonumber(value)
end
if val == nil then
if args.default ~= nil then
val = args.default
else
error(string.format(i18n.errors.not_a_number, tostring(value), t))
end
end
if args.min ~= nil and val < args.min then
error(string.format(i18n.errors.number_too_small, val, args.min))
end
if args.max ~= nil and val > args.max then
error(string.format(i18n.errors.number_too_large, val, args.max))
end
return val
end
function util.cast.version(value, args)
-- Takes a string value and returns as version number
-- If the version number is invalid an error is raised
--
-- args:
-- return_type: defaults to "table"
-- table - Returns the version number broken down into sub versions as a table
-- string - Returns the version number as string
--
if args == nil then
args = {}
end
local result
if args.return_type == 'table' or args.return_type == nil then
result = util.string.split(value, '%.')
if #result ~= 3 then
error(string.format(i18n.errors.malformed_version_string, value))
end
result[4] = string.match(result[3], '%a+')
result[3] = string.match(result[3], '%d+')
for i=1,3 do
local v = tonumber(result[i])
if v == nil then
error(string.format(i18n.errors.non_number_version_component, value))
end
result[i] = v
end
elseif args.return_type == 'string' then
result = string.match(value, '%d+%.%d+%.%d+%a*')
end
if result == nil then
error(string.format(i18n.errors.unrecognized_version_number, value))
end
return result
end
--
-- util.cast.factory
--
-- This section is used to generate new functions for common argument parsing tasks based on specific options
--
-- All functions return a function which accepts two arguments:
-- tpl_args - arguments from the template
-- frame - current frame object
--
-- All factory functions accept have two arguments on creation:
-- k - the key in the tpl_args to retrive the value from
-- args - any addtional arguments (see function for details)
util.cast.factory = {}
function util.cast.factory.array_table(k, args)
-- Arguments:
-- tbl - table to check against
-- errmsg - error message if no element was found; should accept 1 parameter
args = args or {}
return function (tpl_args, frame)
local elements
if tpl_args[k] ~= nil then
elements = util.string.split(tpl_args[k], ',%s*')
for _, element in ipairs(elements) do
local r = util.table.find_in_nested_array{value=element, tbl=args.tbl, key='full'}
if r == nil then
error(string.format(args.errmsg or i18n.errors.missing_element, element))
end
end
tpl_args[args.key_out or k] = xtable:new(elements)
end
end
end
function util.cast.factory.table(k, args)
args = args or {}
return function (tpl_args, frame)
args.value = tpl_args[k]
if args.value == nil then
return
end
local value = util.table.find_in_nested_array(args)
if value == nil then
error(string.format(args.errmsg or i18n.errors.missing_element, k))
end
tpl_args[args.key_out or k] = value
end
end
function util.cast.factory.assoc_table(k, args)
-- Arguments:
--
-- tbl
-- errmsg
-- key_out
return function (tpl_args, frame)
local elements
if tpl_args[k] ~= nil then
elements = util.string.split(tpl_args[k], ',%s*')
for _, element in ipairs(elements) do
if args.tbl[element] == nil then
error(util.html.error{msg=string.format(args.errmsg or i18n.errors.missing_element, element)})
end
end
tpl_args[args.key_out or k] = elements
end
end
end
function util.cast.factory.number(k, args)
args = args or {}
return function (tpl_args, frame)
tpl_args[args.key_out or k] = tonumber(tpl_args[k])
end
end
function util.cast.factory.boolean(k, args)
args = args or {}
return function(tpl_args, frame)
if tpl_args[k] ~= nil then
tpl_args[args.key_out or k] = util.cast.boolean(tpl_args[k])
end
end
end
function util.cast.factory.percentage(k, args)
args = args or {}
return function (tpl_args, frame)
local v = tonumber(tpl_args[k])
if v == nil then
return util.html.error{msg=string.format(i18n.errors.invalid_argument, k)}
end
if v < 0 or v > 100 then
return util.html.error{msg=string.format(i18n.errors.not_a_percentage, k)}
end
tpl_args[args.key_out or k] = v
end
end
-- ----------------------------------------------------------------------------
-- util.args
-- ----------------------------------------------------------------------------
util.args = {}
function util.args.stats(argtbl, args)
-- in any prefix spaces should be included
--
-- argtbl: argument table to work with
-- args:
-- prefix: prefix if any
-- frame: frame used to set subobjects; if not set dont set properties
-- property_prefix: property prefix if any
-- subobject_prefix: subobject prefix if any
-- properties: table of properties to add if any
args = args or {}
args.prefix = args.prefix or ''
local i = 0
local stats = {}
repeat
i = i + 1
local prefix = string.format('%s%s%s_%s', args.prefix, i18n.args.stat_infix, i, '%s')
local id = {
id = string.format(prefix, i18n.args.stat_id),
min = string.format(prefix, i18n.args.stat_min),
max = string.format(prefix, i18n.args.stat_max),
value = string.format(prefix, i18n.args.stat_value),
}
local value = {}
for key, args_key in pairs(id) do
value[key] = argtbl[args_key]
end
if value.id ~= nil and ((value.min ~= nil and value.max ~= nil and value.value == nil) or (value.min == nil and value.max == nil and value.value ~= nil)) then
if value.value then
value.value = util.cast.number(value.value)
argtbl[id.value] = value.value
else
value.min = util.cast.number(value.min)
argtbl[id.min] = value.min
value.max = util.cast.number(value.max)
argtbl[id.max] = value.max
-- Also set average value
value.avg = (value.min + value.max)/2
argtbl[string.format('%sstat%s_avg', args.prefix, i)] = value.avg
end
argtbl[string.format('%sstat%s', args.prefix, i)] = value
stats[#stats+1] = value
elseif util.table.has_all_value(value, {'id', 'min', 'max', 'value'}, nil) then
value = nil
-- all other cases should be improperly set value
else
error(string.format(i18n.errors.improper_stat, args.prefix, i))
end
until value == nil
argtbl[string.format('%sstats', args.prefix)] = stats
end
function util.args.spawn_weight_list(argtbl, args)
args = args or {}
args.input_argument = i18n.args.spawn_weight_prefix
args.output_argument = 'spawn_weights'
args.cargo_table = 'spawn_weights'
util.args.weight_list(argtbl, args)
end
function util.args.generation_weight_list(argtbl, args)
args = args or {}
args.input_argument = i18n.args.generation_weight_prefix
args.output_argument = 'generation_weights'
args.cargo_table = 'generation_weights'
util.args.weight_list(argtbl, args)
end
function util.args.weight_list(argtbl, args)
-- Parses a weighted pair of lists and sets properties
--
-- argtbl: argument table to work with
-- args:
-- output_argument - if set, set arguments to this value
-- frame - if set, automtically set subobjects
-- input_argument - input prefix for parsing the arguments from the argtbl
-- subobject_name - name of the subobject
m_cargo = m_cargo or require('Module:Cargo')
args = args or {}
args.input_argument = args.input_argument or 'spawn_weight'
local i = 0
local id = nil
local value = nil
if args.output_argument then
argtbl[args.output_argument] = {}
end
repeat
i = i + 1
id = {
tag = string.format('%s%s_tag', args.input_argument, i),
value = string.format('%s%s_value', args.input_argument, i),
}
value = {
tag = argtbl[id.tag],
value = argtbl[id.value],
}
if value.tag ~= nil and value.value ~= nil then
if args.output_argument then
argtbl[args.output_argument][i] = value
end
if args.frame and args.cargo_table then
m_cargo.store(args.frame, {
_table = args.cargo_table,
ordinal = i,
tag = value.tag,
weight = util.cast.number(value.value, {min=0}),
})
end
elseif not (value.tag == nil and value.value == nil) then
error(string.format(i18n.errors.invalid_weight, id.tag, id.value))
end
until value.tag == nil
end
function util.args.version(argtbl, args)
-- in any prefix spaces should be included
--
-- argtbl: argument table to work with
-- args:
-- frame: frame for queries
-- set_properties: if defined, set properties on the page
-- variables: table of prefixes
args = args or {}
args.variables = args.variables or {
release = {},
removal = {},
}
local version_ids = {}
local version_keys = {}
for key, data in pairs(args.variables) do
local full_key = string.format('%s_version', key)
if argtbl[full_key] ~= nil then
local value = util.cast.version(argtbl[full_key], {return_type = 'string'})
argtbl[full_key] = value
data.value = value
if data.property ~= nil then
version_ids[#version_ids+1] = value
version_keys[value] = key
end
end
end
-- no need to do a query if nothing was fetched
if #version_ids > 0 then
if args.frame == nil then
error('Properties were set, but frame was not')
end
for i, id in ipairs(version_ids) do
version_ids[i] = string.format('Versions.version="%s"', id)
end
local results = m_cargo.query(
{'Versions'},
{'release_date', 'version'},
{
where = table.concat(version_ids, ' OR '),
}
)
if #results ~= #version_ids then
error(string.format(i18n.too_many_versions, #results, #version_ids))
end
for _, row in ipairs(results) do
local key = version_keys[row.version]
argtbl[string.format('%s_date', key)] = row.release_date
end
end
end
function util.args.from_cargo_map(args)
-- Maps the arguments from a cargo argument table (i.e. the ones used in m_cargo.declare_factory)
--
-- It will expect/handle the following fields:
-- map.order - REQUIRED - Array table for the order in which the arguments in map.fields will be parsed
-- map.table - REQUIRED - Table name (for storage)
-- map.fields[id].field - REQUIRED - Name of the field in cargo table
-- map.fields[id].type - REQUIRED - Type of the field in cargo table
-- map.fields[id].func - OPTIONAL - Function to handle the arguments. It will be passed tpl_args and frame.
-- The function should return the parsed value.
--
-- If no function is specified, default handling depending on the cargo field type will be used
-- map.fields[id].default - OPTIONAL - Default value if the value is not set or returned as nil
-- If default is a function, the function will be called with (tpl_args, frame) and expected to return a default value for the field.
-- map.fields[id].name - OPTIONAL - Name of the field in tpl_args if it differs from the id in map.fields. Used for i18n for example
-- map.fields[id].required - OPTIONAL - Whether a value for the field is required or whether it can be left empty
-- Note: With a default value the field will never be empty
-- map.fields[id].skip - OPTIONAL - Skip field if missing from order
--
--
-- Expects argument table.
-- REQUIRED:
-- tpl_args - arguments passed to template after preprecessing
-- frame - frame object
-- table_map - table mapping object
-- rtr - if set return cargo props instead of storing them
m_cargo = m_cargo or require('Module:Cargo')
local tpl_args = args.tpl_args
local frame = args.frame
local map = args.table_map
local cargo_values = {_table = map.table}
-- for checking missing keys in order
local available_fields = {}
for key, field in pairs(map.fields) do
if field.skip == nil then
available_fields[key] = true
end
end
-- main loop
for _, key in ipairs(map.order) do
local field = map.fields[key]
if field == nil then
error(string.format(i18n.errors.missing_key_in_fields, key, map.table))
else
available_fields[key] = nil
end
-- key in argument mapping
local args_key
if field.name then
args_key = field.name
else
args_key = key
end
-- Retrieve value
local value
-- automatic handling only works if the field type is set
if field.type ~= nil then
value = tpl_args[args_key]
local cfield = m_cargo.parse_field{field=field.type}
local handler
if cfield.type == 'Integer' or cfield.type == 'Float' then
handler = tonumber
elseif cfield.type == 'Boolean' then
handler = function (value)
return util.cast.boolean(value, {cast_nil=false})
end
end
if cfield.list and value ~= nil then
-- ingore whitespace between separator and values
value = util.string.split(value, cfield.list .. '%s*')
if handler then
for index, v in ipairs(value) do
value[index] = handler(v)
if value[index] == nil then
error(string.format(i18n.errors.handler_returned_nil, map.table, args_key, v, field.type))
end
end
end
elseif handler and value ~= nil then
value = handler(value)
if value == nil then
error(string.format(i18n.errors.handler_returned_nil, map.table, args_key, tpl_args[args_key], field.type))
end
end
-- Don't need special handling: String, Text, Wikitext, Searchtext
-- Consider: Page, Date, Datetime, Coordinates, File, URL, Email
end
if field.func ~= nil then
value = field.func(tpl_args, frame, value)
end
-- Check defaults
if value == nil and field.default ~= nil then
if type(field.default) == 'function' then
value = field.default(tpl_args, frame)
elseif type(field.default) == 'table' then
mw.logObject(string.format(i18n.errors.table_object_as_default, key, map.table))
value = mw.clone(field.default)
else
value = field.default
end
end
-- Add value to arguments and cargo data
if value ~= nil then
-- key will be used here since the value will be used internally from here on in english
tpl_args[key] = value
if field.field ~= nil then
cargo_values[field.field] = value
end
elseif field.required == true then
error(string.format(i18n.errors.argument_required, args_key))
end
end
-- check for missing keys and return error if any are missing
local missing = {}
for key, _ in pairs(available_fields) do
missing[#missing+1] = key
end
if #missing > 0 then
error(string.format(i18n.errors.missing_key_in_order, map.table, table.concat(missing, '\n')))
end
-- finally store data in DB
if args.rtr ~= nil then
return cargo_values
else
m_cargo.store(frame, cargo_values)
end
end
function util.args.template_to_lua(str)
--[[
Convert templates to lua format. Simplifes debugging and creating
examples.
Parameters
----------
str : string
The entire template wrapped into string. Tip: Use Lua's square
bracket syntax for defining string literals.
Returns
-------
out : table
out.template - Template name.
out.args - arguments in table format.
out.args_to_str - arguments in readable string format.
]]
local out = {}
-- Get the template name:
out.template = string.match(str, '{{(.-)%s*|')
-- Remove everything but the arguments:
str = string.gsub(str, '%s*{{.-|', '')
str = string.gsub(str, '%s*}}%s*', '')
-- Split up the arguments:
out.args = {}
for i, v in ipairs(util.string.split(str, '%s*|%s*')) do
local arg = util.string.split(v, '%s*=%s*')
out.args[arg[1]] = arg[2]
out.args[#out.args+1] = arg[1]
end
-- Concate for easy copy/pasting:
local tbl = {}
for i, v in ipairs(out.args) do
tbl[#tbl+1]= string.format("%s='%s'", v, out.args[v])
end
out.args_to_str = table.concat(tbl, ',\n')
return out
end
-- ----------------------------------------------------------------------------
-- util.html
-- ----------------------------------------------------------------------------
util.html = {}
function util.html.abbr(text, title, options)
-- Outputs html tag <abbr> as string or as mw.html node.
--
-- options
-- class: class attribute
-- output: set to mw.html to return a mw.html node instead of a string
if not title then
return text
end
options = options or {}
local abbr = mw.html.create('abbr')
abbr:attr('title', title)
local class
if type(options) == 'table' and options.class then
class = options.class
else
class = options
end
if type(class) == 'string' then
abbr:attr('class', class)
end
abbr:wikitext(text)
if options.output == mw.html then
return abbr
end
return tostring(abbr)
end
function util.html.error(args)
-- Create an error message box
--
-- Args:
-- msg - message
if args == nil then
args = {}
end
local err = mw.html.create('strong')
err
:addClass('error')
:tag('span')
:addClass('module-error')
:wikitext(i18n.errors.module_error .. (args.msg or ''))
:done()
return tostring(err)
end
function util.html.poe_color(label, text, class)
if text == nil or text == '' then
return nil
end
class = class and (' ' .. class) or ''
return tostring(mw.html.create('em')
:attr('class', 'tc -' .. label .. class)
:wikitext(text))
end
util.html.poe_colour = util.html.poe_color
function util.html.tooltip(abbr, text, class)
return string.format('<span class="hoverbox c-tooltip %s"><span class="hoverbox__activator c-tooltip__activator">%s</span><span class="hoverbox__display c-tooltip__display">%s</span></span>', class or '', abbr or '', text or '')
end
util.html.td = {}
function util.html.td.na(options)
--
-- options:
-- as_tag
-- output: set to mw.html to return a mw.html node instead of a string
options = options or {}
-- N/A table row, requires mw.html.create instance to be passed
local td = mw.html.create('td')
td
:attr('class', 'table-na')
:wikitext(i18n.na)
:done()
if options.as_tag or options.output == mw.html then
return td
end
return tostring(td)
end
function util.html.format_value(tpl_args, frame, value, options)
-- value: table
-- min:
-- max:
-- options: table
-- func: Function to transform the value retrieved from the database
-- fmt: Format string (or function that returns format string) to use for the value. Default: '%s'
-- fmt_range: Format string to use for the value range. Default: '(%s-%s)'
-- color: poe_color code to use for the value range. False for no color. Default: 'mod'
-- class: Additional css class added to color tag
-- inline: Format string to use for the output
-- inline_color: poe_color code to use for the output. False for no color. Default: 'default'
-- inline_class: Additional css class added to inline color tag
-- no_color: (Deprecated; use color=false instead)
-- return_color: (Deprecated; returns both value.out and value without this)
if options.color ~= false and options.no_color == nil then
if options.color then
value.color = options.color
elseif value.base ~= value.min or value.base ~= value.max then
value.color = 'mod'
else
value.color = 'value'
end
end
if options.func then
value.min = options.func(tpl_args, frame, value.min)
value.max = options.func(tpl_args, frame, value.max)
end
options.fmt = options.fmt or '%s'
if type(options.fmt) == 'function' then -- Function that returns the format string
options.fmt = options.fmt(tpl_args, frame, value)
end
if value.min == value.max then -- Static value
value.out = string.format(options.fmt, value.min)
else -- Range value
options.fmt_range = options.fmt_range or i18n.range
value.out = string.format(
string.format(options.fmt_range, options.fmt, options.fmt),
value.min,
value.max
)
end
if value.color then
value.out = util.html.poe_color(value.color, value.out, options.class)
end
if type(options.inline) == 'function' then
options.inline = options.inline(tpl_args, frame, value)
end
if options.inline and options.inline ~= '' then
value.out = string.format(options.inline, value.out)
if options.inline_color ~= false then
options.inline_color = options.inline_color or 'default'
value.out = util.html.poe_color(options.inline_color, value.out, options.inline_class)
end
end
local return_color = options.return_color and value.color or nil
if return_color then
return value.out, return_color
end
return value.out, value
end
-- ----------------------------------------------------------------------------
-- util.misc
-- ----------------------------------------------------------------------------
util.misc = {}
function util.misc.is_frame(frame)
-- the type of the frame is a table containing the functions, so check whether some of these exist
-- should be enough to avoid collisions.
return not(frame == nil or type(frame) ~= 'table' or (frame.argumentPairs == nil and frame.callParserFunction == nil))
end
function util.misc.get_frame(frame)
if util.misc.is_frame(frame) then
return frame
end
return mw.getCurrentFrame()
end
function util.misc.maybe_sandbox(frame)
-- Did {{#invoke:}} call sandbox version?
frame = util.misc.get_frame(frame)
if string.find(frame:getTitle(), 'sandbox', 1, true) then
return true
end
return false
end
function util.misc.add_category(categories, args)
-- categories: table of categories
-- args: table of extra arguments
-- namespace: id of namespace to validate against
-- ingore_blacklist: set to non-nil to ingore the blacklist
-- sub_page_blacklist: blacklist of subpages to use (if empty, use default)
-- namespace_blacklist: blacklist of namespaces to use (if empty, use default)
if type(categories) == 'string' then
categories = {categories}
end
if args == nil then
args = {}
end
local title = mw.title.getCurrentTitle()
local sub_blacklist = args.sub_page_blacklist or cfg.misc.category_blacklist.sub_pages
local ns_blacklist = args.namespace_blacklist or cfg.misc.category_blacklist.namespaces
if args.namespace ~= nil and title.namespace ~= args.namespace then
return ''
end
if args.ingore_blacklist == nil and (sub_blacklist[title.subpageText] or ns_blacklist[title.subjectNsText]) then
return ''
end
local cats = {}
for i, cat in ipairs(categories) do
cats[i] = string.format('[[Category:%s]]', cat)
end
return table.concat(cats)
end
function util.misc.raise_error_or_return(args)
--
-- Arguments:
-- args: table of arguments to this function (must be set)
-- One required:
-- raise_required: Don't raise errors and return html errors instead unless raisae is set in arguments
-- no_raise_required: Don't return html errors and raise errors insetad unless no_raise is set in arguments
--
-- Optional:
-- msg: error message to raise or return, default: nil
-- args: argument directory to validate against (e.x. template args), default: {}
args.args = args.args or {}
args.msg = args.msg or ''
if args.raise_required ~= nil then
if args.args.raise ~= nil then
error(args.msg)
else
return util.html.error{msg=args.msg}
end
elseif args.no_raise_required ~= nil then
if args.args.no_raise ~= nil then
return util.html.error{msg=args.msg}
else
error(args.msg)
end
else
error(i18n.errors.invalid_raise_error_or_return_usage)
end
end
-- ----------------------------------------------------------------------------
-- util.smw
-- ----------------------------------------------------------------------------
util.smw = {}
util.smw.data = {}
util.smw.data.rejected_namespaces = xtable:new({'User'})
function util.smw.safeguard(args)
-- Used for safeguarding data entry so it doesn't get added on user space stuff
--
-- Args:
-- smw_ingore_safeguard - ingore safeguard and return true
if args == nil then
args = {}
end
if args.smw_ingore_safeguard then
return true
end
local namespace = mw.site.namespaces[mw.title.getCurrentTitle().namespace].name
if util.smw.data.rejected_namespaces:contains(namespace) then
return false
end
return true
end
-- ----------------------------------------------------------------------------
-- util.string
-- ----------------------------------------------------------------------------
util.string = {}
function util.string.strip(str, pattern)
pattern = pattern or '%s'
return string.gsub(str, "^" .. pattern .. "*(.-)" .. pattern .. "*$", "%1")
end
function util.string.split(str, pattern)
-- Splits string into a table
--
-- str: string to split
-- pattern: pattern to use for splitting
local out = {}
local i = 1
local split_start, split_end = string.find(str, pattern, i)
while split_start do
out[#out+1] = string.sub(str, i, split_start-1)
i = split_end+1
split_start, split_end = string.find(str, pattern, i)
end
out[#out+1] = string.sub(str, i)
return out
end
function util.string.split_outer(str, pattern, outer)
--[[
Split a string into a table according to the pattern, ignoring
matching patterns inside the outer patterns.
Parameters
----------
str : string
String to split.
pattern : string
Pattern to split on.
outer : table of strings where #outer = 2.
Table with 2 strings that defines the opening and closing patterns
to match, for example parantheses or brackets.
Returns
-------
out : table
table of split strings.
Examples
--------
-- Nesting at the end:
str = 'mods.id, CONCAT(mods.id, mods.name)'
mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'}))
table#1 {
"mods.id",
"CONCAT(mods.id, mods.name)",
}
-- Nesting in the middle:
str = 'mods.id, CONCAT(mods.id, mods.name), mods.required_level'
mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'}))
table#1 {
"mods.id",
"CONCAT(mods.id, mods.name)",
"mods.required_level",
}
]]
local out = {}
local nesting_level = 0
local i = 0
local pttrn = '(.-)' .. '(' .. pattern .. ')'
for v, sep in string.gmatch(str, pttrn) do
if nesting_level == 0 then
-- No nesting is occuring:
out[#out+1] = v
else
-- Nesting is occuring:
out[#out] = (out[math.max(#out, 1)] or '') .. v
end
-- Increase nesting level:
if string.find(v, outer[1]) then -- Multiple matches?
nesting_level = nesting_level + 1
end
if string.find(v, outer[2]) then
nesting_level = nesting_level - 1
end
-- Add back the separator if nesting is occuring:
if nesting_level ~= 0 then
out[#out] = out[#out] .. sep
end
-- Get the last index value:
i = i + #v + #sep
end
-- Complement with the last part of the string:
if nesting_level == 0 then
out[#out+1] = string.sub(str, math.max(i+1, 1))
else
out[#out] = out[#out] .. string.sub(str, math.max(i+1, 1))
-- TODO: Check if nesting level is zero?
end
return out
end
function util.string.split_args(str, args)
-- Splits arguments string into a table
--
-- str: String of arguments to split
-- args: table of extra arguments
-- sep: separator to use (default: ,)
-- kvsep: separator to use for key value pairs (default: =)
local out = {}
if args == nil then
args = {}
end
args.sep = args.sep or ','
args.kvsep = args.kvsep or '='
if str ~= nil then
local row
for _, str in ipairs(util.string.split(str, args.sep)) do
row = util.string.split(str, args.kvsep)
if #row == 1 then
out[#out+1] = row[1]
elseif #row == 2 then
out[row[1]] = row[2]
else
error(string.format(i18n.number_of_arguments_too_large, #row))
end
end
end
return out
end
function util.string.format(str, vars)
--[[
Allow string replacement using named arguments.
TODO:
* Support %d ?
* Support 0.2f ?
Parameters
----------
str : String to replace.
vars : Table of arguments.
Examples
--------
= util.string.format('{foo} is {bar}.', {foo='Dinner', bar='nice'})
References
----------
http://lua-users.org/wiki/StringInterpolation
]]
if not vars then
vars = str
str = vars[1]
end
return (string.gsub(str, "({([^}]+)})",
function(whole, i)
return vars[i] or whole
end))
end
util.string.pattern = {}
function util.string.pattern.valid_var_name()
--[[
Get a pattern for a valid variable name.
]]
return '%A?([%a_]+[%w_]*)[^%w_]?'
end
-- ----------------------------------------------------------------------------
-- util.table
-- ----------------------------------------------------------------------------
util.table = {}
function util.table.length(tbl)
-- Get number of elements in a table. Counts both numerically indexed elements and associative elements. Does not count nil elements.
local count = 0
for _ in pairs(tbl) do
count = count + 1
end
return count
end
util.table.count = util.table.length
function util.table.assoc_to_array(tbl, args)
-- Turn associative array into an array, discarding the values
local out = {}
for key, _ in pairs(tbl) do
out[#out+1] = key
end
return out
end
function util.table.has_all_value(tbl, keys, value)
-- Whether all the table values with the specified keys are the specified value
for _, k in ipairs(keys or {}) do
if tbl[k] ~= value then
return false
end
end
return true
end
function util.table.has_one_value(tbl, keys, value)
-- Whether one of table values with the specified keys is the specified value
for _, k in ipairs(keys or {}) do
if tbl[k] == value then
return true
end
end
return false
end
function util.table.find_in_nested_array(args)
-- Iterates thoguh the given nested array and finds the given value
--
-- ex.
-- data = {
-- {a=5}, {a=6}}
-- find_nested_array{arg=6, tbl=data, key='a'} -> 6
-- find_nested_array(arg=10, tbl=data, key='a'} -> nil
-- -> returns "6"
--
-- args: Table containing:
-- value: value of the argument
-- tbl: table of valid options
-- key: key or table of key of in tbl
-- rtrkey: if key is table, return this key instead of the value instead
-- rtrvalue: default: true
local rtr
if type(args.key) == 'table' then
for _, item in ipairs(args.tbl) do
for _, k in ipairs(args.key) do
if item[k] == args.value then
rtr = item
break
end
end
end
elseif args.key == nil then
for _, item in ipairs(args.tbl) do
if item == args.value then
rtr = item
break
end
end
else
for _, item in ipairs(args.tbl) do
if item[args.key] == args.value then
rtr = item
break
end
end
end
if rtr == nil then
return rtr
end
if args.rtrkey ~= nil then
return rtr[args.rtrkey]
elseif args.rtrvalue or args.rtrvalue == nil then
return args.value
else
return rtr
end
end
-- ----------------------------------------------------------------------------
-- util.Struct
-- ----------------------------------------------------------------------------
util.Struct = function(map)
local this = {map = map}
-- sets a value to a field
function this:set(field, value)
if not field or not value then
error('One or more arguments are nils')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
if _.validate then
_.value = _.validate(value)
else
_.value = value
end
-- this happen if 'validate' returns nil
if _.required == true and _.value == nil then
error(string.format('Field "%s" is required but has been set to nil', field))
end
end
-- adds a new prop to a field
function this:set_prop(field, prop, value)
if not field or not prop or not value then
error('One or more arguments are nils')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
_[prop] = value
end
-- gets a value from a field
function this:get(field)
if not field then
error('Argument field is nil')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
return _.value
end
-- gets a value from a prop field
function this:get_prop(field, prop)
if not field or not prop then
error('One or more arguments are nils')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
return _[prop]
end
-- shows a value from a field
function this:show(field)
if not field then
error('Argument field is nil')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
if _.show then
return _.show(_)
else
return _.value
end
end
return this
end
-- ----------------------------------------------------------------------------
return util