Module:UKB

-------------------------------------------------------

-- This module is copied from the master version in --

-- no:Module:UKB. Do not change it on this wiki, --

-- but propose changes in the master module instead. --

-------------------------------------------------------

-- The module is used by User:UKBot for --

-- organizing editing contests on Wikipedia. --

-- See https://github.com/WikimediaNorge/UKBot/ for --

-- the bot's code, and contribution. --

-------------------------------------------------------

-- Copied from version:

-- https://no.wikipedia.org/w/index.php?title=Modul:UKB&oldid=25078992

require('strict')

local p = {}

local TNT = require('Module:TNT')

local I18NDATASET = 'I18n/UKB.tab'

local getArgs = require('Module:Arguments').getArgs

--- Get a localized message.

-- @param key The message key

-- @param ... Parameters to be passed to the message ($1, $2, etc.)

-- @return localized string

local function msg( key, ... )

return TNT.format( I18NDATASET, key, ... )

end

--- Reverse a mapping to get a list of localized names => canonical names

-- @param mapping A table containing key-value pairs where the key is the canonical name and the value is an array table of aliases

-- @return A table of localized names => canonical names

local function mappingReverser(mapping)

local ret = {}

for canonical, synonyms in pairs(mapping) do

for _, synonym in ipairs(synonyms) do

ret[synonym] = canonical

end

local keyIsPresent, translations = pcall(msg, 'arg-' .. canonical)

if keyIsPresent then

translations = mw.text.split(translations, '|')

for _, translation in ipairs(translations) do

ret[translation] = canonical

end

end

end

return ret

end

--- Get the argument mapping for a type of item

-- @param itemType The mapping subtype to get. Either 'criteria' or 'rules'

-- @param returnType Which mapping to get; 'canonical' or 'translated'

-- @return A table of mappings

local function getArgumentMapping(itemType, returnType)

-- if a new argument is added, it should also be added to the i18n module

-- in c:Data:I18n/UKB.tab

local argumentMapping = {

['criteria'] = {

['backlinks'] = { 'backlink' },

['bytes'] = { 'byte' },

['categories'] = { 'category' },

['forwardlinks'] = { 'forwardlink' },

['new'] = {},

['existing'] = {},

['namespaces'] = { 'namespace' },

['pages'] = { 'page' },

['sparql'] = {},

['stub'] = {}, -- deprecated, not in i18n

['templates'] = { 'template' }

},

['rules'] = {

['bytes'] = { 'byte' },

['bytebonus'] = {},

['categoryremoval'] = {},

['edit'] = {},

['eligiblepage'] = {},

['extlink'] = { 'exlink', 'externallink' },

['image'] = { 'images' },

['listbyte'] = { 'listbytes' },

['newpage'] = {},

['newredirect'] = {},

['reference'] = { 'ref' },

['section'] = {},

['templateremoval'] = {},

['wikidata'] = {},

['word'] = { 'words' },

['wordbonus'] = {}

},

['modifiers'] = {

['aliases'] = {},

['all'] = {},

['description'] = {},

['descriptions'] = {},

['distinct'] = {},

['ignore'] = {},

['initialimagelimit'] = {},

['labels'] = {},

['max'] = {},

['ownimage'] = {},

['properties'] = {},

['query'] = {},

['requirereference'] = { 'require reference', 'require_reference' },

['redirects'] = { 'redirect' },

['site'] = {},

}

}

if returnType == 'canonical' then

return argumentMapping[itemType]

end

local translatedMap = {

['criteria'] = mappingReverser(argumentMapping.criteria),

['rules'] = mappingReverser(argumentMapping.rules),

['modifiers'] = mappingReverser(argumentMapping.modifiers)

}

return translatedMap[itemType]

end

--[ Helper methods ] ------------------------------------------------------------------

--- Make an error string

-- @tparam string text Text to be wrapped in an error class

-- @treturn string The text wrapped in an error class

local function makeErrorString(text)

local html = mw.html.create('strong')

:addClass('error')

:wikitext(text)

return tostring(html)

end

--- Get an error string

-- @tparam string key A message key (from i18n)

-- @tparam string arg An argument to pass along to the message function

-- @treturn string An error message

local function getErrorString(key, arg)

return makeErrorString(msg(key, arg))

end

--- Parse and translate anonymous and named arguments

-- @tparam table frame A frame object

-- @tparam string|nil itemType An item type to return ('criteria', 'rules' or nil)

-- @treturn table A table of anonymous arguments (args)

-- @treturn table A table of named arguments (kwargs)

local function parseArgs(frame, itemType, translate)

local args = {}

local kwargs = {}

local canonicalMap = getArgumentMapping(itemType, 'translated')

if itemType == nil then

canonicalMap = {}

end

local kwargsMap = getArgumentMapping('modifiers', 'translated')

for k, v in pairs(getArgs(frame)) do

v = mw.text.trim(frame:preprocess(v))

if v ~= '' then

if type(k) == 'number' then

if k == 1 and canonicalMap[v] ~= nil and translate then

args[1] = canonicalMap[v]

else

args[k] = v

end

else

if kwargsMap[k] ~= nil and translate then

kwargs[kwargsMap[k]] = v

else

kwargs[k] = v

end

end

end

end

return args, kwargs

end

--- Turn an array table into a string in list form

-- @tparam table items An array of items

-- @tparam string itemType Maybe unnecessary?

-- @tparam string word The strings 'or' or 'and' (representing i18n message keys)

-- @treturn string A string with the table returned as a list

local function listify(items, itemType, word)

word = word or 'or'

if #items == 0 then

return getErrorString('anon-argument-missing', itemType)

end

if #items == 1 then

return items[1]

end

return mw.text.listToText(items, ', ', ' ' .. msg(word) .. ' ' )

end

--- Get link data for a link to a page in a specific namespace

-- @tparam table frame A frame object

-- @tparam string ns A canonical (English) namespace name; 'Template' and 'Category' supported

-- @tparam string page A page name

-- @treturn table A table containing: language code, link target and page name

local function makeNsLink(frame, ns, page)

local linkTarget

local nsNumbers = {

['Template'] = 10,

['Category'] = 14

}

local lang, pageName = mw.ustring.match(page, '^([a-z]+):(.+)$') -- FIXME: Better language code detection

if lang then

-- English namespace name is guaranteed to work, avoids need to maintain

-- lists of namespace names in the module

linkTarget = mw.ustring.format(':%s:%s:%s', lang, ns, pageName)

else

linkTarget = mw.ustring.format(':%s:%s', frame:callParserFunction('ns', nsNumbers[ns]), page)

end

return {

['lang'] = lang,

['linkTarget'] = linkTarget,

['pageName'] = pageName or page

}

end

--- Make a link to a single template, wrapped in curly brace syntax

-- @tparam table frame A frame object

-- @tparam template Name of a template (optionally with an interlanguage prefix)

-- @treturn string An HTML string linking to the template in question

local function makeTemplateLink(frame, template)

local nsLink = makeNsLink(frame, 'Template', template)

local wikitext = mw.text.nowiki('{{') .. mw.ustring.format('%s', nsLink['linkTarget'], nsLink['pageName']) .. mw.text.nowiki('}}')

local html = mw.html.create('span')

:addClass('template-link')

:css('font-family', 'monospace,monospace')

:wikitext(wikitext)

return tostring(html)

end

--- Make a link to a single category

-- @tparam table frame A frame object

-- @tparam category Name of a category (optionally with an interlanguage prefix)

-- @treturn string An HTML string linking to the category in question

local function makeCategoryLink(frame, category)

local nsLink = makeNsLink(frame, 'Category', category)

return mw.ustring.format('%s', nsLink['linkTarget'], nsLink['pageName'])

end

--- Make a list of templates

-- @tparam table frame A frame object

-- @tparam table args An array of template names (optionally with interlanguage prefixes)

-- @treturn table A table of template links

local function makeTemplateList(frame, args)

local templates = {}

for i, v in ipairs(args) do

table.insert(templates, makeTemplateLink(frame, v))

end

setmetatable(templates, {

__tostring = function(self)

return listify(templates, 'templates')

end

})

return templates

end

--- Make a list of categories

-- @tparam table frame A frame object

-- @tparam table args An array of category names (optionally with interlanguage prefixes)

-- @treturn table A table of category links

local function makeCategoryList(frame, args)

local categories = {}

for i, v in ipairs(args) do

v = mw.text.trim(v)

if v ~= '' then

table.insert(categories, makeCategoryLink(frame, v))

end

end

setmetatable(categories, {

__tostring = function(self)

return listify(categories, 'categories')

end

})

return categories

end

--- Make a list of templates

-- @tparam table args An array of page names (optionally with interlanguage prefixes)

-- @treturn table A table of page links

local function makePageList(args)

local pages = {}

for i, v in ipairs(args) do

v = mw.text.trim(v)

if v ~= '' then

local lang, page = string.match(v, '^([a-z]+):(.+)$')

if lang then

table.insert(pages, string.format('%s', lang, page, page))

else

table.insert(pages, string.format(':%s', v))

end

end

end

setmetatable(pages, {

__tostring = function(self)

return listify(pages, 'pages')

end

})

return pages

end

--- Make a list of namespaces

-- @tparam table args An array of namespace IDs

-- @treturn table A table of namespace names

local function makeNsList(args)

local namespaces = {}

local namespaceName = msg('article')

for _, namespaceId in ipairs(args) do

namespaceId = mw.text.trim(namespaceId)

if namespaceId ~= '' then

if namespaceId ~= "0" then

namespaceName = '{{lc:{{ns:' .. namespaceId .. '}}}}'

end

table.insert(namespaces, namespaceName)

end

end

setmetatable(namespaces, {

__tostring = function(self)

return listify(namespaces, 'namespaces')

end

})

return namespaces

end

--[ Criterion format methods ]-------------------------------------------------------------

local criterion = {}

--- Formatter function for the backlinks criterion

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the criterion

function criterion.backlinks(args, kwargs, frame)

local pageList = makePageList(args)

return msg('criterion-backlinks', #pageList, tostring(pageList))

end

--- Formatter function for the bytes criterion

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the criterion

function criterion.bytes(args, kwargs, frame)

return msg('criterion-bytes', args[1])

end

--- Formatter function for the categories criterion

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the criterion

function criterion.categories(args, kwargs, frame)

local categoryList = makeCategoryList(frame, args)

local ret = msg('criterion-categories', #categoryList, tostring(categoryList))

if kwargs.ignore ~= nil then

local ignoredCats = mw.text.split(kwargs.ignore, ',')

ignoredCats = makeCategoryList(frame, ignoredCats)

ret = ret .. msg('categories-except', #ignoredCats, tostring(ignoredCats))

end

return ret

end

--- Formatter function for the existing criterion

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the criterion

function criterion.existing(args, kwargs, frame)

return msg('criterion-existing')

end

--- Formatter function for the forwardlinks criterion

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the criterion

function criterion.forwardlinks(args, kwargs, frame)

local pages = makePageList(args)

return msg('criterion-forwardlinks', #pages, tostring(pages))

end

--- Formatter function for the namespaces criterion

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the criterion

function criterion.namespaces(args, kwargs, frame)

local nsList = makeNsList(args)

local message

if #nsList == 1 and args[1] == '0' then

message = msg('criterion-namespace-0')

else

message = msg('criterion-namespace', #nsList, tostring(nsList))

end

if kwargs.site ~= nil then

return msg('page-at-site', message, mw.ustring.format('[https://%s %s]', kwargs.site, kwargs.site))

end

return message

end

--- Formatter function for the new page criterion

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the criterion

function criterion.new(args, kwargs, frame)

if kwargs.redirects ~= nil then

return msg('criterion-new-with-redirects')

end

return msg('criterion-new')

end

--- Formatter function for the pages (page list) criterion

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the criterion

function criterion.pages(args, kwargs, frame)

local pages = makePageList(args)

return msg('criterion-pages', #pages, tostring(pages))

end

--- Formatter function for the SPARQL criterion

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the criterion

function criterion.sparql(args, kwargs, frame)

local query = ''

if kwargs.distinct ~= nil then

query = 'SELECT DISTINCT ?item WHERE {\n ' .. kwargs.query .. '\n}'

else

query = 'SELECT ?item WHERE {\n ' .. kwargs.query .. '\n}'

end

local url = 'http://query.wikidata.org/#' .. mw.uri.encode(query, 'PATH')

if kwargs.description ~= nil then

return msg('criterion-sparql-with-explanation', kwargs.description, url)

end

return msg('criterion-sparql', url)

end

--- Formatter function for the templates criterion

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the criterion

function criterion.templates(args, kwargs, frame)

local templates = makeTemplateList(frame, args)

return msg('criterion-templates', #templates, tostring(templates))

end

--- Main function for getting criterion messages

-- @tparam table frame A frame object

-- @treturn string A string representing the criterion (or an error message string)

function p.criterion(frame)

local args, kwargs = parseArgs(frame, 'criteria', true)

local criterionArg = table.remove(args, 1)

local permittedCriteria = getArgumentMapping('criteria', 'canonical')

if criterionArg == nil or criterionArg == '' then

return frame:preprocess(getErrorString('argument-missing', 'criterion'))

elseif permittedCriteria[criterionArg] == nil or criterion[criterionArg] == nil then

return frame:preprocess(getErrorString('invalid-criterion', criterionArg))

end

-- Use manual description if given

if kwargs.description ~= nil and criterionArg ~= 'sparql' then

return kwargs.description

end

return frame:preprocess(criterion[criterionArg](args, kwargs, frame))

end

--[ Rule format methods ]-------------------------------------------------------------

local rule = {}

--- Formatter function for custom rules

-- @tparam number points A number of points; may by a float

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the rule

function rule.custom(points, args, kwargs, frame)

return msg('rule-custom', points, kwargs.description)

end

--- Formatter function for image rules

-- @tparam number points A number of points; may by a float

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the rule

function rule.image(points, args, kwargs, frame)

local out

local tplargs = {

['points'] = points,

}

if kwargs.initialimagelimit ~= nil then

out = msg('rule-image-limited', points, kwargs.initialimagelimit)

else

out = msg('rule-image', points)

end

if kwargs.ownimage ~= nil then

out = out .. ' ' .. msg('rule-image-own', kwargs.ownimage)

end

return out

end

--- Formatter function for Wikidata rules

-- @tparam number points A number of points; may by a float

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the rule

function rule.wikidata(points, args, kwargs, frame)

local out

local params

local argTypes = { msg('properties'), msg('labels'), msg('aliases'), msg('descriptions') }

local results = {}

if kwargs.properties == nil and kwargs.labels == nil and kwargs.aliases == nil and kwargs.descriptions == nil then

return getErrorString('argument-missing', listify(argTypes))

end

if kwargs.properties ~= nil then

params = mw.text.split(kwargs.properties, ',')

for k, v in pairs(params) do

params[k] = string.format('%s', v, v)

end

table.insert(results, listify(params))

end

if kwargs.labels ~= nil then

params = mw.text.split(kwargs.labels, ',')

table.insert(results, msg('label') .. ' (' .. listify(params) .. ')')

end

if kwargs.aliases ~= nil then

params = mw.text.split(kwargs.aliases, ',')

table.insert(results, msg('alias') .. ' (' .. listify(params) .. ')')

end

if kwargs.descriptions ~= nil then

params = mw.text.split(kwargs.descriptions, ',')

table.insert(results, msg('description') .. ' (' .. listify(params) .. ')')

end

results = table.concat( results, ' ' .. msg('and') .. ' ' )

if kwargs.all ~= nil then

out = msg('rule-wikidata-all', points, results)

else

out = msg('rule-wikidata-first', points, results)

end

if kwargs.requireReference ~= nil then

out = out .. ' ' .. msg('rule-wikidata-require-reference')

end

return out

end

--- Formatter function for reference rules

-- @tparam number points A number of points; may by a float

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the rule

function rule.reference(points, args, kwargs, frame)

return msg('rule-reference', points, args[1])

end

--- Formatter function for template removal rules

-- @tparam number points A number of points; may by a float

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the rule

function rule.templateremoval(points, args, kwargs, frame)

local templateList = makeTemplateList(frame, args)

return msg('rule-templateremoval', points, #templateList, tostring(templateList))

end

--- Formatter function for category removal rules

-- @tparam number points A number of points; may by a float

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the rule

function rule.categoryremoval(points, args, kwargs, frame)

local categoryList = makeCategoryList(args)

return msg('rule-categoryremoval', points, #categoryList, tostring(categoryList))

end

--- Formatter function for section adding rules

-- @tparam number points A number of points; may by a float

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the rule

function rule.section(points, args, kwargs, frame)

if kwargs.description ~= nil then

return msg('rule-section-desc', points, kwargs.description)

end

return msg('rule-section', points, #args, listify(args))

end

--- Formatter function for byte bonus rules

-- @tparam number points A number of points; may by a float

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the rule

function rule.bytebonus(points, args, kwargs, frame)

return msg('rule-bytebonus', points, args[1])

end

--- Formatter function for word bonus rules

-- @tparam number points A number of points; may by a float

-- @tparam table args Anonymous arguments to the module

-- @tparam table kwargs Keyword arguments to the module

-- @treturn string A message corresponding to the rule

function rule.wordbonus(points, args, kwargs, frame)

return msg('rule-wordbonus', points, args[1])

end

--- Main function for getting criterion messages

-- @tparam table frame A frame object

-- @treturn string A string representing the rule (or an error message string)

function p.rule(frame)

local args, kwargs = parseArgs(frame, 'rules', true)

local ruleArg = table.remove(args, 1)

local points = table.remove(args, 1)

local permittedRules = getArgumentMapping('rules', 'canonical')

if ruleArg == nil or ruleArg == '' then

return frame:preprocess(getErrorString('argument-missing', 'rule'))

elseif permittedRules[ruleArg] == nil then

return frame:preprocess(getErrorString('invalid-rule', ruleArg))

end

if kwargs.description ~= nil then

ruleArg = 'custom'

end

-- All rules requires argument 1: number of points awarded

if points == nil then

return frame:preprocess(getErrorString('argument-missing', '1 (number of points)'))

end

points = mw.language.getContentLanguage():formatNum(tonumber(points))

-- If there's a rule formatter function, use it.

-- Otherwise, use the string from the messages table.

local out

if rule[ruleArg] ~= nil then

out = rule[ruleArg](points, args, kwargs, frame)

else

-- It shouldn't be necessary to check if the message exists here, because

-- of the previous check against permittedRules above

out = msg('rule-' .. ruleArg, points)

end

if kwargs.site ~= nil then

out = msg('rule-site', out, mw.ustring.format('[https://%s %s]', kwargs.site, kwargs.site))

end

if kwargs.max ~= nil then

out = msg('base-rule-max', out, mw.language.getContentLanguage():formatNum(tonumber(kwargs.max)))

end

return frame:preprocess(out)

end

--- Function to generate documentation for a module or template using this module

-- Not implemented yet

function p.generateDocs(frame)

-- Generate documentation subpage for templates using the module

end

--- Function to get warnings about duplicate or invalid i18n values

-- Not implemented yet

function p.getI18nWarnings(frame)

-- Function to be used on /doc page, to report any duplicate arguments

-- from the i18n, and potentially other things that should be fixed in the

-- i18n for the current language.

end

--- Get a single message string from the module's i18n, localized into the page

--- if possible

-- @tparam table frame A frame object

-- @treturn string A formatted message (or an HTML error string if the key doesn't exist)

function p.getMessage(frame)

local args, kwargs = parseArgs(frame, nil, false)

local key = table.remove(args, 1)

local exists, message = pcall(msg, key, args)

if exists then

if mw.isSubsting() then

-- substitute magic words etc. if the module proper is being substed

message = mw.ustring.gsub( message, '{{(#?%a+):', '{{subst:%1:' )

end

return frame:preprocess(message)

else

return getErrorString('message-key-missing', key)

end

end

--- Function to get i18n data for use by the bot

-- @treturn string A JSON-encoded string of all keys and (localized) values from the i18n dataset

function p.getAllI18n()

local lang = mw.title.getCurrentTitle().pageLang:getCode()

local sensible = {}

local i18n = mw.ext.data.get(I18NDATASET, lang)['data']

for _,v in ipairs(i18n) do

-- turn the array of message objects into a sensible key->value mapping

sensible[v[1]] = v[2]

end

return mw.text.jsonEncode(sensible)

end

return p