Documentation for this module may be created at Module:Countries/doc

-- This module implements templates that output a countries navbox.
-- First usage was at [[Template:Countries of Europe]].

local _langSwitch -- cache for loading the Fallback module lazily
-- 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

-- 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*$')
		if text ~= '' then -- not nil and not empty
			return text:gsub('%s+', ' ') -- compress and normalize internal spaces
		end
	end
	return nil
end

-- used by getList()
local _makeSortKey -- cache for loading the MakeSortKey module lazily
-- used by _main()
local function getList(lists, exclude, options, infos)
	-- Return:
	-- * title of redirect target (prefixed by ':' if it's a category), if 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
			local target = titleObj.redirectTarget.prefixedText -- false if target does not exist
			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 =
				content:match('{{%s*[Cc]ategory[%s_]*[Rr]edirect%s*|%s*(.-)%s*}}') or
				content:match('{{%s*[Cc]at[%s%-]?redirect%s*|%s*(.-)%s*}}') or
				content:match('{{%s*[Ss]ee[%s_]*cat%s*|%s*(.-)%s*}}')
			if target then
				target = target:match('^%s*:?[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:%s*(.+)') or target
				return ':Category:' .. 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

	local info -- table used by getTitle() and makeItem() and set as info = codes[code]
	local function getTitle(tryThe) -- uses (info, options) from getList()
		-- Return the title of a link to an existing page (if not 'all'), or nil if none.
		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 -- string used by makeItem()

	local post -- string used by makeItem() and set according to info
	local function makeItem(tryThe) -- uses (info, options, itemPattern, suffix) from getList()
		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 sorting is crude (language neutral) and will often be unhelpful for specific languages.
	It is based on sort keys computed in Module:MakeSortKey.
	]=]
	local codes
	if 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>[<tt>' .. code .. '</tt>]</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:87%">' .. 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
		result = result:sub(1, -stdBulletTrailLen - 1) .. trail  -- omit trailing bullet from last item
	end
	return result
end

-- used by _main() and main()
local function isnonempty(text)
	return text and text ~= ''
end

-- 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 = {
		lang = lang,
		dir = langObj:getDir(), -- 'ltr' or 'rtl'
		colon = options.colon or ': ',
	}
	local wantSimple = options.simple and data.simple
	local exceptions = data.simple or {}
	local sections = exceptions.sections or {}
	for section, titles in pairs(data.titles) do
		if not wantSimple or sections[section] then -- add support for the variable named like '{section}title'
			var[section .. 'title'] = '<bdi>' .. langSwitch(titles, lang) .. '</bdi>'
		end
	end
	for section, lists in pairs(data.lists) do
		if not wantSimple or sections[section] then -- add support for the variable named like '{section}list'
			local exclude = wantSimple and {} or sections[section] or {}
			var[section .. 'list'] = getList(lists, exclude, options, data.infos)
		end
	end
	-- pattern contains '{variablename}' placeholders to replace by var['variablename']
	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 = ''
		for _, condpattern in ipairs(pattern) do	-- Elements with keys outside the numbered sequence are ignored.
			--[=[
			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		-- This is an unconditional subpattern.
				subpattern, condition = condpattern, true
			elseif type(condpattern) == 'table' then	-- This is a conditional subpattern.
				subpattern, condition = '', false
				for i, v in ipairs(condpattern) do	-- Elements with keys outside the numbered sequence are ignored.
					-- 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	-- The subpattern will EFFECTIVELY be used.
							break			-- Don't need to evaluate other conditions.
						--else	-- Other string values of v evaluate are ignored.
							-- Condition evaluates as false, other conditions must be evaluated.
						end
					-- Handle conditions that are arrays of subconditions (AND).
					-- If any one 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
						condition = true	-- The subpattern will then be used unless a subcondition evaluates to false
						for _, w in ipairs(v) do	-- Elements with keys outside the numbered sequence are ignored
							-- 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	-- The subcondition evaluated as false.
									break			-- The subpattern may still be used, but need to evaluate other conditions.
								--else	-- Other string values of v are ignored. Subcondition evaluates as false.
									-- Other 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):
								condition = false	-- The subcondition evaluated as false.
								break			-- The subpattern may still be used, but need to evaluate other conditions.
							end
						end
						-- if the array of subconditions (AND) is still true, the main condition (OR) evaluates as true immediately
						if condition then	-- The subpattern will EFFECTIVELY be used.
							break		-- Don't need to evaluate other conditions.
						end
					--else	-- Don't know what to do with this type of condition. And because this is part of an union (OR):
						-- Ignored, 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

-- 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.
-- Autodetect also 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

-- 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
	local id = stripToNil(frame.args[1]) -- identifies the group, for example 'Europe' (case sensitive) or 'Module:CustomData'
	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
	if data.infos == nil and data.countries ~= nil then -- compatiblity: infos is preferred, countries is legacy
		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 -- It's not possible to define both (ambiguous): infos is preferred, countries is legacy
		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
	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 = {
		-- Template                                      Data module id (without any ":", uses "Module:Countries/(id)" by default)
		{ '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' },
		{ 'Departments of France'                     , 'Module:Departments of France' },
		{ 'Regions of France'                         , 'Module:Regions 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)
		local infos = data.infos or data.countries -- infos is prefered, countries is legacy
		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, function (a, b) return a < b end)
			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"| <tt style="font-size:smaller">' .. code .. '</tt>')
				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"| &nbsp;')
				end
				output(info[1] and ('| [[' .. info[1] .. ']] <small>([[:Category:' .. info[1] .. '|cat]])</small>')
					or '|style="background:#CCC"| &nbsp;')
				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"| &nbsp;')
				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
}
Community content is available under CC-BY-SA unless otherwise noted.