Module:Countries/sandbox
Jump to navigation
Jump to search
CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules
Lua
Documentation for this module may be created at Module:Countries/sandbox/doc
Code
--[==[
This module implements templates that output a countries navbox.
First usage was at [[Template:Countries of Europe]].
--]==]
-- Locally used by langSwitch().
local _langSwitch -- Cache for loading 'Module:Fallback' lazily.
-- Locally used by getList():getCodes() and _main().
local function langSwitch(translations, lang)
if translations[lang] then
return translations[lang]
end
--[==[ Note: the sandbox version handles BCP 47 rules more strictly for
fallbacks. In addition it is a bit faster, uses less temporary memory than
the current non-sandbox version of Module:Fallback.
--]==]
_langSwitch = _langSwitch or require('Module:Fallback/sandbox')._langSwitch
return _langSwitch(translations, lang) or ''
end
-- Locally used by getList():getTitle() and getGroupData().
--[==[
If text is a non-empty string (not just containing spaces), return its trimmed
content. Otherwise, return nil (text is an empty string or is not a string).
--]==]
local function stripToNil(text)
if type(text) == 'string' then
text = text:match('^%s*(.-)%s*$') -- trim starting and trailing spaces
if text ~= '' then -- not nil and not empty
return text:gsub('%s+', ' ') -- compress and normalize inner spaces
end
end
return nil
end
-- Locally used by getTargetFromCatRedirect().
local _getTargetFromCatRedirect -- Cache for loading 'Module:Redirect' lazily.
--[==[
Detect soft redirect in wiki page content of 'Category:' pages using template
{{Category redirect|target}} or one of its known aliases on Commons.
Loaded lazily from 'Module:Redirect'.
--]==]
local function getTargetFromCatRedirect(...)
if not _getTargetFromCatRedirect then
_getTargetFromCatRedirect =
require('Module:Redirect').getTargetFromCatRedirect
end
getTargetFromCatRedirect = _getTargetFromCatRedirect
return getTargetFromCatRedirect(...)
end
-- Locally used by getList().
local _makeSortKey -- Cache for loading 'Module:MakeSortKey' lazily.
-- Locally used by _main().
local function getList(lists, exclude, options, infos)
--[==[ Return:
* title of redirect target (prefixed by ':' if it's a category), if the
specified page is a redirect and target exists;
* false if specified page is not a suitable target, try next page if any;
* nil if specified page should be used as the target
--]==]
local function getRedirectTarget(titleObj)
if titleObj.isRedirect then
-- target is false if target page with that title does not exist
local target = titleObj.redirectTarget.prefixedText
return type(target) == 'string'
and target:match('^%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:')
and (':' .. target) or target
end
local content = titleObj:getContent()
if content then
local target = getTargetFromCatRedirect(content)
if target then
return ':' .. target
end
if content:match('{{%s*[Dd]isambig%s*}}')
or content:match('{{%s*[Dd]ab%s*}}') then
return false
end
end
return nil
end
--[==[
Avoid overhead of checking target for a country with no alternative name.
LATER: It appears the extra overhead may be low; perhaps remove this?
--]==]
local function getNilTarget(titleObj)
return nil
end
-- Locally used by getTitle() and makeItem() and set as info = codes[code].
local info
--[==[
Return the title of a link to an existing page (if not 'all'),
or nil if there's none.
Uses (info, options) from getList().
--]==]
local function getTitle(tryThe)
local pfx = (stripToNil(options.prefix) or '') .. options.presep
--[==[ Enforce a single leading ':' for links to special namespaces
(namespace names are not case-significant).
--]==]
local ns, name = pfx:match('^:*%s-(%w+)%s*:%s*(.-)$')
if ns then
--[==[ TODO: should we recognize interwiki prefixes here
(e.g. "fr:" or "de:" which are also special namespaces)
to enforce an inline link to the other wiki, instead of generating
an interwiki on the side bar?
This is for not needed on international wikis like Commons, but may
be needed on Wikipedia.
--]==]
for _, special in ipairs({'Category', 'File', 'Special'}) do
if special:lower() == ns:lower() then
pfx = ':' .. special .. ':' .. name
end
end
end
local sfx = stripToNil(options.suffix) or ''
--[==[ Replace final parts found in (option.suffix), according to the
optional (subst) table defined in the (info) taken from the entry
defined for the country code in (lists)
--]==]
sfx = options.sufsep .. (type(info.subst) == 'table' and
sfx:gsub('%S.+$', info.subst) or sfx)
local getTarget = #info > 1 and getRedirectTarget or getNilTarget
for i, name in ipairs(info) do
if tryThe then
if i > 1 then
return nil
end
name = 'the ' .. name
end
--[==[ Compress whitespaces in excess possibly introduced by
(option.presep, option.sufsep) in (pfx) or (sfx), or left after
applying (info.subst) to the (option.suffix)
--]==]
local title = stripToNil(pfx .. name .. sfx) or ''
if options.all then
return title
end
local titleObj = mw.title.new(title)
if titleObj and titleObj.exists then
local t = getTarget(titleObj)
if t ~= false then
return t or title
end
end
end
return nil
end
local itemPattern = '[[{title}|<bdi>{label}</bdi>]]{post}{bullet}'
local head, trail = '<span style="white-space:nowrap">', '</span>'
--[==[ These bullets are used by makeItem(), but also after processing
all items to remove the last bullet==stdBullet.
--]==]
local altBullet, stdBullet = ' ≈', " '''·'''"
--[==[ trail is used here, but also after processing all items to remove
the last bullet==stdBullet.
--]==]
itemPattern = head .. itemPattern .. trail
local post
-- Locally used recursively.
local function makeItem(tryThe)
local pretitle = ''
local itemLabel
local bullet = stdBullet
local second = ''
if tryThe then
--[==[ LATER:
This assumes 'the' applies to the first name, and only the first.
--]==]
if not info.the then
return nil
end
if options.all then
pretitle = 'the '
itemLabel, second = makeItem()
bullet = altBullet
second = ' ' .. second
end
end
local title = getTitle(tryThe)
if not title then
return nil
end
itemLabel =
not options.all and mw.wikibase.getLabelByLang(info.qid, options.lang) or
(pretitle .. info[1])
return itemLabel,
(itemPattern:gsub('{(%a+)}', {
title = title,
label = itemLabel,
post = post,
bullet = bullet,
})
) .. second
end
--[==[ Language selection.
The following works with a list that defines results for various languages.
It also handles the special entries illustrated in the following.
If the provided list does not contain an entry for the provided lang,
langSwitch() will look for fallbacks defined in that list: assume 'LX' and
'LY' are language codes that are not defined in the list, and 'LX' falls
back to 'en', while 'LY' falls back to 'default'. Currently, the latter
cannot occur, but it conceivably could.
list = {
automatic = 'AB CD EF GH', -- country codes
english = 'automatic', -- uselang=en → 'AB CD EF GH' without sort
default = 'automatic sorted', -- uselang=LY → 'AB CD EF GH' after sorting
en = 'automatic sorted', -- uselang=LX → 'AB CD EF GH' after sorting
},
* We'll use codes = the space-separated codes which are the most
appropriate order for lang.
* We'll use getSortKey = nil or a function to make a sort key, if the
entry was 'automatic sorted'.
* The two entries 'automatic' and 'english' override what langSwitch alone
would return. But a specific language may be set to use.
As an optimization, lang=='en' uses the 'english' entry setting, if defined:
* when english=='automatic', the result is the automatic setting, which
may or may not use the overhead of sorting.
Sorting applies to country names obtained from Wikidata in the user's
language:
* The automatic sorting is crude (language neutral) and will often be
unhelpful for specific languages (notably for the ideographic script
which remains sorted in binary order and still requires manual sorting).
* It is based on sort keys computed in Module:MakeSortKey.
--]==]
local codes
if options.lang == 'en' and lists.english then
codes = lists.english
else
codes = langSwitch(lists, options.lang)
end
local getSortKey
if codes == 'automatic' or codes == 'automatic sorted' then
if codes == 'automatic sorted' then
if not _makeSortKey then
_makeSortKey = require('Module:MakeSortKey').makeSortKey
end
getSortKey = _makeSortKey
end
codes = lists.automatic or error('Codes list uses "'
.. codes .. '" but "automatic" is not defined')
end
--[==[
Split the space-separated list of codes, find and process their info to
build the unsorted items to display. Items will be actually sorted, if
wanted, once they are complete.
--]==]
local items, wantSort = {}, getSortKey and not options.all
for code in codes:gmatch('%S+') do
if not exclude[code] then
info = infos[code]
if info then
post =
(options.showcode and
(' <bdi>[<kbd>' .. code .. '</kbd>]</bdi>')
or '') ..
(type(info.mark) == 'string' and
('<sup><bdi>' .. info.mark .. '</bdi></sup>')
or '') ..
(type(info.note) == 'string' and
(' <bdi>' .. info.note .. '</bdi>')
or '')
post = post ~= '' and '<small style="font-size:88%">'
.. post .. '</small>' or ''
local itemLabel, result = makeItem(true)
if not result then
itemLabel, result = makeItem()
end
if result then
table.insert(items,
wantSort and {
getSortKey(itemLabel, options.lang),
result
} or result)
end
else
options.message = options.message or ('No info about code "'
.. code .. '"')
end
end
end
--[==[
Sort the items, if wanted, using the sort key (precomputed above)
with which they were associated.
--]==]
if wantSort then
table.sort(items, function (a, b) return a[1] < b[1] end)
for i, v in ipairs(items) do
items[i] = v[2]
end
end
--[==[
Pack the items into a single string, and remove the last
bullet==stdBullet just before trail.
--]==]
local result = table.concat(items, ' ')
local stdBulletTrail = stdBullet .. trail
local stdBulletTrailLen = #stdBulletTrail
if result:sub(-stdBulletTrailLen) == stdBulletTrail then
-- Omit trailing bullet from last item
result = result:sub(1, -stdBulletTrailLen - 1) .. trail
end
return result
end
-- Locally used by _main() and main().
local function isnonempty(text)
return text and text ~= ''
end
-- Locally used by main().
--[==[
Exported by this module, for usage in Lua.
--]==]
local function _main(options, data)
-- Caller must provide a valid language code in options.lang.
local lang = options.lang
local langObj = mw.language.new(lang)
--[==[
Generate the table of variable names supported in placeholders
(inside patterns or subpatterns), or in simple conditions.
--]==]
local var = {
-- These supported variables are independant of the sections.
lang = lang,
dir = langObj:getDir(), -- value = 'ltr' or 'rtl'
colon = options.colon or ': ',
}
local wantSimple = options.simple and data.simple
local exceptions = data.simple or {}
-- Each section adds two variables for its title and its list of items.
local sections = exceptions.sections or {}
for section, titles in pairs(data.titles) do
-- Add support for the variable named like '{section}title'.
if not wantSimple or sections[section] then
var[section .. 'title'] = '<bdi>' .. langSwitch(titles, lang)
.. '</bdi>'
end
end
for section, lists in pairs(data.lists) do
-- Add support for the variable named like '{section}list'.
if not wantSimple or sections[section] then
local exclude = wantSimple and {} or sections[section] or {}
var[section .. 'list']
= getList(lists, exclude, options, data.infos)
end
end
--[==[
The pattern may contain '{variablename}' placeholders to be replaced by
the value of `var['variablename']`. See above for supported variables.
--]==]
local pattern = wantSimple and exceptions.pattern or data.pattern
local usedpattern
if type(pattern) == 'string' then
-- This is an unconditional pattern, represented as a simple string
usedpattern = pattern
elseif type(pattern) == 'table' then
--[==[ pattern is an array containing an ordered array of conditional
patterns, added in the same order to the result (non-integer keys in
this table are ignored).
--]==]
usedpattern = ''
-- Note: keys not in the numbered sequence are ignored.
for _, condpattern in ipairs(pattern) do
--[=[
Each conditional pattern is represented either:
- as a simple string when there's no condition (this unconditional
pattern will be always added to the result), or
- as an ordered table, whose first element is a subpattern string
and the other elements represent an union of several
(non-exclusive) conditions.
(If any one of the conditions evaluates to true (OR), the conditional
subpattern will be used.)
Each condition may itself be represented either:
- as a simple string for simple conditions, or
- as an ordered table of subconditions, i.e. a conjunction of several
(non-exclusive) simple conditions.
(If any one of the subconditions evaluates to false (AND), the
conditional subpattern will NOT be used. Simple conditions are
represented as strings, used to evaluate tests based on names of
variables (usable in placeholders of patterns or subpatterns.)
The variable names used in simple conditions don't need to be
present within the pattern or subpattern strings.
A simple condition can currently take one the following forms:
- 'variablename': the simple condition is true if the variable
with that name is non-empty;
- '!variablename': the simple condition is true if the variable
with that name is empty.
--]=]
local subpattern, condition
if type(condpattern) == 'string' then -- Unconditional subpattern.
subpattern, condition = condpattern, true
elseif type(condpattern) == 'table' then -- Conditional subpattern.
subpattern, condition = '', false
-- Note: keys not in the numbered sequence are ignored.
for i, v in ipairs(condpattern) do
--[==[ The first element is the subpattern, other numbered
elements are its conditions (forming an union).
--]==]
if i == 1 then
subpattern = v
-- Handle conditions that are simple strings.
elseif type(v) == 'string' then
--[==[ Evaluate the condition string which is for now a
simple variable name:
* the condition '!variablename' is true if this variable
has an empty string value;
* the condition 'variablename' is true if this variable
has a non-empty string value;
* the supported variable names are defined above (e.g.
{section}..'title' and {section}..'list').
--]==]
if v:sub(1, 1) == '!' and not isnonempty(var[v:sub(2)])
or v:sub(1, 1) ~= '!' and isnonempty(var[v]) then
condition = true -- Subpattern EFFECTIVELY used.
break -- Don't need to evaluate other conditions.
--else
--[==[
Other string values of v are ignored. This
condition evaluates as false, other possible
conditions must be evaluated.
--]==]
end
--[==[ Handle conditions that are arrays of subconditions
(AND). If any subcondition evaluates to false, the
condition also evaluates to false, and other conditions
must be checked to evaluate them as OR).
--]==]
elseif type(v) == 'table' then
--[==[ The subpattern will then be used unless a
subcondition evaluates to false.
--]==]
condition = true
-- Note: keys not in the numbered sequence are ignored.
for _, w in ipairs(v) do
-- Handle subconditions that are simple strings.
if type(w) == 'string' then
--[==[ Evaluate the subcondition string which
is for now a simple variable name (like above).
--]==]
if w:sub(1, 1) == '!'
and isnonempty(var[w:sub(2)])
or w:sub(1, 1) ~= '!'
and not isnonempty(var[w])
then
condition = false
--[==[ This subcondition is evaluated as
false. The subpattern may still be used,
but need to evaluate other conditions.
]==]
break
--else
--[==[
Other string values of v are ignored. This
subcondition evaluates as false, other
possible subconditions must be evaluated.
--]==]
end
else
--[==[ Don't know what to do with this type of
subcondition. And because this is part of a
conjonction (AND), the subcondition evaluates
as false. The subpattern may still be used, but
we need to evaluate other conditions.
--]==]
condition = false
break
end
end
if condition then
--[==[ If the array of subconditions (AND) is still
true, the main condition (OR) instantly evaluates
as true. The subpattern will EFFECTIVELY be used.
Don't need to evaluate other conditions.
--]==]
break
end
--else
--[==[ Don't know what to do with this type of
condition. And because this is part of an union (OR),
it is ignored, but we can evaluate other conditions.
--]==]
end
end
end
if condition then
usedpattern = usedpattern .. subpattern
end
end
--else
--[==[ Don't know what to do with this type of pattern (not used in
the final pattern)
--]==]
end
return usedpattern:gsub('{(%a+)}', var)
end
-- Locally used by getGroupData() and show().
--[==[
If there's no ':' in the id, it is assumed to be in a subpage of
Module:Countries; otherwise it can be the full page name of any other module
stored anywhere. Also autodetect the '/sandbox' version according to the
parent page name.
--]==]
local function getDataModuleName(frame, id)
return (id:find(':', 1, true) and id or 'Module:Countries/' .. id) ..
(frame and frame:getTitle():find('sandbox', 1, true)
and '/sandbox' or '')
end
-- Locally used by main().
local function getGroupData(frame)
--[==[ Return table of data defining a group of items for the first
template parameter. The data will rarely be used more than once on a
page, so mw.loadData is not useful.
--]==]
local data
-- Id of the group (case sensitive), e.g. 'Europe' or 'Module:CustomData'.
local id = stripToNil(frame.args[1])
if id then
local module, status = getDataModuleName(frame, id), nil
status, data = pcall(require, module)
if not status then
error('Data could not be loaded from [[:' .. module ..']]')
end
end
if type(data) ~= 'table' then
error('First template parameter must specify a defined data module')
end
-- Compatiblity note: `.infos` is preferred, `.countries` is legacy.
-- We do not allow to define both (would be ambiguous).
if data.infos == nil and data.countries ~= nil then
data.infos = data.countries
data.countries = nil
end
if type(data.titles) ~= 'table' or
type(data.lists) ~= 'table' or
type(data.infos) ~= 'table' or type(data.countries) ~= 'nil' or
type(data.pattern) ~= 'string' and type(data.pattern) ~= 'table' or
type(data.simple) ~= 'nil' and type(data.simple) ~= 'table' or
type(data.simple) == 'table' and (
type(data.simple.pattern) ~= 'string'
and type(data.simple.pattern) ~= 'table' or
type(data.simple.sections) ~= 'table'
) then
error('The specified data module ('
.. id .. ') is not validly defined')
end
if frame.args.mode == 'fop' then
--[==[
For Module:TNTExpandByCountries which generates
"Commons:Freedom of panorama" pages:
* Use any given fop exception name. For example, the exception
fop = 'Luxembourg' means that [[Luxembourg|...]] should be the
returned link so TNTExpandByCountries uses Template:Luxembourg
which correctly contains {{Label|Q32|...}}.
* Do not follow redirects. That occurs if only one name for a country
code is given.
--]==]
for code, spec in pairs(data.infos) do
if spec.fop then
if spec.fop == 'EXCLUDE' then
data.infos[code] = nil
else
data.infos[code] = {
spec.fop, qid = spec.qid
-- other fields in `spec` are not needed.
}
end
end
end
end
return data
end
--[==[
Exported by this module, for usage in wiki templates.
--]==]
local function main(frame)
local args = frame.args
local options = {
--[==[
Use default args set by "{{#invoke:}}" used in any page or in the
main template.
--]==]
prefix = args.prefix or '',
presep = args.presep or args.sep or ' ',
sufsep = args.sufsep or args.sep or ' ',
suffix = args.suffix or '',
simple = args.simple,
showcode = args.showcode,
all = args.all,
nocat = args.nocat,
lang = args.lang or frame:callParserFunction('Int', 'Lang'),
}
local lang = options.lang
--[==[
Override args with those passed to the main template (and check them
verbosely).
--]==]
args = frame:getParent().args
if args then
options.prefix = args.prefix or options.prefix
options.presep = args.presep or args.sep or options.presep
options.sufsep = args.sufsep or args.sep or options.presep
options.suffix = args.suffix or options.suffix
options.simple = args.simple or options.simple
options.showcode = args.showcode or options.showcode
options.all = args.all or options.all
options.nocat = args.nocat or options.nocat
local goodArgs, badArgs = {
prefix = true,
presep = true,
sufsep = true,
suffix = true,
sep = true,
simple = true,
showcode = true,
all = true,
nocat = true,
}, {}
for k, v in pairs(args) do
if not goodArgs[k] then
table.insert(badArgs, k .. '=' .. v)
end
end
if #badArgs > 0 then
options.message = 'invalid parameter "|'
.. mw.text.nowiki(table.concat(badArgs, '|')) .. '"'
end
end
options.colon = frame:expandTemplate({
title = 'colon',
args = {
lang = lang,
},
})
options.simple = isnonempty(options.simple)
options.showcode = isnonempty(options.showcode)
options.all = isnonempty(options.all)
options.nocat = isnonempty(options.nocat)
local result = _main(options, getGroupData(frame))
if options.message then
--[==[
Check if a warning should be displayed for invalid input.
--]==]
local success, revid = pcall(function ()
return frame:preprocess('{{REVISIONID}}')
end)
if success and revid == '' then
result = result .. '<strong class="error">Error: '
.. options.message .. '</strong>'
end
if not options.nocat then
result = result
.. '[[Category:Countries template with invalid parameters]]'
end
end
return result
end
--[==[
Exported by this module, for usage by {{#invoke:thisModule|show}} in wiki.
For documentation, return wikitext listing data from the country modules.
See [[Module talk:Countries/show]] for results.
--]==]
local function show(frame)
local templateids = {
--[==[
Format for each entry: { '<Template name>', '<Data module id>' },
Note: without any embedded ':', the '<Data module id>' indicates
loading 'Module:Countries/<Data module id>' by default. Data modules
do not necessarily have to be subpages of 'Module:Countries', but must
then specify their full page name (starting by 'Module:').
Templates and data modules don't need to be listed all, they are only
here to be used on the report shown on [[Module talk:Countries/show]],
which performs extensive check of all features of this module.
--]==]
{ 'Countries of Africa', 'Africa' },
{ 'Countries of the Americas', 'Americas' },
{ 'Countries of the Arab world', 'Arab world' },
{ 'Countries of Asia', 'Asia' },
{ 'Countries of the Caribbean', 'Caribbean' },
{ 'Countries of Central America', 'Central America' },
{ 'Countries of Europe', 'Europe' },
{ 'Countries of the European Union', 'European Union' },
{ 'Countries of North America', 'North America' },
{ 'Countries of North America (subcontinent)', 'North America (subcontinent)' },
{ 'Countries of Oceania', 'Oceania' },
{ 'Countries of South America', 'South America' },
{ 'Countries in the United Kingdom', 'United Kingdom' },
{ 'Copyright rules by territory', 'CRT other' },
{ 'Olympic teams', 'Olympic teams' },
{ 'Most populous cities of the world', 'Module:Most populous cities of the world' },
{ 'Regions of France', 'Module:Regions of France' },
{ 'Departments of France', 'Module:Departments of France' },
}
local lines = {}
local function output(line)
lines[#lines + 1] = line
end
for _, templateid in ipairs(templateids) do
local template, id = templateid[1], templateid[2]
local module = getDataModuleName(frame, id)
local data = require(module)
-- Compatibility note: `.infos` is prefered, `.countries` is legacy.
local infos = data.infos or data.countries
if infos then
local codes, maxnames, usethe = {}, 0, false
for code, info in pairs(infos) do
codes[#codes + 1] = '' .. code
maxnames = math.max(#info, maxnames)
usethe = usethe or info.the
end
table.sort(codes)
output('== ' .. template .. ' ==')
output('* [[Template:' .. template .. ']]')
output('* [[' .. module .. ']]')
output('{|class="wikitable sortable"')
output('|-')
output('!scope="col"| Code')
output('!scope="col"| Wikidata')
if usethe then
output('!scope="col"| the')
end
output('!scope="col" colspan="' .. (maxnames < 2 and maxnames or 2)
.. '"| Name' .. (maxnames <= 1 and ''
or maxnames == 2 and ' and alias'
or ' and aliases')
.. ' on Commons')
for _, code in ipairs(codes) do
local info = infos[code]
output('|-')
output('!scope="row" style="text-align:left"| <kbd style="font-size:smaller">'
.. code .. '</kbd>')
output('| <small>' .. (info.qid and ('[[d:' .. info.qid .. '|'
.. info.qid .. ']]') or '') .. '</small>')
if usethe then
output(info.the
and ('| [[The ' .. info[1]
.. '|the]] <small>([[:Category:The ' .. info[1]
.. '|cat]])</small>')
or '|style="background:#CCC"| ')
end
output(info[1]
and ('| [[' .. info[1] .. ']] <small>([[:Category:'
.. info[1] .. '|cat]])</small>')
or '|style="background:#CCC"| ')
if maxnames >= 2 then
local aliases = {}
for i = 2, #info do
aliases[#aliases + 1] = '[[' .. info[i]
.. ']] <small>([[:Category:' .. info[i]
.. '|cat]])</small>'
end
output(#aliases > 0 and ('| '
.. table.concat(aliases, ', '))
or '|style="background:#CCC"| ')
end
end
output('|}')
output('')
end
end
return table.concat(lines, '\n')
end
--[==[
Exports.
--]==]
return {
main = main, -- For use in a MediaWiki template.
_main = _main, -- For use in Lua only.
show = show, -- Only for the documentation of this module.
}