Jump to content

Module:UKB

From Wikipedia, the free encyclopedia
This is the current revision of this page, as edited by Jon Harald Søby (WMNO) (talk | contribs) at 21:19, 10 April 2025 (new version). The present address (URL) is a permanent link to this version.
(diff) ← Previous revision | Latest revision (diff) | Newer revision → (diff)

-------------------------------------------------------
-- 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|%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|%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:%s|%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('[[:d:Property:%s|%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