Module:Wikidata Infobox/sandbox

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Lua

CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules

This module provides following functions:

How to contribute[edit]

Adding new properties[edit]

  1. Add the new property to property_groups. Properties are displayed in the order they appear in this table, so please look at Template:Wikidata Infobox/doc/properties to find a suitable place for the new property.
  2. If the property needs custom formatting, add it to property_logic

Testing your changes[edit]

Apart from saving your edits to the sandbox module, there are two ways to test your changes. You can preview the sandbox module on any Commons page by adding following lines to meta:Special:MyPage/global.js:

// [[w:User:Jackmcbarn/advancedtemplatesandbox.js]]
mw.loader.load( 'https://en.wikipedia.org/w/index.php?title=User:Jackmcbarn/advancedtemplatesandbox.js&action=raw&ctype=text/javascript' );

Then you can edit the sandbox module. Under "Preview page with this template", enter a page name. If the page uses {{Wikidata Infobox}}, you need to change the template name to "Module:Wikidata Infobox". Click "Show preview".

Another option is to use the Lua debug console: enter e.g. =p.debug'Q42' and paste the generated wikitext somewhere to preview it. Note that custom CSS is not applied and the images may not be switchable if you use this method.

Dependencies[edit]

This module depends on:

See also[edit]

Code

local p = {}
require('strict')
local WikidataIB = require( 'Module:WikidataIB' )
local i18n = require( 'Module:Wikidata Infobox/i18n' ).i18n
local getBestStatements = mw.wikibase.getBestStatements
local frame = mw.getCurrentFrame()

local config = {
	-- toggle/customize infobox features:
	defaultsort = true,
	interwiki = true,
	autocat = true,
	trackingcats = true,
	uploadlink = true,
	sitelinks = true,
	authoritycontrol = true,
	helperlinks = true,
	coordtemplate = 1, -- 0 = none, 1 = Geohack, 2 = Coord
	mapwidth = 250,
	mapheight = 250,
	imagesize = '230x500px',

	-- parameters for WikidataIB:
	spf = '',        -- suppressfields
	fwd = 'ALL',     -- fetchwikidata
	osd = 'no',      -- onlysourced
	noicon = 'yes',  -- pencil icon
	wdlinks = 'id',  -- add links to Wikidata if no label found
	collapse = 10,   -- collapse list of values if too many values
	maxvals = 30,    -- stop fetching Wikidata after this number of values
}

-- variables set by main():
local ITEM            -- mw.wikibase.entity table
local QID             -- qid of ITEM, e.g. 'Q42'
local CLAIMS          -- ITEM.claims
local ISTAXON         -- whether ITEM is a biological taxon
local INSTANCEOF = {} -- Hash set of ITEM's best "instance of" values
local MYLANG          -- user's languge code
local LANG            -- language object of user's language
local FALLBACKLANGS   -- list containing MYLANG and its fallback languages

-- Can't have more than one {{#coordinates:primary}}, so keep track of count
local primary_coordinates = 0

--- Returns label of given Wikidata entity in user's language.
--- If label doesn't exist, returns the id as link to Wikidata.
--- @param id string
--- @param nolink? boolean: Whether to return link to Wikidata if no label found
local function getLabel( id, nolink )
	local label = mw.wikibase.getLabel( id )
	if label then
		return mw.text.nowiki( label ) -- nowiki to prevent wikitext injection
	elseif nolink then
		return id
	else
		return '[[d:' .. id .. ']]'
	end
end

--- Query Wikidata entity for the first best value of property _pid_.
--- Returns nil if first best value is novalue or somevalue.
--- Returns nil if entityOrId is neither table nor string.
--- @param entityOrId table|string: getEntity() or qid.
--- @param pid string
--- @return unknown|nil
local function getSingleValue( entityOrId, pid )
	local claim
	if type( entityOrId ) == 'table' then
		claim = entityOrId:getBestStatements( pid )[1]
	elseif type( entityOrId ) == 'string' then
		claim = getBestStatements( entityOrId, pid )[1]
	end
	return claim and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value
end

--- Iterator function over a list of Wikidata claims/statements
--- @param t table as returned by wikibase.getBestStatements
local function iclaims( t )
	local i = 1
	return function()
		while i <= #t do
			local dv = t[i].mainsnak.datavalue
			local v = dv and dv.value
			i = i + 1
			if v then return v end
		end
	end
end

--- Returns Commons sitelink (full page title), preferably to category
--- @param qid string
--- @return string|nil
local function getCommonsLink( qid )
	local sitelink = mw.wikibase.getSitelink( qid, 'commonswiki' )
	if sitelink and sitelink:sub(1,9) == 'Category:' then
		return sitelink  -- sitelink to category page
	end

	local maincat = getSingleValue( qid, 'P910' )  -- topic's main category
	if maincat and maincat.id then
		local sl = mw.wikibase.getSitelink( maincat.id, 'commonswiki' )
		if sl then return sl end
	end

	local listcat = getSingleValue( qid, 'P1754' )  -- category related to list
	if listcat and listcat.id then
		local sl = mw.wikibase.getSitelink( listcat.id, 'commonswiki' )
		if sl then return sl end
	end

	local P373 = getSingleValue( qid, 'P373' )  -- Commons category
	if P373 then
		return 'Category:' .. P373
	end

	return sitelink  -- sitelink to gallery page
end

local getSitelink = (mw.wikibase.getGlobalSiteId() == 'commonswiki') and getCommonsLink or mw.wikibase.getSitelink

--- Returns sitelink to Commons as wikilink or the label of the given Q-item
--- @param qid string
local function getLinkOrLabel( qid )
	local sitelink = getSitelink( qid )
	if sitelink then
		return "[[:" .. sitelink .. "|" .. getLabel( qid, true ) .. "]]"
	else
		return getLabel( qid )
	end
end

--- Renders snak as rich wikitext. Returns nil if snak is nil or false.
--- @param snak table: claim.mainsnak or claim.qualifiers[pid]
local function renderSnak( snak )
	if not snak then return end
	local snaktype = snak.snaktype
	if snaktype == 'value' then
		local datatype = snak.datatype
		local value = snak.datavalue.value
		if datatype == 'wikibase-item' then
			return getLinkOrLabel( value.id )
		else
			return mw.wikibase.formatValue( snak )
		end
	elseif snaktype == 'somevalue' then
		local label = mw.message.new('Wikibase-snakview-variations-somevalue-label'):inLanguage(MYLANG):plain()
		return '<i style="color:#54595d">'..label..'</i>'
	end
end

--- Returns claim whose "language of work or name" (P407) qualifier matches
--- langcode, or nil if none matches.
--- @param claims table as returned by getBestStatements()
--- @param langcode string, e.g. "en"
--- @return unknown|nil
local function getClaimByLang( claims, langcode )
	for _, claim in ipairs( claims or {} ) do
		for _, qual in ipairs( claim.qualifiers and claim.qualifiers['P407'] or {} ) do
			if qual.datavalue and qual.datavalue.value and getSingleValue( qual.datavalue.value.id, 'P424' ) == langcode then
				return claim
			end
		end
	end
end

--- If the given snaks of datatype monolingualtext contain a string in one of
--- the user's fallback languages, the string is returned; otherwise a random
--- string is retuned. The second return value indicates whether finding a
--- string in one of the user's fallback languages was successful.
--- @param snaks table, e.g. claims.qualifiers['P2096']
--- @return string?, boolean? success
local function extractMonolingualText( snaks )
	if not snaks or snaks == {} then return end

	-- collect strings into hash table with langcodes as keys
	local monotext = {}
	for _, snak in ipairs( snaks ) do
		local ms = snak.mainsnak or snak
		local v = ms and ms.datavalue and ms.datavalue.value
		if v then
			monotext[v.language] = v.text
		end
	end

	for _, lang in ipairs( FALLBACKLANGS ) do
		if monotext[lang] then return monotext[lang], true end
	end

	-- return random string
	local _, v = next( monotext )
	return v, false
end

--- Parses a string in WikiHiero syntax
local function expandhiero( hiero )
	return frame:callParserFunction{ name = '#tag:hiero', args = {hiero} }
end

--- Returns a string containing two table rows
local function format2rowline( header, content )
	return '<tr><th class="wikidatainfobox-lcell" style="text-align:left" colspan="2">'..header..'</th></tr><tr><td style="vertical-align:top" colspan="2">'..content..'</td></tr>'
end

--- Returns a string containing a single table row
local function format1rowline( trqid, header, content )
	return '<tr id="'..trqid..'"><th class="wikidatainfobox-lcell">'..header..'</th><td style="vertical-align:top">'..content..'</td></tr>'
end

--- Returns a string containing the HTML markup for an infobox row.
--- Returns nil if content is empty.
--- @param eid string: ID of Wikidata entity whose label shall be used as heading
--- @param content string|nil
--- @param mobile? boolean: Set to true to show on devices with narrow screens
local function formatLine( eid, content, mobile )
	if not content or content == '' then return end
	local row = mw.html.create( 'tr' )
	if not mobile then
		row:addClass( 'wdinfo_nomobile' ) -- [[Template:Wikidata_Infobox/styles.css]]
	end
	row:tag( 'th' )
		:addClass( 'wikidatainfobox-lcell' )
		:node( LANG:ucfirst( getLabel(eid) ) )
	row:tag( 'td' )
		:node( content )
	return tostring( row )
end

--- Returns unbulleted HTML list if given a sequence table.
--- @param list string[]
local function ubl( list )
	if #list == 0 then return end
	local out = table.concat( list, '</li><li>' )
	return '<div class="plainlist"><ul><li>'..out..'</li></ul></div>'
end

--- Given a language code, returns its databaseId (as used by Wikidata sitelinks).
--- All databaseIds that a wiki knows are stored in its [[mw:Manual:sites table]].
--- @param langcode string
local function databaseId( langcode )
	local exceptions = {
		['be-tarask'] = 'be_x_old',     -- Belarusian (Taraškievica orthography)
		['bho']       = 'bh',           -- Bhojpuri
		['cbk-zam']   = 'cbk_zam',      -- Chavacano de Zamboanga
		['gsw']       = 'als',          -- Alemannic
		['ike']       = 'iu',           -- Inuktitut
		['lzh']       = 'zh_classical', -- Classical Chinese
		['map-bms']   = 'map_bms',      -- Basa Banyumasan
		['nan']       = 'zh_min_nan',   -- Min Nan Chinese
		['nb']        = 'no',           -- Norwegian Bokmål
		['nds-nl']    = 'nds_nl',       -- Low Saxon
		['mo']        = 'ro',           -- Moldaawisk
		['roa-tara']  = 'roa_tara',     -- Tarantino
		['rup']       = 'roa_rup',      -- Aromanian
		['sgs']       = 'bat_smg',      -- Samogitian
		['vro']       = 'fiu_vro',      -- Võro
		['yue']       = 'zh_yue',       -- Cantonese
		-- I did my best to make this list as comprehensive as possible.
		-- Useful pages for finding exceptions:
		-- [[mw:Manual:$wgExtraLanguageCodes]]
		-- [[meta:Special_language codes]]
		-- [[meta:List_of_Wikipedias#Nonstandard_language_codes]]
		-- [[meta:Template:N en/list]]
		-- [[meta:Template:Wikilangcode]]
	}

	local exception = exceptions[langcode]
	if exception then return exception end

	return langcode:gsub("-.*", "") -- delete everything after hyphen
end

-- Set of pids whose values should always be linked even if they are collapsed.
-- Adding new pids may slow down the infobox on certain pages.
local should_be_linked = {
	-- pid          property label             rationale
	P2789=true,  -- connects with              [[Template_talk:Wikidata_Infobox/Archive_5#P2789_-_connects_with]]
	P527=true,   -- has part(s)                [[Template_talk:Wikidata_Infobox/Archive_5#P2789_-_connects_with]]
	P1382=true,  -- partially coincident with  [[Template_talk:Wikidata_Infobox/Archive_5#P1382_vs._P527]]
	P40=true,    -- child                      [[Template_talk:Wikidata_Infobox#No_links_if_information_is_folded?]]
	P3373=true,  -- sibling                    [[Template_talk:Wikidata_Infobox#No_links_if_information_is_folded?]]
}

--- Wrapper around WikidataIB. Returns nil if the item has no _pid_ statement.
--- @param pid string: Wikidata property id
--- @param args? table: arguments for WikidataIB
--- @return string|nil
local function getValue( pid, args )
	args = args or {}

	local collapse = args.collapse or config.collapse
	if collapse == 0 and args.linked == nil then
		error("getValue: Must give linked='no' or linked='yes' if collapse=0", 2)
	end

	-- linking many values harms performance if the value items are big and the sitelink needs to be taken from P910, P1754 or P373
	local linked = args.linked or should_be_linked[pid]
			or #getBestStatements(args.qid or QID, pid) <= collapse

	return WikidataIB._getValue{
		pid,
		name = pid,
		qid = args.qid or QID,
		linked = linked,
		wdlinks = args.wdlinks or config.wdlinks,
		prefix = args.prefix,
		postfix = args.postfix,
		linkprefix  = ':', -- suppress categorization
		qlinkprefix = ':', -- suppress categorization
		sorted = args.sorted,
		qual = args.qual or 'MOST',
		qualsonly = args.qualsonly,
		maxvals = args.maxvals or config.maxvals,
		postmaxvals = '…',
		collapse = collapse,
		spf = args.spf or config.spf,
		fwd = args.fwd or config.fwd,
		osd = args.osd or config.osd,
		rank = 'best',
		noicon = args.noicon or config.noicon,
		list = args.list or 'Unbulleted list',
		sep = args.sep,
		unitabbr = args.unitabbr,
		df = args.df, -- date format
		plaindate = args.plaindate,
		lang = args.lang,
		gendered = args.gendered,
	}
end

--- Used if no custom logic was specified for pid.
local function defaultFunc( pid, args )
	return formatLine( pid, getValue(pid, args) )
end

local function defaultFuncMobile( pid, args )
	return formatLine( pid, getValue(pid, args), true )
end
local function defaultFuncMobileGendered( pid )
	return formatLine( pid, getValue(pid, {gendered=true}), true )
end

local function getAudio( pid )
	local audiofile = getSingleValue( ITEM, pid )
	return audiofile and formatLine( pid, '[[File:' .. audiofile .. '|100px]]' )
end

local function getAudioByLang( pid )
	local claims = ITEM:getBestStatements( pid )
	local claim = claims[1]
	for i = 1, #FALLBACKLANGS do
		local c = getClaimByLang( claims, FALLBACKLANGS[i] )
		if c then
			claim = c
			break
		end
	end
	local audiofile = claim and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value
	return audiofile and formatLine( pid, '[[File:' .. audiofile .. '|100px]]' )
end

-- Example at [[Category:Thutmosis III]]
local function getHieroglyphs()
	local rows = {}
	for _, v in ipairs( ITEM:getBestStatements('P7383') ) do -- name in hiero markup
		local idv = v.mainsnak.datavalue.value
		if v.qualifiers and v.qualifiers['P3831'] then
			for _, w in ipairs( v.qualifiers['P3831'] ) do
				if w.datavalue then
					local label = getLabel( w.datavalue.value.id )
					rows[#rows+1] = format2rowline( label, expandhiero(idv) )
				end
			end
		else
			rows[#rows+1] = format2rowline( getLabel('Q82799', true), expandhiero(idv) )
		end
	end
	return table.concat( rows )
end

--- WikidataIB arguments for birth and death related properties
local birthdeath_args = { list = '', quals = table.concat({
	'P4241',  -- refine date
	'P805',   -- statement is subject of
	'P1932',  -- object stated as
	'P1810',  -- subject named as
	'P5102',  -- nature of statement
	'P1480',  -- sourcing circumstances
	'P459',   -- determination method
	'P1013',  -- criterion used
	'P1441',  -- present in work
	'P10663', -- applies to work
}, ',') }

local function getBirth( pid )
	local out = {}
	out[#out+1] = getValue( pid, birthdeath_args )                     -- date
	out[#out+1] = CLAIMS['P19'] and getValue( 'P19', birthdeath_args ) -- place
	out[#out+1] = extractMonolingualText( ITEM:getBestStatements('P1477') ) -- name
	return formatLine( pid, table.concat(out, '<br>') )
end

local function getDeath( pid )
	local out = {}
	out[#out+1] = getValue( pid, birthdeath_args )                     -- date
	out[#out+1] = CLAIMS['P20'] and getValue( 'P20', birthdeath_args ) -- place
	return formatLine( pid, table.concat(out, '<br>') )
end

local function getWebsite( pid )
	for _, claim in ipairs( ITEM:getBestStatements(pid) ) do
		local quals = claim.qualifiers
		local url = claim.mainsnak.datavalue and claim.mainsnak.datavalue.value
		if url and not (quals and quals['P582']) then -- no "end time" qualifier
			return '<tr><td colspan=2 style="text-align:center">['..url..' '..getLabel(pid)..']</td></tr>'
		end
	end
end

local function getSignature( pid )
	local img = getSingleValue( ITEM, pid )
	if img then
		local alt = LANG:ucfirst( getLabel(pid, true) )
		return '<tr class="wdinfo_nomobile"><td colspan=2 style="text-align:center"><span class="wpImageAnnotatorControl wpImageAnnotatorCaptionOff">[[File:'..img..'|150px|alt='..alt..']]</span></td></tr>'
		-- equivalent to {{ImageNoteControl | caption=off | type=inline}}
	end
end

--- If ITEM has a pid statement, this behaves exactly like defaultFunc. Otherwise
--- figures out the region that ITEM is in and queries the region item for pid.
--- It finds the region by first checking if ITEM has a regionPid value.
--- Otherwise it takes the region from the P971 (category combines topics)
--- statement of ITEM's main category.
--- Examples at Cat:Health_in_Gabon, Cat:Economy_of_Germany, Q7246071
--- @param pid string, e.g. "P2250" for life expectancy
--- @param regionPid string: usually "P17" (country) or "P276" (location)
--- @param collapse number: argument for WikidataIB
local function getByRegion( pid, regionPid, collapse )
	local region = getSingleValue( ITEM, regionPid )
	if CLAIMS[pid] then
		region = QID
	elseif region then
		region = region.id
	else
		local maincat = getSingleValue( ITEM, 'P910' ) -- topic's main category
		if maincat then
			for topic in iclaims( getBestStatements(maincat.id, 'P971') ) do
				local id = topic.id
				if id ~= 'Q12147' and id ~= 'Q8434' and id ~= 'Q159810' then
					-- assume id is QID of a region if it's not the QID for "health", "education", or "economy"
					region = id
				end
			end
		end
	end
	return region and defaultFunc( pid, {
		qid = region,
		collapse = collapse,
	})
end
local function getByCountry( pid )
	return getByRegion( pid, 'P17', 10 )
end
local function getByLocation( pid )
	return getByRegion( pid, 'P276', 10 )
end
local function getByLocationCollapse4( pid )
	return getByRegion( pid, 'P276', 4 )
end

local function getPrimeFactors()
	local out = {}
	for _, claim in ipairs( ITEM:getBestStatements('P5236') ) do
		local quals = claim.qualifiers and claim.qualifiers['P1114']
		local quantity = quals and quals[1].datavalue.value.amount
		if quantity then
			quantity = quantity:sub(2)  -- strip plus sign
			out[#out+1] = renderSnak(claim.mainsnak) .. '<sup>'..quantity..'</sup>'
		else
			out[#out+1] = renderSnak(claim.mainsnak)
		end
	end
	return formatLine( 'Q4846249', table.concat(out, ' × ') )
end

local function getUnicodeChars( pid )
	local rows = {}
	for _, v in ipairs( ITEM:getBestStatements(pid) ) do
		local idv = v.mainsnak.datavalue.value
		for _, w in ipairs( v.qualifiers and v.qualifiers['P3831'] or {} ) do
			if w.datavalue then
				local qualid = w.datavalue.value.id
				rows[#rows+1] = format1rowline( qualid, getLabel(qualid), idv )
			end
		end
	end
	return table.concat( rows )
end

local function getCodes( pid )
	local rows = {}
	for _, v in ipairs( ITEM:getBestStatements(pid) ) do
		local idv = v.mainsnak.datavalue.value
		for _, w in ipairs( v.qualifiers and v.qualifiers['P3294'] or {} ) do
			if w.datavalue then
				local qualid = w.datavalue.value.id
				if qualid == "Q68101340" then
					idv = expandhiero( idv )
				end
				rows[#rows+1] = format1rowline( qualid, getLinkOrLabel(qualid), idv )
			end
		end
	end
	return table.concat( rows )
end

local function getCodeImages( pid )
	local rows = {}
	for _, v in ipairs( ITEM:getBestStatements(pid) ) do
		local idv = v.mainsnak.datavalue.value
		for _, w in ipairs( v.qualifiers and v.qualifiers['P3294'] or {} ) do
			if w.datavalue then
				local qualid = w.datavalue.value.id
				local img = '[[File:' .. idv .. '|none|35px]]'
				rows[#rows+1] = format1rowline( qualid, getLabel(qualid), img )
			end
		end
	end
	return table.concat( rows )
end

local function getLocation()
	local function fallback()
		local set = {} -- locations as keys
		local out = {} -- locations as values
		for _, pid in ipairs{ 'P706', 'P276', 'P131', 'P17' } do
			for _, claim in ipairs( ITEM:getBestStatements(pid) ) do
				local location
				if pid == 'P17' then  -- don't link to countries
					local dv = claim.mainsnak.datavalue
					location = dv and getLabel( dv.value.id )
				else
					location = renderSnak( claim.mainsnak )
				end
				if location and not set[location] then
					local n = #out + 1
					set[location] = true     -- we don't want duplicate values
					out[n]        = location -- we want to preserve the order

					if n > config.maxvals then
						out[n] = '…' -- postmaxvals
						return formatLine( 'P276', ubl(out) )
					end
				end
			end
		end
		return formatLine( 'P276', ubl(out) )
	end

	local P131,P276,P706 = CLAIMS['P131'] or {}, CLAIMS['P276'] or {}, CLAIMS['P706'] or {}
	if (#P131 < 2) and (#P276 < 2) and (#P706 < 2) then
		return formatLine( 'P276', WikidataIB.location{ args={QID} } ) or fallback()
	else
		return fallback()
	end
end

local function getAuthors()
	if CLAIMS['P50'] or CLAIMS['P2093'] then
		local args = { list='', sep='</li><li>', collapse=0, maxvals=10, linked='yes', qual='P1545,P518,P5102,P3831' }
		local authors = getValue( 'P50', args ) or ''
		local namestrings = getValue( 'P2093', args )
		return formatLine( 'P50', ubl{authors, namestrings} )
	end
end

local function getDifferentFrom()
	local out = {}
	local i = 0
	for different in iclaims( ITEM:getBestStatements('P1889') ) do
		i = i + 1
		if i > config.maxvals then break end
		local href = getSitelink( different.id ) or ( 'd:'..different.id )
		local label = getLabel( different.id, true )

		local class = getSingleValue( different.id, 'P31' )
		local isdab = class and class.id == 'Q4167410'
		local icon = isdab and ' [[File:Disambig.svg|18px|alt='..mw.wikibase.getLabel('Q4167410')..']]'

		local desc = mw.wikibase.getDescription( different.id )
		if desc then
			label = '<span title="'..mw.text.nowiki(desc)..'">'..label..'</span>'
		end

		out[#out+1] = string.format( '[[:%s|%s]]%s', href, label, icon or '' )

	end
	return formatLine( 'P1889', ubl(out) )
end

--- Returns common taxon name using [[Module:Wikidata4Bio]]
local function getVernacularName()
	if ISTAXON then
		local vn = frame:expandTemplate{ title = 'VNNoDisplay', args = {
			useWikidata = QID
		}}
		if vn:sub(3,10) ~= 'Category' and not vn:match('class="error') then
			-- we found at least one common name and there are no errors
			local label = LANG:ucfirst( getLabel('Q502895') )
			return '<tr><td colspan=2><table style="width:100%"><tr><th style="background: #cfe3ff>'..label..'</th></tr><tr><td><div style="overflow-wrap: break-word" class="mw-collapsible mw-collapsed wikidatainfoboxVN" id="wdinfoboxVN">'..vn..'</div></td></tr></table></td></tr>'
		end
	end
end

local function getTaxontree()
	local content = require('Module:Taxontree').show{ args = {
		qid = QID,
		authorcite = 'y',
		first = 'y',
	}}
	local label = LANG:ucfirst( getLabel('Q8269924') )
	return '<tr><td colspan=2><table style="width:100%" id="wdinfo_taxon" class="mw-collapsible"><tr><th style="background: #cfe3ff" colspan=2>'..label..'</th></tr>'..content..'</table></td></tr>'
end

local function getOriginalCombination()
	local ocomb = getSingleValue( ITEM, 'P1403' )
	ocomb = ocomb and ocomb.id
	local taxoname = ocomb and getSingleValue( ocomb, 'P225'  ) or ''
	local citation = ocomb and getSingleValue( ocomb, 'P6507' ) or ''
	if taxoname then
		return formatLine( 'P1403', '<i>'..taxoname..'</i>' .. ' ' .. citation )
	end
end

--- Creates a taxon author citation from P405 and P574 qualifiers if
--- P6507 (taxon author citation as string) not present since otherwise
--- Taxontree already shows the citation.
local function getTaxonAuthor()
	local claims = CLAIMS['P225'] -- P225 = taxon name
	if #claims > 1 then
		return defaultFunc( 'P225' ) -- Example at [[Category:Acacia stricta]]
	elseif #claims == 1 then
		if CLAIMS['P6507'] then -- P6507 = taxon author citation (string)
			return -- Taxontree already shows citation, see [[Ophiogymna]]
		end
		local quals = claims[1].qualifiers
		local author = renderSnak( quals and quals['P405'] and quals['P405'][1] )
		local year = renderSnak( quals and quals['P574'] and quals['P574'][1] )
		if author and year then
			return formatLine( 'P405', author .. ', ' .. year )
		elseif year then
			return formatLine( 'P574', year ) -- [[Cat:Porphyrophora polonica]]
		end
	end
end

--- Given an area, returns a map zoom level to use with mw:Extension:Kartographer.
--- Fallback output is 15.
local function autoMapZoom( area )
	if not area then return 15 end
	if area.unit == 'http://www.wikidata.org/entity/Q35852' then  -- hectare
		area = area.amount / 100  -- convert to km²
	elseif area.unit == 'http://www.wikidata.org/entity/Q25343' then  -- m²
		area = area.amount / 1e6  -- convert to km²
	elseif area.unit == 'http://www.wikidata.org/entity/Q81292' then  -- acre
		area = area.amount * 0.004  -- convert to km²
	else
		area = tonumber( area.amount )  -- assume the unit is km²
	end
	local LUT = { 5000000, 1000000, 100000, 50000, 10000, 2000, 150, 50, 19, 14, 5, 1, 0.5 }
	for zoom, scale in ipairs( LUT ) do
		if area > scale then
			return zoom + 1
		end
	end
	return 15
end

local function getCoordinates( pid )
	local coords = getSingleValue( ITEM, pid )
	if coords then
		local out
		local long = coords.longitude
		local lat  = coords.latitude
		local globeId = coords.globe:match( "Q%d+" )
		if globeId == 'Q2' then -- coords are on Earth
			local externaldata = { -- [[mw:Help:Extension:Kartographer]]
				type = "ExternalData",
				service = "geoshape",
				ids = QID,
				properties = {
					['fill'] = "#999999",
					['stroke'] = "#636363",
					['stroke-width'] = 2
				}
			}

			-- detect roads, mountain passes, rivers, borders etc.
			if CLAIMS['P2043'] or CLAIMS['P16']        -- length, transport network
			or CLAIMS['P974']  or CLAIMS['P4552']      -- tributary, mountain range
			or CLAIMS['P177']  or CLAIMS['P1064']      -- crosses, track gauge
			or CLAIMS['P15']   or CLAIMS['P14']        -- route map, traffic sign
			or CLAIMS['P930']  or CLAIMS['P3858'] then -- electrification, route diagram
				externaldata.service = 'geoline'
				externaldata.properties['stroke'] = "#ff0000"
			end

			local geojson = {
				externaldata,
				{ type = "Feature",
				  geometry = { type="Point", coordinates = {long, lat} },
				  properties = {
				  	['marker-size'] = "medium",
				  	['marker-color'] = "006699"
				  },
				},
			}

			local zoom
			if CLAIMS['P402'] then  -- OpenStreetMap relation ID
				-- Let Kartographer figure out zoom level based on OSM geoshape.
				-- Kartographer uses [[mw:Wikimedia_Maps/API#OSM_Geoshapes_and_lines]]
				-- instead of P402 to find the OSM relation but there is no Lua
				-- interface for that. You can help adding P402 statements using
				-- https://mix-n-match.toolforge.org/#/catalog/688
			else
				local area = getSingleValue( ITEM, 'P2046' )
				zoom = autoMapZoom( area )
			end

			out = frame:extensionTag( 'mapframe', mw.text.jsonEncode(geojson), {
				frameless = 1,
				lang = MYLANG,
				width = config.mapwidth,
				height = config.mapheight,
				zoom = zoom,
				align = 'center',
			})
			if config.trackingcats then
				out = out ..'[[Category:Uses of Wikidata Infobox with maps]]'
			end
			if config.coordtemplate == 1 then
				if primary_coordinates == 0 then
					out = out .. frame:callParserFunction('#coordinates:primary', lat, long)
					primary_coordinates = 1
				end
				out = out .. '<small>'..require('Module:Coordinates')._GeoHack_link{ lat=lat, lon=long, lang=MYLANG }..'</small>'
			elseif config.coordtemplate == 2 then
				local args = {
					display = 'inline,title',
					format = 'dms',
					nosave = 1,
					qid = QID
				}
				out = out .. '<small>'..frame:expandTemplate{ title = 'Coord', args = args }..'</small>'
			end
		else -- coords not on Earth
			local globe = mw.wikibase.getLabelByLang( globeId, 'en' )
			out = require('Module:Coordinates')._GeoHack_link{ lat=lat, lon=long, globe=globe, lang=MYLANG }
		end

		return '<tr class="wdinfo_nomobile"><td colspan=2 style="text-align:center">'..out..'</td></tr>'
	elseif config.trackingcats and (CLAIMS['P706'] or CLAIMS['P131']) then
		return '[[Category:Uses of Wikidata Infobox with no coordinate]]'
	end
end

--- Show map using [[mw:Help:Map Data]] if ITEM has no coordinates
local function getCommonsMapData()
	if CLAIMS['P625'] then return end
	local commonsdata = getSingleValue( QID, 'P3896' )
	if not commonsdata then return end
	local geojson = {{
		type = "ExternalData",
		service = 'page',
		title = commonsdata:sub(6),  -- strip "Data:" prefix
	}}
	local out = frame:extensionTag( 'mapframe', mw.text.jsonEncode(geojson), {
		frameless = 1,
		lang = MYLANG,
		width = config.mapwidth,
		height = config.mapheight,
		align = 'center',
	})
	if config.trackingcats then
		out = out ..'[[Category:Uses of Wikidata Infobox with maps]]'
	end
	return '<tr class="wdinfo_nomobile"><td colspan=2 style="text-align:center">'..out..'</td></tr>'
end

local function getCelestialCoordinates()
	local ra = getSingleValue( ITEM, 'P6257' )  -- right ascension
	local de = getSingleValue( ITEM, 'P6258' )  -- declination
	if ra and de then
		local url = 'http://www.wikisky.org/?ra='..(ra.amount / 15)..'&de='..de.amount..'&de=&show_grid=1&show_constellation_lines=1&show_constellation_boundaries=1&show_const_names=1&show_galaxies=1&img_source=DSS2&zoom=9 '
		local ra_unit = getLabel( ra.unit:match('Q%d+') )
		local de_unit = getLabel( de.unit:match('Q%d+') )
		local ra_fmt = LANG:formatNum( tonumber(ra.amount) )
		local de_fmt = LANG:formatNum( tonumber(de.amount) )
		local text = LANG:ucfirst( getLabel('P6257') )..' '..ra_fmt..' '..ra_unit..
		     '<br>'..LANG:ucfirst( getLabel('P6258') )..' '..de_fmt..' '..de_unit
		return '<tr class="wdinfo_nomobile"><td colspan=2 style="text-align:center">['..url..text..']</td></tr>'
	end
end

local autocats_by_id = {
	P3596 = 'Archaeological monuments in Denmark with known IDs',
	P1371 = 'ASI monuments with known IDs',
	P2917 = 'Buildings of Madrid with COAM Register number',
	P3170 = 'Cultural heritage monuments in Armenia with known IDs',
	P2951 = 'Cultural heritage monuments in Austria with known IDs',
	P4244 = 'Cultural heritage monuments in Bavaria with known IDs',
	P2424 = 'Cultural heritage monuments in Berlin with known ID',
	P2948 = 'Cultural heritage monuments in Estonia (with known IDs)',
	P4009 = 'Cultural heritage monuments in Finland with known IDs',
	P380  = 'Cultural heritage monuments in France with known IDs',
	P4166 = 'Cultural heritage monuments in Georgia with known IDs',
	P1769 = 'Cultural heritage monuments in Hesse with known ID',
	P1369 = 'Cultural heritage monuments in Iran with known IDs',
	P1799 = 'Cultural heritage monuments in Malta with known IDs',
	P758  = 'Cultural heritage monuments in Norway with known IDs',
	P1770 = 'Cultural heritage monuments in Romania with known IDs',
	P1708 = 'Cultural heritage monuments in Saxony with known ID',
	P808  = 'Cultural heritage monuments in Spain by ID',
	P762  = 'Cultural monuments in the Czech Republic with known IDs',
	P477  = 'Heritage properties in Canada with known IDs',
	P5094 = 'HPIP with known IDs',
	P1702 = 'IGESPAR with known IDs',
	P5500 = 'IPHAN with known IDs',
	P2783 = 'Listed buildings in Denmark with known IDs',
	P1216 = 'Listed buildings in England with known IDs',
	P1460 = 'Listed buildings in Northern Ireland with known IDs',
	P709  = 'Listed buildings in Scotland with known IDs',
	P1459 = 'Listed buildings in Wales with known IDs',
	P649  = 'National Register of Historic Places with known IDs',
	P4120 = 'Ontario Heritage Trust sites with known IDs',
	P2961 = 'Periodicals in the Biblioteca Virtual de Prensa Histórica',
	P7135 = 'Rijksmonumentcomplexen with known IDs',
	P359  = 'Rijksmonumenten with known IDs',
	P1700 = 'SIPA with known IDs',
	P3759 = 'Uses of Wikidata Infobox providing SAHRA ids',
	P809  = 'Uses of Wikidata Infobox providing WDPA ids',
}

--- qualifiers for "headquarters location" (P159)
local hq_quals = table.concat({
	'P6375',  -- street address
	'P669',   -- located on street
	'P670',   -- street number
	'P4856',  -- conscription number
	'P281',   -- postal code
	'P580',   -- start time
	'P582',   -- end time
	'P585',   -- point in time
	'P1264',  -- valid in period
	'P3831',  -- object has role
	'P1810',  -- subject named as
	'P5102',  -- nature of statement
}, ',' )

--- associates pids with a table of arguments for WikidataIB or with a function
--- that will be called with pid as the only argument
local property_logic = {
	P51    = getAudio,                    -- audio
	P989   = getAudioByLang,              -- spoken text audio
	P443   = getAudioByLang,              -- pronunciation audio
	P990   = getAudioByLang,              -- recording of subject's voice
	P7383  = getHieroglyphs,              -- name in hiero markup
	P569   = getBirth,                    -- date of birth
	P570   = getDeath,                    -- date of death
	P69    = { qual='P580,P582,P585,P512,P812' }, -- educated at
	P185   = { collapse=4, maxvals=20 },  -- doctoral student
	P106   = defaultFuncMobileGendered,   -- occupation
	P39    = { qual='P642,P580,P582,P585', collapse=6 }, -- position held
	P2522  = { collapse=4 },              -- victory
	P26    = { qual='DATES' },            -- spouse, TODO: sort by date qualifier (also P793)
	P451   = { qual='DATES' },            -- partner
	P166   = { qual='P585' },             -- award received
	P856   = getWebsite,                  -- official website
	P109   = getSignature,                -- signature
	P31    = defaultFuncMobile,           -- instance of
	P2250  = getByCountry,                -- life expectancy
	P4841  = getByCountry,                -- total fertility rate
	P5236  = getPrimeFactors,             -- prime factor
	P487   = getUnicodeChars,             -- Unicode character
	P3295  = getCodes,                    -- code
	P7415  = getCodeImages,               -- code (image)
	P3270  = getByLocation,               -- compulsory education (minimum age)
	P3271  = getByLocation,               -- compulsory education (maximum age)
	P6897  = getByLocationCollapse4,      -- literacy rate
	P2573  = getByLocationCollapse4,      -- number of out-of-school children
	P971   = { osd='no' },                -- category combines topics
	P180   = { list='prose', qual='' },   -- depicts
	P276   = getLocation,                 -- location
	P50    = getAuthors,                  -- author
	P2789  = { qual='' },                 -- connects with
	P85    = { qual='DATES' },            -- anthem
	P953   = { qual='P407', prefix="[", postfix="]" }, -- full work at
	P127   = { qual='DATES' },            -- owned by
	P159   = { qual=hq_quals },           -- headquarters location
	P466   = { collapse=5 },              -- occupant
	P126   = { collapse=5, maxvals=20 },  -- maintained by
	P348   = { qual='P548,P577,P805' },   -- software version identifier
	P286   = { collapse=3 },              -- head couch
	P527   = { collapse=5, maxvals=20 },  -- has part
	P1382  = { collapse=5, maxvals=20 },  -- partially coincident with
	P1990  = { collapse=5 },              -- species kept
	P1923  = { collapse=5, maxvals=10 },  -- participating team
	P1346  = { collapse=5, maxvals=20 },  -- winner
	P112   = { maxvals=20 },              -- founded by
	P577   = {
		linked = 'no',             -- make film categories load much quicker
		rank = 'preferred normal', -- See [[d:Property_talk:P577#Constraint_about_unique_best_value]]
	},
	P1082  = { qual='P585' },             -- population (qual = point in time)
	P200   = { collapse=4, maxvals=20 },  -- lake inflows
	P205   = { collapse=5, maxvals=20 },  -- basin country
	P974   = { collapse=5, maxvals=20 },  -- tributary
	P726   = { collapse=5 },              -- candidate
	P1889  = getDifferentFrom,            -- different from
	P460   = { collapse=20, list='' },    -- same as (lots of values for given names)
	P1843  = getVernacularName,           -- taxon common name
	P171   = getTaxontree,                -- parent taxons
	P1403  = getOriginalCombination,      -- original combination
	P225   = getTaxonAuthor,              -- taxon name (and qualifiers)
	P2078  = getWebsite,                  -- user manual URL
	P625   = getCoordinates,              -- coordinate location
	P3896  = getCommonsMapData,           -- geoshape
	P6257  = getCelestialCoordinates,     -- right ascension
}

--[==[----------------------------------------------------------------------
This table is used by main() to generate the infobox and by doc() to
generate [[Template:Wikidata Infobox/doc/properties]].

* `humans_allowed` determines whether the group should be displayed if the
  item is a human (Q5) or a fictional human (Q15632617). It defaults to false.
* A group will only be displayed if `P31_allowed_values` contains the
  "instance of" (P31) value of the item or `P31_allowed_values` is not present.
* If `bypass_property_exists_check` is set to true, the infobox tries to fetch
  the values for each pid in the group, even if the item has no pid statement.
* `logic` can be a function that will be called with pid as the only argument.
  `logic` can also be a WikidataIB arguments table for defaultFunc.
]==]
local property_groups = {
	{ groupname = 'Switchable images', -- this group needs to be at index 1
	  comment = 'Users can switch between these images using [[MediaWiki:Gadget-Infobox.js|Gadget-Infobox.js]].',
	  humans_allowed = true,
	  pids = {'P2716','P18','P117','P8224','P1442','P1801','P3383','P4640','P4291','P3451','P5252','P2713','P8592','P8517','P5555','P5775','P7417','P9721','P3311','P7420','P7457','P8195','P1543','P996','P3030','P154','P2910','P41','P94','P4004','P158','P2425','P8766','P14','P1766','P15','P8512','P181','P207','P242','P1944','P1943','P1846','P1621','P367','P491','P6655','P10','P4896','P11101','P11702','P12565'},
	},
	{ groupname = 'Audio and hieroglyphs',
	  humans_allowed = true,
	  pids = {'P51','P989','P443','P990','P7383'},
	},
	{ groupname = 'Human',
	  P31_allowed_values = { 'Q5', 'Q15632617' },
	  humans_allowed = true,
	  pids = {'P1559','P569','P570','P1196','P509','P157','P119','P742','P2031','P2032','P1317','P27','P1532','P551','P69','P184','P185','P106','P2416','P6087','P54','P108','P463','P102','P39','P101','P135','P66','P103','P97','P2962','P2522','P53','P22','P25','P3373','P40','P26','P1038','P451','P937','P800','P1441','P166','P856','P109'},
	},
	{ groupname = 'Instance/subclass of',
	  pids = {'P31','P279'},
	},
	{ groupname = 'Health by region',
	  P31_allowed_values = { 'Q64027457' },
	  pids = {'P2250','P4841'},
	  bypass_property_exists_check = true,
	},
	{ groupname = 'Natural number',
	  P31_allowed_values = { 'Q21199' },
	  pids = {'P5236','P487','P3295','P7415'},
	},
	{ groupname = 'Education by region',
	  P31_allowed_values = { 'Q64801076' },
	  pids = {'P3270','P3271','P6897','P2573'},
	  bypass_property_exists_check = true,
	},
	{ groupname = 'National economy',
	  P31_allowed_values = { 'Q6456916' },
	  pids = {'P38','P2299','P4010','P2131','P2132','P2219','P1279','P2134','P2855'},
	  bypass_property_exists_check = true,
	  logic = getByLocationCollapse4,
	},
	{ groupname = 'Miscellaneous 1',
	  pids = {'P361','P1639','P1269','P921','P629','P1559','P452','P7163','P971','P4224','P831','P2317','P138','P825','P417','P547','P180','P2596','P186','P136','P376','P3018','P7532'},
	},
	{ groupname = 'Location',
	  comment = 'The properties {{P|131}}, {{P|276}}, {{P|706}}, and {{P|17}} together produce a single infobox row.',
	  pids = {'P276'},
	  bypass_property_exists_check = true,
	},
	{ groupname = 'Miscellaneous 2',
	  pids = {'P1001','P206','P5353','P4856','P6529','P9759','P6375','P669','P495','P1885','P149','P708','P2872','P16','P2789','P59','P65','P215','P223','P196','P36','P122','P194','P208','P209','P37','P85','P38','P35','P6','P210'},
	},
	{ groupname = 'Author',
	  comment = 'Will be displayed together with {{P|2093}}.',
	  pids = {'P50'},
	  bypass_property_exists_check = true,
	},
	{ groupname = 'Miscellaneous 3',
	  pids = {'P655','P123','P1433','P84','P193','P170','P86','P676','P87','P61','P189','P98','P58','P110','P162','P175','P393','P291','P4647','P407','P2635','P437','P953','P275','P1441','P1080','P88','P6291','P199','P169','P366','P121','P127','P159','P466','P137','P126','P177','P2505','P144','P822','P115','P5138','P118','P505','P286','P527','P1454','P1990','P2522','P1427','P1444','P1923','P1132','P1346','P176','P1071','P617','P504','P532','P8047','P289','P426','P113','P114','P375','P619','P1145','P522','P664','P823','P5804','P57','P161','P195','P217','P178','P112','P400','P306','P1435','P814','P141','P348','P585','P606','P729','P730','P580','P571','P577','P1191','P5444','P575','P1619','P3999','P582','P576','P2669','P793','P516','P2957','P2109','P618','P128','P129','P111','P179'},
	},
	{ groupname = 'Quantities',
	  pids = {'P1093','P2067','P2261','P2262','P2049','P2386','P2043','P3157','P2583','P2048','P5524','P2808','P2144','P3439','P4183','P5141','P4552','P2660','P2659','P610','P559','P7309','P1082','P2052','P2217','P2046','P2044','P2050','P2047'},
	  logic = { unitabbr='yes' },
	},
	{ groupname = 'Miscellaneous 4',
	  pids = {'P140','P1083','P2351','P2324','P6801','P6855','P3032','P3137','P770','P1398','P167','P81','P197','P833','P834'},
	},
	{ groupname = 'Water',
	  pids = {'P885','P403','P200','P201','P4614','P205','P974','P4792','P4661','P469','P2673','P2674'},
	},
	{ groupname = 'Miscellaneous 5',
	  pids = {'P155','P156','P1365','P1366','P3730','P3729'},
	},
	{ groupname = 'Elections',
	  pids = {'P991','P726','P1831','P1867','P1868','P1697','P5043','P5045','P5044'},
	},
	{ groupname = 'Miscellaneous 6',
	  pids = {'P1590','P1120','P1446','P1339','P1092','P784','P783','P785','P786','P787','P788','P789','P183','P2130','P2769','P1174','P859','P218','P78','P238','P239','P1889','P460','P1382','P2010','P2009','P2033','P1531','P8193'},
	},
	{ groupname = 'Taxon common name',
	  comment = "Common names are taken from the item's label, sitelink, and {{P|1843}}.",
	  pids = {'P1843'},
	  bypass_property_exists_check = true,
	},
	{ groupname = 'Taxonomy',
	  pids = {'P171','P1403','P225'},
	},
	{ groupname = 'Miscellaneous 7',
	  pids = {'P6591','P7422','P2078','P856','P6257'},
	},
	{ groupname = 'Maps',
	  comment = '{{P|3896}} is only used if no {{P|625}} statement exists. Tracked at {{c|Uses of Wikidata Infobox with maps}}.',
	  pids = {'P625','P3896'},
	  bypass_property_exists_check = true,
	},
}

local externalIDs = {
	{ groupname = 'Authority control',
	  pids = {'P213','P214','P227','P244','P268','P269','P270','P349','P409','P508','P640','P651','P691','P886','P902','P906','P947','P949','P950','P1003','P1006','P1015','P1048','P1157','P1207','P1225','P1415','P1695','P2558','P2581','P4819','P5034','P5587','P7293','P8189','P9371','P10539',}
	},
	{ groupname = 'Books/magazines/authors/libraries',
	  pids = {'P236','P271','P396','P648','P723','P724','P2961','P5199',}
	},
	{ groupname = 'Science',
	  pids = {'P356','P496','P549','P698','P717','P932','P1053','P2349','P3083','P8273',}
	},
	{ groupname = 'Biology',
	  pids = {'P428','P627','P685','P687','P6535','P815','P830','P838','P842','P846','P850','P938','P959','P960','P961','P962','P1070','P1076','P1348','P1391','P1421','P1727','P1745','P1746','P1747','P1761','P1772','P1832','P1895','P1940','P1991','P1992','P2007','P2026','P2036','P2040','P2426','P2434','P2455','P2464','P2752','P2833','P2946','P3031','P3060','P3064','P3099','P3100','P3101','P3102','P3151','P3240','P3288','P3398','P3420','P3444','P3591','P3594','P3606','P3746','P4024','P4122','P4194','P4301','P4526','P4567','P4728','P4758','P4855','P5036','P5037','P5055','P5216','P5221','P5257','P5299','P6678','P7051',}
	},
	{ groupname = 'Art',
	  pids = {'P245','P347','P434','P650','P781','P1882','P1901','P3293','P3634','P4399','P4659','P4701','P5950','P6506','P6631','P7704','P8386','P9394',}
	},
	{ groupname = 'Culture',
	  pids = {'P345','P539','P1219','P1220','P1248','P1362','P6113','P6132','P12037',}
	},
	{ groupname = 'Sports',
	  pids = {'P1146','P1440','P1469','P1665','P2020','P2276','P2446','P2458','P2574','P3171','P3537','P3538','P3681','P3924','P8286',}
	},
	{ groupname = 'Cultural heritage and architecture',
	  pids = {'P359','P380','P381','P454','P481','P649','P709','P718','P757','P758','P762','P808','P1216','P1305','P1459','P1483','P1600','P1700','P1702','P1708','P1764','P1769','P2424','P2783','P2081','P2917','P3038','P3177','P3178','P3318','P3449','P3596','P3758','P3759','P4009','P4075','P4102','P4244','P4360','P4372','P4868','P5094','P5310','P5313','P5500','P5525','P5528','P6102','P6542','P6736','P7006','P7170','P7304','P7630','P7659','P7694','P7900','P9148','P9154','P9339','P9342','P10486','P11351',}
	},
	{ groupname = 'Protected areas',
	  pids = {'P809','P3425','P3613','P3974','P5965','P6602','P6230','P6280','P6478','P6560','P6659','P3296','P677',}
	},
	{ groupname = 'Places and geographical features',
	  pids = {'P402','P11693','P10689','P3120','P3580','P3616','P3628','P4266','P6630','P7350','P7352','P7548','P8655','P8988','P10451','P4533',}
	},
	{ groupname = 'Administrative subdivisions',
	  pids = {'P772','P836','P1894','P3118','P3615','P3639','P3419','P7526','P2788','P7577','P7606','P7635','P7636','P7579','P7752','P7673','P7674','P7736','P7735',}
	},
	{ groupname = 'Other',
	  pids = {'P458','P587','P2037','P3112','P10557','P3479','P4344','P6228','P7721',}
	},
}

--- @param group table
local function groupIsAllowed( group )
	local ishuman = INSTANCEOF['Q5'] or INSTANCEOF['Q15632617']
	if ishuman and not group.humans_allowed then return false end

	local allowlist = group.P31_allowed_values
	if not allowlist then return true end
	for _, class in ipairs( allowlist ) do
		if INSTANCEOF[class] then return true end
	end
	return false
end

local function noImage()
	-- Wikidata classes that don't need an image
	local dontNeedImg = {
		'Q4167410',  -- disambiguation page
		'Q4167836',  -- Wikimedia category
		'Q11266439', -- Wikimedia template
		'Q14204246', -- Wikimedia project page
		'Q13406463', -- Wikimedia list article
		'Q101352',   -- family name
		'Q202444',   -- given name
		'Q12308941', -- male given name
		'Q11879590', -- female given name
		'Q3409032',  -- unisex given name
	}
	for _, class in ipairs( dontNeedImg ) do
		if INSTANCEOF[class] then return end
	end

	local hasImg
	for _, imgPid in ipairs( property_groups[1].pids ) do
		if CLAIMS[imgPid] then
			hasImg = true
			break
		end
	end
	if not hasImg then
		return '[[Category:Uses of Wikidata Infobox with no image]]'
	end
end

--- Returns string with all labels/descs/aliases for search engine optimization
local function seo()
	local out = {}

	for lang, v in pairs( ITEM.labels or {} ) do
		out[#out+1] = v.value
	end

	for lang, v in pairs( ITEM.descriptions or {} ) do
		out[#out+1] = v.value
	end

	for lang, v in pairs( ITEM.aliases or {} ) do
		for _, w in ipairs( v ) do
			out[#out+1] = w.value
		end
	end

	return table.concat( out, '; ' )
end

-- wikiprojects that are not Wikipedia despite their IDs ending with 'wiki'
local excludedProjects = {
	wikidatawiki = true, commonswiki   = true, specieswiki   = true,
	metawiki     = true, mediawikiwiki = true, outreachwiki  = true,
	sourceswiki  = true, wikimaniawiki = true, incubatorwiki = true,
	akwiki       = true, foundationwiki = true, wikifunctionswiki = true,
}

-- Returns interwiki link if site is Wikipedia
local function interwikilink( site, title )
	if site:sub(-4) == 'wiki' and not excludedProjects[site] then
		local iwprefix = site:sub(1, -5):gsub('_', '-') -- "zh_yuewiki" to "zh-yue"
		return string.format( '[[%s:%s]]', iwprefix, title )
	end
end

--- Adds Wikipedia sitelinks from similar items. Example at Cat:Moore_(surname)
local function interwikis()
	local out = {}

	-- ITEM is usually P301 of connected item, so this is not redundant:
	for site, v in pairs( ITEM.sitelinks or {} ) do
		out[#out+1] = interwikilink( site, v.title )
	end

	for _, pid in ipairs{ 'P910', 'P2354', 'P1753', 'P460', 'P1420' } do -- topic's main category, has list, related list, said to be same as, taxon synonym
		for similar in iclaims( ITEM:getBestStatements(pid) ) do
			for site, v in pairs( mw.wikibase.getEntity(similar.id).sitelinks or {} ) do
				out[#out+1] = interwikilink( site, v.title )
			end
		end
	end

	return table.concat( out )
end

local charMap -- memoized
local function stripDiacritics( str )
	if not charMap then
		local from = 'ÁÀÂÄǍĂĀÃÅẠĄƏĆĊĈČÇĎĐḐḌÐÉÈĖÊËĚĔƐƎỀỂỄẾỆĒẼĘẸĠĜĞĢĤĦḤİÍÌÎÏǏĬĪĨĮỊĴĶĹĿĽĻŁḶḸṂŃŇÑŅṆŊÓÒÔÖǑŎŌÕǪỌŐØꝚŔŘŖⱤɌƦȐȒṘṚṜŚŜŠŞȘṢŤŢȚṬÚÙÛÜǓŬŪŨŮŲỤŰǗǛǙǕŴÝŶŸỸȲŹŻŽ'..
		             'ằắắáẳàẵâäǎăāãåặầẩẫấậảạąəćċĉčçḑďđḍðéèėêëěɛǝềểễếệĕēẽęẹġĝğģḩĥħḥıíìîïǐĭīĩįịĵķĺŀľļłḷḹṃńňñņṇŋơóồòôöǒŏōõǫọőøꝛŕɽřŗṛṝɍʀȑȓṙśŝšşșṣťţțṭưúùûứừüǔŭūũůųụűǘǜǚǖŵýŷÿỹȳźżž'
		local to   = 'AAAAAAAAAAAACCCCCDDDDDEEEEEEEEEEEEEEEEEEGGGGHHHIIIIIIIIIIIJKLLLLLLLMNNNNNNOOOOOOOOOOOORRRRRRRRRRRRSSSSSSTTTTUUUUUUUUUUUUUUUUWYYYYYZZZ'..
		             'aaaaaaaaaaaaaaaaaaaaaaaacccccdddddeeeeeeeeeeeeeeeeeegggghhhhiiiiiiiiiiijklllllllmnnnnnnoooooooooooooorrrrrrrrrrrrssssssttttuuuuuuuuuuuuuuuuuuuwyyyyyzzz'
		charMap = {}
		for i = 1, mw.ustring.len( from ) do
			charMap[mw.ustring.sub(from, i, i)] = mw.ustring.sub(to, i, i)
		end
		charMap['ß'] = 'ss'; charMap['ẞ'] = 'SS'
		charMap['æ'] = 'ae'; charMap['ǣ'] = 'ae'; charMap['ǽ'] = 'ae'
		charMap['Æ'] = 'AE'; charMap['Ǣ'] = 'AE'; charMap['Ǽ'] = 'AE'
		charMap['œ'] = 'oe'; charMap['Œ'] = 'OE'
		charMap['þ'] = 'th'; charMap['Þ'] = 'Th'
	end

	return (string.gsub( str, '[^\128-\191][\128-\191]*', charMap ))
end

local function humannames( out )
	local surname    = ITEM:formatPropertyValues('P734').value:gsub(',.*', '')
	local givennames = ITEM:formatPropertyValues('P735').value:gsub(', ', ' ')
	local spanish2nd = ITEM:formatPropertyValues('P1950').value:gsub(',.*', '')

	if config.trackingcats then
		if surname == '' then
			out[#out+1] = '[[Category:Uses of Wikidata Infobox with no family name]]'
		end
		if givennames == '' then
			out[#out+1] = '[[Category:Uses of Wikidata Infobox with no given name]]'
		end
	end

	if config.autocat then
		for _, pid in ipairs{ 'P734', 'P1950', 'P9139' } do
			for name in iclaims( ITEM:getBestStatements(pid) ) do
				local sitelink = getCommonsLink( name.id )
				if sitelink and sitelink:sub(1,9) == 'Category:' then
					if givennames == '' then
						out[#out+1] = string.format('[[%s]]', sitelink)
					else
						out[#out+1] = string.format('[[%s|%s]]', sitelink, stripDiacritics(givennames))
					end
				else
					name = mw.wikibase.getLabelByLang( name.id, 'en' )
					if givennames == '' then
						out[#out+1] = name and string.format('[[Category:%s (surname)]][[Category:Uses of Wikidata Infobox with no surname sitelink]]', name)
					else
						out[#out+1] = name and string.format('[[Category:%s (surname)|%s]][[Category:Uses of Wikidata Infobox with no surname sitelink]]', name, stripDiacritics(givennames))
					end
				end
			end
		end

		for name in iclaims( ITEM:getBestStatements('P735') ) do
			name = mw.wikibase.getLabelByLang( name.id, 'en' )
			out[#out+1] = name and string.format('[[Category:%s (given name)]]', name)
			-- no sort key needed because DEFAULTSORT starts with family name
		end
	end

	if not config.defaultsort then
		out[#out+1] = '[[Category:Uses of Wikidata Infobox with defaultsort suppressed]]'
	elseif surname ~= '' and surname ~= 'no value' and surname ~= 'some value' then
		if spanish2nd ~= '' then
			surname = surname .. ' ' .. spanish2nd
		end
		local sortkey = stripDiacritics( surname..', '..givennames )
		out[#out+1] = frame:preprocess('{{DEFAULTSORT:'..sortkey..'}}')
	end
end

--- @param pid "P569"|"P570"
--- @param event "birth"|"death"
local function datecat( pid, event, out )
	local year = WikidataIB._getValue{ pid, qid=QID, ps=1, df='y', plaindate='adj', lang='en', maxvals=1 }
	if year and year ~= 'unknown value' then
		local cat = 'Category:' .. year .. ' ' .. event .. 's'
		if mw.title.new( cat ).exists then
			out[#out+1] = '[['..cat..']]'
		elseif config.trackingcats then
			mw.addWarning( 'Categorization under [[:'..cat..']] supressed' )
			out[#out+1] = '[[Category:Uses of Wikidata Infobox with unknown '..event..' category|'..year..']]'
		end
	end
end

local function countrycat( out )
	local exceptions = {
		Q30 = 'United States',
		Q1005 = 'Gambia',
	}
	for country in iclaims( ITEM:getBestStatements('P27') ) do
		local countryLabel = exceptions[country.id] or mw.wikibase.getLabelByLang( country.id, 'en' )
		if countryLabel then
			local sex = getSingleValue( ITEM, 'P21' )
			local sexLabel = sex and ({
					Q6581097  = 'Men',
					Q2449503  = 'Men',
					Q6581072  = 'Women',
					Q1052281  = 'Women',
			})[sex.id]

			if sexLabel then
				local cat1 = 'Category:'..sexLabel..' of the '..countryLabel..' by name'
				local cat2 = 'Category:'..sexLabel..' of '..countryLabel..' by name'
				if mw.title.new( cat1 ).exists then
					out[#out+1] = '[['..cat1..']]'
				elseif mw.title.new( cat2 ).exists then
					out[#out+1] = '[['..cat2..']]'
				elseif config.trackingcats then
					mw.addWarning( 'Categorization under [[:'..cat2..']] supressed' )
					out[#out+1] = '[[Category:Uses of Wikidata Infobox with unknown country category|'..countryLabel..']]'
				end
			end
		end
	end
end

local function autocat( out, pid, dict )
	for _, claim in ipairs( ITEM:getAllStatements(pid) ) do
		if claim.rank ~= "deprecated" then
			local dv = claim.mainsnak.datavalue
			local cat = dict[dv and dv.value.id]
			out[#out+1] = cat and '[[Category:'..cat..']]'
		end
	end
end

local function metadata()
	local out = {}

	if config.trackingcats then
		out[#out+1] = noImage()
		if not (CLAIMS['P31'] or CLAIMS['P279']) then
			out[#out+1] = '[[Category:Uses of Wikidata Infobox with no instance of]]'
		end
		if INSTANCEOF['Q5'] and not CLAIMS['P569'] then
			out[#out+1] = '[[Category:Uses of Wikidata Infobox with no year of birth]]'
		elseif INSTANCEOF['Q4167836'] and not (CLAIMS['P301'] or CLAIMS['P971']) then
			out[#out+1] = '[[Category:Uses of Wikidata Infobox with no topic]]'
		end
	end

	out[#out+1] = '<div style="display:none"><nowiki>'..seo()..'</nowiki></div>'

	-- Add interwiki links from related items, inspired by Module:Interwiki
	if config.interwiki and mw.title.getCurrentTitle().namespace == 14 then
		out[#out+1] = interwikis()
	end

	if config.autocat then
		for pid, cat in pairs( autocats_by_id ) do
			local val = getSingleValue( ITEM, pid )
			out[#out+1] = val and string.format( '[[Category:%s| %s]]', cat, val )
		end

		out[#out+1] = CLAIMS['P757'] and '[[Category:World Heritage Sites by name]]'

		autocat( out, 'P1435', {  -- heritage designation
			Q34932610 = 'Conjuntos de Interesse Municipal in Portugal by name',
			Q28419115 = 'Conjuntos de Interesse Público in Portugal by name',
			Q54171320 = 'Monuments under study in Portugal by name',
			Q15697324 = 'Imóveis de Interesse Público in Portugal by name',
			Q11791    = 'Imóveis de Interesse Municipal in Portugal by name',
			Q53806418 = 'Monuments included in classified sites in Portugal by name',
			Q28423275 = 'Monumentos de Interesse Municipal in Portugal by name',
			Q22222923 = 'Monumentos de Interesse Público in Portugal by name',
			Q908411   = 'Monumentos Nacionais in Portugal by name',
			Q28419400 = 'Sítios de Interesse Municipal in Portugal by name',
			Q28419109 = 'Sítios de Interesse Público in Portugal by name',
			Q54163210 = 'Pending classification monuments in Portugal by name',
		})


		autocat( out, 'P31', {  -- instance of
			Q235670   = 'Common years starting and ending on Sunday',
			Q235673   = 'Common years starting and ending on Saturday',
			Q235676   = 'Common years starting and ending on Wednesday',
			Q235680   = 'Common years starting and ending on Friday',
			Q235684   = 'Common years starting and ending on Tuesday',
			Q235687   = 'Common years starting and ending on Monday',
			Q235690   = 'Common years starting and ending on Thursday',
			Q217041   = 'Leap years starting on Sunday and ending on Monday',
			Q217026   = 'Leap years starting on Saturday and ending on Sunday',
			Q217015   = 'Leap years starting on Wednesday and ending on Thursday',
			Q217036   = 'Leap years starting on Friday and ending on Saturday',
			Q217034   = 'Leap years starting on Tuesday and ending on Wednesday',
			Q217024   = 'Leap years starting on Monday and ending on Tuesday',
			Q217019   = 'Leap years starting on Thursday and ending on Friday',
			Q66010119 = 'Months starting on Monday',
			Q66010126 = 'Months starting on Tuesday',
			Q66010132 = 'Months starting on Wednesday',
			Q66010139 = 'Months starting on Thursday',
			Q66010148 = 'Months starting on Friday',
			Q66010153 = 'Months starting on Saturday',
			Q66010158 = 'Months starting on Sunday',
			Q3305213  = 'Individual painting categories',
		})

		if INSTANCEOF['Q5'] and mw.title.getCurrentTitle().namespace == 14 then
			humannames( out )
			datecat( 'P569', 'birth', out )
			datecat( 'P570', 'death', out )
			countrycat( out )

			autocat( out, 'P21', {  -- sex or gender
				Q6581097  = 'Men by name',
				Q6581072  = 'Women by name',
				Q1052281  = 'LGBT people by name]][[Category:Women by name',
				Q2449503  = 'LGBT people by name]][[Category:Men by name',
				Q48270    = 'Non-binary people by name',
				Q12964198 = 'LGBT people by name', -- genderqueer
				Q1097630  = 'LGBT people by name', -- intersex
				Q18116794 = 'LGBT people by name', -- genderfluid
				Q505371   = 'LGBT people by name', -- agender
			})

			autocat( out, 'P509', {  -- cause of death
				Q2840     = "Deaths from influenza",
				Q8277     = "Deaths from multiple sclerosis",
				Q9687     = "Deaths from road accidents",
				Q11081    = "Deaths from Alzheimer's disease",
				Q11085    = "Deaths from Parkinson's disease",
				-- Q12078    = "Deaths from cancer", -- too unspecific
				Q12090    = "Deaths from cholera",
				Q12152    = "Deaths from myocardial infarction",
				Q12156    = "Deaths from malaria",
				Q12192    = "Deaths from pneumonia",
				Q12199    = "Deaths from AIDS",
				Q12202    = "Deaths from stroke",
				Q12204    = "Deaths from tuberculosis",
				Q12206    = "Deaths from diabetes",
				Q12214    = "Deaths from smallpox",
				Q12796    = "Deaths by gunshot",
				Q29496    = "Deaths from leukemia",
				Q36956    = "Deaths from leprosy",
				Q40867    = "Deaths by poisoning",
				Q41083    = "Deaths from syphilis",
				Q41571    = "Deaths from epilepsy",
				Q47790    = "Deaths from tetanus",
				Q47912    = "Deaths from lung cancer",
				Q48143    = "Deaths from meningitis",
				Q83030    = "Deaths from dementia",
				Q83319    = "Deaths from typhoid fever",
				Q128015   = "People executed by guillotine",
				Q128581   = "Deaths from breast cancer",
				Q131742   = "Deaths from hepatitis",
				Q133462   = "People who committed seppuku",
				Q133780   = "Deaths from plague (disease)",
				Q134649   = "Deaths from diphtheria",
				Q147778   = "Deaths from cirrhosis",
				Q152234   = "Deaths from edema",
				Q160105   = "Deaths from cervical cancer",
				Q160649   = "Deaths from typhus",
				Q172341   = "Deaths from ovarian cancer",
				Q175111   = "Death by hanging",
				Q178275   = "Deaths from Spanish flu",
				Q180614   = "Deaths from melanoma",
				Q181257   = "Deaths from prostate cancer",
				Q181754   = "Deaths from heart failure",
				Q183134   = "Deaths from sepsis",
				Q188605   = "Deaths from emphysema",
				Q188874   = "Deaths from colorectal cancer",
				Q189389   = "Deaths from aneurysm",
				Q189588   = "Deaths from stomach cancer",
				Q190564   = "Deaths from Huntington's disease",
				Q190805   = "Deaths from diseases and disorders of the heart",
				Q192102   = "Deaths from skin cancer",
				Q193840   = "Asphyxia",
				Q199804   = "Deaths from chronic obstructive pulmonary disease",
				Q200779   = "Deaths from genetic diseases and disorders",
				Q202837   = "Deaths from cardiac arrest",
				Q204933   = "People executed by decapitation",
				Q206901   = "Deaths from amyotrophic lateral sclerosis",
				Q208414   = "Deaths from lymphoma",
				Q210392   = "Military people killed in action",
				Q212961   = "Deaths from pancreatic cancer",
				Q220570   = "Deaths from pulmonary embolism",
				Q223102   = "Deaths from peritonitis",
				Q261327   = "Deaths from thrombosis",
				Q275466   = "Deaths from embolism",
				Q372701   = "Deaths from esophageal cancer",
				Q389735   = "Deaths from diseases and disorders of the cardiovascular system",
				Q401402   = "Deaths from nephritis",
				Q468455   = "People executed by burning",
				Q476921   = "Deaths from kidney failure",
				Q504775   = "Deaths from bladder cancer",
				Q506616   = "Deaths from drowning",
				Q621076   = "Self-immolation",
				Q623031   = "Deaths from liver cancer",
				Q707774   = "Deaths from coronary thrombosis",
				Q744913   = "Victims of aviation accidents or incidents",
				Q767485   = "Deaths from respiratory failure",
				Q809831   = "BASE jumping deaths",
				Q826522   = "Deaths from thyroid cancer",
				Q847583   = "Deaths from cardiomyopathy",
				Q852423   = "Deaths from laryngeal cancer",
				Q857667   = "Deaths from pulmonary edema",
				Q929737   = "Deaths from diseases and disorders of the liver",
				Q949302   = "Deaths from diseases and disorders of the skin",
				Q958797   = "Deaths from scleroderma",
				Q970208   = "Deaths from liver failure",
				Q977787   = "Deaths from gallbladder cancer",
				Q1036696  = "Deaths from hypothermia",
				Q1054718  = "Deaths from diseases and disorders of the kidneys",
				Q1193870  = "Deaths from multiple organ failure",
				Q1198391  = "Deaths from intracranial aneurysm",
				Q1209744  = "Deaths from uterine cancer",
				Q1368943  = "Deaths from cerebral hemorrhage",
				Q1649580  = "Deaths from organ failure",
				Q1963588  = "Deaths from diseases and disorders of the blood",
				Q2140674  = "Deaths by gunshot",
				Q2300099  = "Deaths from diseases and disorders of the digestive system",
				Q2509220  = "Deaths from blood cancer",
				Q2661443  = "Deaths from diseases and disorders of the endocrine system",
				Q2967712  = "Deaths by horse-riding accident",
				Q3010352  = "Deaths from diseases and disorders of the cerebrovascular system",
				Q3242950  = "Deaths from kidney cancer",
				Q3286546  = "Deaths from diseases and disorders of the respiratory system",
				Q3339235  = "Deaths from diseases and disorders of the nervous system",
				Q3392853  = "Deaths from diseases and disorders of the lungs",
				Q3505252  = "Deaths from drug overdose",
				-- Q3966286  = "Deaths from executions", -- too unspecific
				Q4941552  = "Deaths from diseases and disorders of the skeletal system",
				Q5526839  = "Deaths from gastrointestinal cancer",
				Q7130407  = "Deaths from diseases and disorders of the pancreas",
				Q7258523  = "Deaths in childbirth",
				Q7692360  = "Deaths from volcanic eruptions",
				Q7900883  = "Deaths from diseases and disorders of the genitourinary system",
				Q8084905  = "Deaths from autoimmune diseases and disorders",
				Q9303627  = "Deaths from brain cancer",
				Q14467705 = "Deaths from surgical complications",
				Q15747939 = "People executed by shooting",
				Q18123741 = "Deaths from infectious diseases and disorders",
				Q18554919 = "Deaths from bone cancer",
				Q19403959 = "Victims of rail transport accidents or incidents",
				Q55790434 = "Deaths from oral cancer",
				-- Q84263196 = "Deaths from COVID-19", -- has a subcategory for every country
			})

			out[#out+1] = '[[Category:People by name]]'
			out[#out+1] = CLAIMS['P570'] and '[[Category:Deceased people by name]]'
			out[#out+1] = WikidataIB.getAwardCat{ args = {qid=QID, fwd='ALL', osd=config.osd, noicon='yes'} }

			if not CLAIMS['P570'] then
				-- This person has no death date, but are they really alive?
				local birth = getSingleValue( ITEM, 'P569' )
				local year = tonumber( birth and birth.time:gsub('-.*', '') )
				if year and os.date('%Y') - year < 100 then
					out[#out+1] = '[[Category:Living people]]'
				end
			end
		end
	end

	return table.concat( out )
end

--- @return string|nil
local function getImage( pid )
	local claims = ITEM:getBestStatements( pid )
	local claim = getClaimByLang( claims, MYLANG ) or claims[1]
	local ms = claim and claim.mainsnak
	local file = ms and ms.datavalue and ms.datavalue.value

	if file then
		local panoramalink = (pid == 'P4640') and '|link=https://panoviewer.toolforge.org/#'..mw.uri.encode(file, 'WIKI') or ''
		local img = '<span class="wpImageAnnotatorControl wpImageAnnotatorCaptionOff">[[File:'..file..'|'..config.imagesize..panoramalink..']]</span>' -- equivalent to {{ImageNoteControl | caption=off | type=inline}}

		local medialegends = claim.qualifiers and claim.qualifiers['P2096']
		if medialegends then
			return img .. '<div>'..extractMonolingualText( medialegends )..'</div>'
		else
			return img -- no image caption
		end
	end
end

--- Returns images and sitelinks
--- @param uploadlink? boolean: Whether to show the "Upload media" link
local function header( uploadlink )
	local imgs = {}
	for _, imgPid in ipairs( property_groups[1].pids ) do
		local formatted_img = getImage(imgPid)
		imgs[#imgs+1] = formatted_img and { imgPid, formatted_img }
	end

	local switcherContainer = mw.html.create( 'div' )
	switcherContainer:addClass( 'switcher-container' )

	-- Only show switching labels if we have more than one image to show
	if #imgs > 1 then
		for _, img in ipairs( imgs ) do
			switcherContainer:tag( 'div' )
				:addClass( 'center' )
				:node( img[2] )
				:tag( 'span' )
					:attr{ class = "switcher-label", style = "display:none" }
					:node( '&nbsp;' .. getLabel(img[1]) .. '&nbsp;' )
		end
	elseif #imgs == 1 then
		switcherContainer:tag( 'div' )
			:addClass( 'center' )
			:node( imgs[1][2] )
	end

	local images = mw.html.create( 'tr' )
	images:tag( 'td' )
		:attr{ colspan=2, class="wdinfo_nomobile" }
		:css( 'text-align', 'center' )
		:tag( 'div' )
			:node( ITEM:getDescription() or '')
			:done()
		:node( switcherContainer )

	local out = {}

	if INSTANCEOF['Q4167410'] or INSTANCEOF['Q15407973'] then -- disambiguation page/category
		if config.trackingcats then
			out[1] = '[[Category:Uses of Wikidata Infobox for disambig pages]]'
		end
	elseif uploadlink then
		local url = tostring(mw.uri.fullUrl('Special:UploadWizard', {
			categories = mw.title.getCurrentTitle().text
		}))
		local text = mw.message.new('Cx-contributions-upload'):inLanguage(MYLANG):plain()
		out[1] = '<tr><td colspan=2 style="text-align:center"><b>['..url..' '..text..']</b></td></tr>'
	end

	local sitelinks = ITEM.sitelinks
	if config.sitelinks and sitelinks then
		out[#out+1] = '<tr><td colspan=2 style="text-align:center; font-weight:bold">'
		local langId = databaseId(MYLANG)
		local langprefix = langId:gsub('_', '-')

		local wikis = {
			-- wikiId,       prefix     logo,                qid,      multilang
			{ 'wiki',        '',        'Wikipedia-logo-v2', 'Q52',    false },
			{ 'wikiquote',   'q',       'Wikiquote-logo',    'Q369',   false },
			{ 'wikisource',  's',       'Wikisource-logo',   'Q263',   false },
			{ 'wikibooks',   'b',       'Wikibooks-logo',    'Q367',   false },
			{ 'wikinews',    'n',       'Wikinews-logo',     'Q964',   false },
			{ 'wikiversity', 'v',       'Wikiversity-logo',  'Q370',   false },
			{ 'specieswiki', 'species', 'Wikispecies-logo',  'Q13679', true  },
			{ 'wikivoyage',  'voy',     'Wikivoyage-logo',   'Q373',   false },
		}

		for _, v in ipairs( wikis ) do
			local wikiId, prefix, logo, qid, multilang = unpack( v )
			logo = '[[File:'..logo..'.svg|16x16px|alt=|link=]]&nbsp;'
			if multilang then
				local sitelink = sitelinks[wikiId]
				if sitelink then
					out[#out+1] = '<div>'..logo..'[['..prefix..':'..sitelink.title..'|'..getLabel(qid)..']]</div>'
				end
			else
				local sitelink = sitelinks[langId .. wikiId]
				if sitelink then
					out[#out+1] = '<div>'..logo..'[['..prefix..':'..langprefix..':'..sitelink.title..'|'..getLabel(qid)..']]</div>'
				end
			end
		end
		out[#out+1] = '</td></tr>'
	end

	return tostring( images ) .. table.concat( out )
end

--- Returns "Edit at Wikidata" pencil
local function pencil()
	local msg, lang = i18n( 'editlink-alttext', FALLBACKLANGS )
	local out = mw.html.create( 'tr' )
	out
		:addClass( "wdinfo_nomobile" )
		:tag( 'td' )
			:css( 'text-align', 'right' )
			:attr{ lang = lang, colspan = 2 }
			:node( string.format('[[File:Blue pencil.svg|15px|link=d:%s|%s]]', QID, msg) )
	return tostring( out )
end

--- Evaluates all non-image property groups and adds generated HTML rows to
--- the table given as argument.
local function getBodyContent( t )
	for i, group in ipairs( property_groups ) do
		if i > 1 and groupIsAllowed( group ) then
			for _, pid in ipairs( group.pids ) do
				if CLAIMS[pid] or group.bypass_property_exists_check then
					local x = property_logic[pid] or group.logic or defaultFunc
					if type(x) == 'function' then
						t[#t+1] = x( pid )
					else -- type(x) == 'table'
						t[#t+1] = defaultFunc( pid, x )
					end
				end
			end
		end
	end
end

--- Returns the infobox's main content
local function body()
	if not CLAIMS then return '' end

	local out = {}
	getBodyContent( out )

	-- If category combines at most 2 topics, show subinfoboxes for those topics.
	-- See Category:Uses_of_Wikidata_Infobox_with_subinfoboxes
	local topics = ITEM:getBestStatements( 'P971' )
	if not topics or #topics > 2 then return table.concat( out ) end

	-- country (Q6256), continent (Q5107), sovereign state (Q3624078), ocean (Q9430)
	local geoEntities = { 'Q6256', 'Q5107', 'Q3624078', 'Q9430' }

	-- The loop below modifies these variables and restores them afterwards
	local qid, item, claims, istaxon, instanceof = QID, ITEM, CLAIMS, ISTAXON, INSTANCEOF

	local map
	for _, claim in ipairs( topics ) do
		QID = claim.mainsnak.datavalue.value.id
		ITEM = mw.wikibase.getEntity( QID )
		if not ITEM then
			out[#out+1] = '[[Category:Uses of Wikidata Infobox for deleted Wikidata items]]'
			break
		end
		CLAIMS = ITEM.claims or {}
		ISTAXON = CLAIMS['P105'] or CLAIMS['P171'] or CLAIMS['P225'] or CLAIMS['P1843']

		INSTANCEOF = {}
		for class in iclaims( ITEM:getBestStatements('P31') ) do
			INSTANCEOF[class.id] = true
		end

		local skip
		for _, geoEnt in ipairs( geoEntities ) do
			if INSTANCEOF[geoEnt] then
				skip = true
				map = getCoordinates( 'P625' )
				break
			end
		end

		-- Skip if topic is a calendar year (Q3186692) or decade (Q39911)
		skip = skip or INSTANCEOF['Q3186692'] or INSTANCEOF['Q39911']

		if not skip and #getBestStatements(QID, 'P279') == 0 then -- subclass of
			if config.trackingcats then
				out[#out+1] = '[[Category:Uses of Wikidata Infobox with subinfoboxes]]'
			end
			out[#out+1] = '<tr><th colspan=2>'..(ITEM:getLabel() or QID)..'</th></tr>'
			out[#out+1] = header( false )
			getBodyContent( out )
			out[#out+1] = pencil()
		end
	end
	out[#out+1] = map

	QID, ITEM, CLAIMS, ISTAXON, INSTANCEOF = qid, item, claims, istaxon, instanceof
	return table.concat( out )
end

local function authoritycontrol()
	if not config.authoritycontrol then return '' end

	local ids = {}
	for _, group in ipairs( externalIDs ) do
		for _, pid in ipairs( group.pids ) do
			if CLAIMS[pid] then
				local icon = getSingleValue( pid, 'P2910' )
				icon = icon and '[[File:'..icon..'|18px|alt=|link=]] ' or ''
				local fmtSt = ITEM:formatStatements( pid )
				if fmtSt.value ~= '' then
					ids[#ids+1] = icon .. fmtSt.label .. ': ' .. fmtSt.value
				end
			end
		end
	end

	local wdlogo = '[[File:Wikidata-logo.svg|20px|alt='..getLabel('Q2013')..'|link=d:'..QID..']]'
	return table.concat{
		'<tr><th style="background: #cfe3ff">',
			LANG:ucfirst( getLabel('Q36524') ),
		'</th></tr>',

		'<tr><td style="text-align: center;">',
			'<div style="overflow-wrap: break-word; font-size: smaller">',
				wdlogo..'&nbsp;[[d:'..QID..'|'..QID..']]<br>',
				'<span class="wdinfo_nomobile">',
					table.concat(ids, '<br>'),
				'</span>',
			'</div>',
		'</td></tr>',
	}
end

local function helperlinks()
	if not config.helperlinks then return '' end

	local hl = {}
	local title = mw.title.getCurrentTitle()
	local pagename = title.text
	local pagenamee = mw.uri.encode(pagename, 'WIKI')

	local coords = getSingleValue( ITEM, 'P625' )
	local otherplanet = coords and coords.globe ~= 'http://www.wikidata.org/entity/Q2'

	hl[#hl+1] = '[https://reasonator.toolforge.org/?q='..QID..' '..getLabel('Q20155952')..']'
	hl[#hl+1] = '[[toolforge:scholia/'..QID..'|'..getLabel('Q45340488')..']]'
	hl[#hl+1] = '[https://wikidocumentaries-demo.wmcloud.org/'..QID..' '..getLabel('Q85947706')..']'

	if title.namespace == 14 then
		hl[#hl+1] = '[https://petscan.wmflabs.org/?language=commons&categories='..pagenamee..'&project=wikimedia&ns%5B6%5D=1 '..getLabel('Q23665536')..']'
		hl[#hl+1] = '[https://glamtools.toolforge.org/glamorgan.html?&category='..pagenamee..'&depth=1&month=last '..getLabel('Q12483')..']'
		if not otherplanet then
			hl[#hl+1] = '[https://wikimap.toolforge.org/?cat='..pagenamee..'&subcats=true&subcatdepth=1&cluster=true '..getLabel('Q99232292')..']'
			hl[#hl+1] = '[https://locator-tool.toolforge.org/#/geolocate?category='..pagenamee..' '..getLabel('Q66498380')..']'
		end
	end

	hl[#hl+1] = '[https://kmlexport.toolforge.org/?project=commons&article='..mw.uri.encode(title.prefixedText)..' '..getLabel('P3096')..']'

	if coords and not otherplanet then
		hl[#hl+1] = '[https://wikishootme.toolforge.org/#q='..QID..'&main_commons_category='..pagenamee..' '..getLabel('Q26964791')..']'
		hl[#hl+1] = '[https://overpass-api.de/api/interpreter?data='..mw.uri.encode('[out:custom];rel[wikidata='..QID..'];if(count(relations)==0){way[wikidata='..QID..'];if(count(ways)==0){node[wikidata='..QID..'];};};out 1;', 'PATH')..' '..getLabel('Q936')..']'
	end

	for i, v in ipairs( hl ) do
		hl[i] = '<span style="white-space:nowrap">' .. v .. '</span>'
	end

	hl[#hl+1] = '[[Special:Search/haswbstatement:P180='..QID..'|'..i18n('search-depicted', FALLBACKLANGS)..']]'
	hl[#hl+1] = ISTAXON and '[https://commons-query.wikimedia.org/#%23defaultView%3AImageGrid%0ASELECT%20%3Ffile%20%3Fimage%0AWITH%20%7B%0A%20%20SELECT%20%3Fitem%20WHERE%20%7B%0A%20%20%20%20SERVICE%20%3Chttps%3A%2F%2Fquery.wikidata.org%2Fsparql%3E%20%7B%0A%20%20%20%20%20%20%20%20%3Fitem%20wdt%3AP171%2Fwdt%3AP171%2a%20wd%3A'..QID..'.%0A%20%20%20%20%7D%20%0A%20%20%7D%0A%7D%20AS%20%25get_items%0AWHERE%20%7B%0A%20%20INCLUDE%20%25get_items%0A%20%20%3Ffile%20wdt%3AP180%20%3Fitem%20.%0A%20%20%3Ffile%20schema%3AcontentUrl%20%3Furl%20.%0A%20%20BIND%28IRI%28CONCAT%28%22http%3A%2F%2Fcommons.wikimedia.org%2Fwiki%2FSpecial%3AFilePath%2F%22%2C%20wikibase%3AdecodeUri%28SUBSTR%28STR%28%3Furl%29%2C53%29%29%29%29%20AS%20%3Fimage%29%0A%7D '..i18n('taxon-depicted', FALLBACKLANGS)..']'

	return table.concat{
		'<tr class="wdinfo_nomobile">',
			'<td colspan=2 style="text-align: center"><small>',
				'<div class="hlist hlist-separated"><ul>',
					'<li>' .. table.concat(hl, '</li><li>') .. '</li>',
				'</ul></div>',
			'</small></td>',
		'</tr>',
	}
end

local function footer()
	return (config.authoritycontrol or config.helperlinks) and table.concat{
		'<tr><td colspan=2>',
			'<table style="width:100%" id="wdinfo_ac" class="mw-collapsible">',
				authoritycontrol(),
				helperlinks(),
			'</table>',
		'</td></tr>',
	} or ''
end

--- @param eid string: Wikidata entity ID starting with Q or P
local function entityLink( eid )
	local label = getLabel( eid, true )
	local ns = ( eid:sub(1, 1) == 'P' ) and 'Property:' or ''
	return '[[d:'..ns..eid..'|'..label..' <small>('..eid..')</small>]]'
end

--- Generates [[Template:Wikidata Infobox/doc/properties]]
function p.doc()
	local out = {}
	for _, group in ipairs( property_groups ) do
		out[#out+1] = '<h2>' .. group.groupname .. '</h2>'
		if group.comment then
			out[#out+1] = frame:preprocess( group.comment )
		end

		if group.P31_allowed_values then
			local classes = {}
			for _, class in ipairs( group.P31_allowed_values ) do
				classes[#classes+1] = entityLink( class )
			end
			out[#out+1] = 'This group is only shown if the connected Wikidata item is an instance of ' .. table.concat(classes, ' or ') .. '.'
		elseif group.humans_allowed then
			out[#out+1] = 'This group is always shown.'
		end

		local props = {}
		for _, pid in ipairs( group.pids ) do
			props[#props+1] = entityLink( pid )
		end
		out[#out+1] = table.concat( props, ' • ' )
	end

	-- authority control
	out[#out+1] = '<h2>'..getLabel('Q36524')..'</h2>'
	out[#out+1] = 'This group is always shown.'
	for _, group in ipairs( externalIDs ) do
		out[#out+1] = '<h3>' .. group.groupname .. '</h3>'
		local props = {}
		for _, pid in ipairs( group.pids ) do
			props[#props+1] = entityLink( pid )
		end
		out[#out+1] = table.concat( props, ' • ' )
	end

	return table.concat( out, '\n\n' )
end

local function configure( t )
	config.defaultsort      = t['defaultsort']           == 'y'
	config.interwiki        = t['interwiki']             == 'yes'
	config.autocat          = t['autocat']               == 'yes'
	config.trackingcats     = t['trackingcats']          == 'yes'
	config.uploadlink       = t['conf_upload']           == 'yes'
	config.sitelinks        = t['conf_sitelinks']        == 'yes'
	config.authoritycontrol = t['conf_authoritycontrol'] == 'yes'
	config.helperlinks      = t['conf_helperlinks']      == 'yes'

	if t['conf_coordtemplate'] then config.coordtemplate = tonumber( t['conf_coordtemplate'] ) end
	if t['conf_mapwidth'] then config.mapwidth = t['conf_mapwidth'] end
	if t['conf_mapheight'] then config.mapheight = t['conf_mapheight'] end
	if t['conf_imagesize'] then config.imagesize = t['conf_imagesize'] end

	if t['spf'] then config.spf = t['spf'] end
	if t['fwd'] then config.fwd = t['fwd'] end
	if t['osd'] then config.osd = t['osd'] end
	if t['noicon'] then config.noicon = t['noicon'] end
end

function p.main( frame )
	MYLANG = frame:callParserFunction( 'int', 'lang' ) or "en"
	LANG = mw.language.new( MYLANG )
	FALLBACKLANGS = { MYLANG, unpack(mw.language.getFallbacksFor(MYLANG)) }
	QID = frame.args[1]
	ITEM = mw.wikibase.getEntity( QID )
	if not ITEM then
		return '[[Category:Uses of Wikidata Infobox for deleted Wikidata items]]'
	end
	CLAIMS = ITEM.claims
	if not CLAIMS then
		local msg = i18n('noclaims', FALLBACKLANGS):gsub('$1', '[[d:'..QID..'|'..QID..']]' )
		return '[[Category:Uses of Wikidata Infobox with no claims]]<table id="wdinfobox" class="fileinfotpl-type-information vevent infobox mw-content-'..LANG:getDir()..'"><tr><td><strong class="error">'..msg..'</strong></td></tr>'
	end

	-- identifying a taxon by checking whether it has a taxon property is faster than checking whether its P31 value is a subclass of taxon
	ISTAXON = CLAIMS['P105'] or CLAIMS['P171'] or CLAIMS['P225'] or CLAIMS['P1843']
	local parentframe = frame:getParent()
	if parentframe then
		configure( parentframe.args )
	end

	for class in iclaims( ITEM:getBestStatements('P31') ) do
		INSTANCEOF[class.id] = true
	end

	local out = {
		metadata(),
		'<table id="wdinfobox" class="fileinfotpl-type-information vevent infobox mw-collapsible mw-content-'..LANG:getDir()..'">',
			'<caption class="fn org" id="wdinfoboxcaption">',
				'<b>' .. (ITEM:getLabel() or QID) .. '&nbsp;</b>',
			'</caption>',
			header( config.uploadlink ),
			body(),
			footer(),
			pencil(),
		'</table>',
	}
	if config.trackingcats and os.clock() > 2.5 then -- longer than 2.5 seconds
		out[#out+1] = '[[Category:Uses of Wikidata Infobox with bad performance]]'
	end
	return table.concat( out )
end

function p.debug( qid )
	frame.args = { qid or 'Q42' }
	return p.main( frame )
end

return p

-- Credits:
-- Original authors: Mike Peel with contributions by Jura1
-- 2022 rewrite: LennardHofmann