Module:Sandbox/Module:Wikidata Infobox

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Lua
CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules

Documentation for this module may be created at Module:Sandbox/Module:Wikidata Infobox/doc

Code

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

local config = {
	-- toggle/customize infobox features:
	defaultsort = true,
	autocat = true,
	trackingcats = true,
	uploadlink = true,
	sitelinks = true,
	authoritycontrol = true,
	helperlinks = true,
	coordhelperlinks = true,
	coordtemplate = 1, -- 0 = none, 1 = Geohack, 2 = Coord
	mapwidth = 250,
	mapheight = 250,
	imagesize = '230x500px', -- CHANGE: was 250px but most images used hardcoded 200px

	-- parameters for WikidataIB:
	spf = '',        -- suppressfields
	fwd = 'ALL',     -- fetchwikidata
	osd = 'no',      -- onlysourced
	noicon = 'yes',  -- pencil icon
	wdlinks = 'yes', -- whether 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 set by main()
local QID             -- qid of ITEM, e.g. 'Q42'
local CLAIMS          -- ITEM.claims
local INSTANCEOF = {} -- Hash set of ITEM's best "instance of" values
local MYLANG          -- user's languge code set by main()
local LANG            -- language object of user's language
local FALLBACKLANGS   -- list containing MYLANG and its fallback languages

--- 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
--- @return string
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
		-- CHANGE: ids are linked to allow editors to easily add a label
		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

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

	local maincat = getSingleValue( qid, 'P910' ) or {} -- topic's main category
	if maincat.id then
		sitelink = getSitelink( maincat.id, 'commonswiki' )
		if sitelink then return sitelink end
	end

	local listcat = getSingleValue( qid, 'P1754' ) or {} -- category related to list
	if listcat.id then
		sitelink = getSitelink( listcat.id, 'commonswiki' )
		if sitelink then return sitelink end
	end

	local P373 = getSingleValue( qid, 'P373' )
	if P373 then
		return 'Category:' .. P373
	else
		return nil
	end
end

--- Returns sitelink to Commons as wikilink or the label of the given Q-item
--- @param qid string
--- @return string
local function getLinkOrLabel( qid )
	local sitelink = getCommonsLink( 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 value 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"
local function getValueByLang( claims, langcode )
	for _, claim in ipairs( claims or {} ) do
		for _, qual in ipairs( claim.qualifiers and claim.qualifiers['P407'] or {} ) do
			if getSingleValue( qual.datavalue.value.id, 'P424' ) == langcode then
				return claim.mainsnak.datavalue and claim.mainsnak.datavalue.value
			end
		end
	end
end

--- Given snaks of datatype monolingualtext, extracts string in MYLANG
--- (or fallback language). Returns random string if no string is in MYLANG.
--- @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.datavalue.value
		monotext[v.language] = v.text
	end

	for _, lang in ipairs( FALLBACKLANGS ) do
		if monotext[lang] then return monotext[lang], true end
	end
	local _, v = next(monotext)
	return v, false
end

--- Removes parentheses from end of input
--- @param title string|nil
local function suppressDisambiguation( title )
	if not title then return end
	return title:gsub( '%s+%b()$', '', 1 )
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( cell1, cell2 )
	return '<tr><th class="wikidatainfobox-lcell" style="text-align:left" colspan="2">'..cell1..'</th></tr><tr><td style="vertical-align:top" colspan="2">'..cell2..'</td></tr>'
end

--- Returns a string containing a single table row
local function format1rowline( trqid, cell1, cell2 )
	return '<tr id="'..trqid..'"><th class="wikidatainfobox-lcell">'..cell1..'</th><td style="vertical-align:top">'..cell2..'</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

--- Wrapper around formatLine for audio files. Returns nil if audiofile is empty.
--- @param pid string
--- @param audiofile string|nil: Commons filename without "File:" prefix
local function formatAudio( pid, audiofile )
	if audiofile == nil or audiofile == '' then return end
	return formatLine( pid, '[[File:' .. audiofile .. '|100px]]', false )
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]]
	}

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

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

--- 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 {}
	return WikidataIB._getValue{
		pid,
		name = pid,
		qid = args.qid or QID,
		linked = args.linked,
		wdlinks = 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 = args.collapse or config.collapse,
		spf = config.spf,
		fwd = config.fwd,
		osd = config.osd,
		rank = 'best',
		noicon = config.noicon,
		list = args.list or 'Unbulleted list',
		sep = args.sep,
		unitabbr = args.unitabbr,
	}
end

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

local function defaultFuncMobile( pid )
	return defaultFunc( pid, {}, true )
end

local function getAudio( pid )
	return formatAudio( pid, getSingleValue(ITEM, pid) )
end

--- Return audio file in user's language from Wikidata
local function getAudioByLang( pid )
	local claims = ITEM:getBestStatements(pid)
	for i = 1, #FALLBACKLANGS do
		local audiofile = getValueByLang( claims, FALLBACKLANGS[i] )
		if audiofile then
			return formatAudio( pid, audiofile )
		end
	end
end

--- expand P7383 value in <hiero></hiero> tags
local function getHieroglyphs()
	local rows = {}
	for _, v in ipairs( CLAIMS['P7383'] ) do
		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( 'Name', 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 = {
		getValue( pid, birthdeath_args ),                    -- date
		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 = {
		getValue( pid, birthdeath_args ),                    -- date
		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 ) or ''
	if img ~= '' then
		local alt = LANG:ucfirst( getLabel(pid) )
		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

local function getByRegion( pid, regionPid, collapse )
	return defaultFunc( pid, {
		qid = getSingleValue( ITEM, regionPid ).id,
		linked = 'no',
		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 getUnicodeChars( pid )
	local rows = {}
	for _, v in ipairs( CLAIMS[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 id = w.datavalue.value.id
				rows[#rows+1] = format1rowline( id, getLabel(id), idv )
			end
		end
	end
	return table.concat( rows )
end

local function getCodes( pid )
	local rows = {}
	for _, v in ipairs( CLAIMS[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( CLAIMS[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 label = getLabel( qualid )
				rows[#rows+1] = format1rowline( qualid, label, '[[File:' .. idv .. '|none|35px]]' )
			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( CLAIMS[pid] or {} ) do
				local location = renderSnak( claim.mainsnak )
				if location and not set[location] then
					set[location] = true     -- we don't want duplicate values
					out[#out+1]   = location -- we want to preserve the order
				end
			end
		end
		if #out == 0 then return end
		out = table.concat( out, '</li><li>' )
		out = '<div class="plainlist"><ul><li>'..out..'</li></ul></div>'
		return formatLine( 'P276', 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 getAddress()
	local address
	if CLAIMS['P6375'] then
		address = getValue( 'P6375' ) -- street address as string
	elseif CLAIMS['P669'] then
		address = getValue( 'P669', { qual='P670, P281' } )
	end
	return formatLine( 'P6375', address, true )
end

local function getAuthors()
	if CLAIMS['P50'] or CLAIMS['P2093'] then
		local args = { list='', sep='</li><li>' }
		local authors = getValue( 'P50', args ) or ''
		local namestrings = getValue( 'P2093', args )
		if namestrings then
			authors = authors .. '</li><li>' .. namestrings
		end
		local list = '<div class="plainlist"><ul><li>'..authors..'</li></ul></div>'
		return formatLine( 'P50', list )
	end
end

--- Returns only linked values. Otherwise mostly identical to defaultFunc
local function getOnlyLinkedValues( pid )
	local out = {}
	for i, claim in ipairs( ITEM:getBestStatements(pid) or {} ) do
		local dv = claim.mainsnak.datavalue
		local qid = dv and dv.value.id
		local sitelink = qid and getCommonsLink( qid )
		if sitelink then
			local link = "[[:" .. sitelink .. "|" .. getLabel( qid, true ) .. "]]"
			local quals = claim.qualifiers
			if quals then
				quals['P1310'] = nil  -- don't show "disputed by" qualifier
				out[#out+1] = link .. ' ('..mw.wikibase.formatValues(quals)..')'
			else
				out[#out+1] = link
			end
		end
		if i == 30 then
			break -- arbitrary limit to avoid calling getCommonsLink too often
		end
	end
	if #out == 0 then return end
	out = table.concat( out, '</li><li>' )
	out = '<div class="plainlist"><ul><li>'..out..'</li></ul></div>'
	return formatLine( pid, out )
end

--- Returns common taxon name using [[Module:Wikidata4Bio]]
local function getVernacularName()
	if CLAIMS['P105'] or CLAIMS['P171'] or CLAIMS['P225'] or CLAIMS['P1843'] then -- identifying a taxon by checking whether it has a taxon property is easier than checking whether its P31 value is a subclass of taxon
		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()
	-- TODO: Sync Taxontree with sandbox
	local content = require('Module:Taxontree/sandbox').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' )
	local citation = ocomb and getSingleValue( ocomb, 'P6507' )
	if taxoname and citation then
		return formatLine( 'P1403', '<i>'..taxoname..'</i>' .. citation )
	elseif taxoname then
		return formatLine( 'P1403', '<i>'..taxoname..'</i>' )
	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

--- @param entityOrId? table|string: getEntity() or id like Q42. Defaults to ITEM
local function getCoordinates( pid, entityOrId )
	entityOrId = entityOrId or ITEM
	local coords = getSingleValue( entityOrId, 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 geojson = { -- [[mw:Help:Extension:Kartographer#GeoJSON]]
				{ type = "ExternalData",
				  service = "geoshape",
				  ids = QID,
				  properties = {
				  	['fill'] = "#999999",
				  	['stroke'] = "#636363",
				  	['stroke-width'] = 2
				  },
				},
				{ type = "Feature",
				  geometry = { type="Point", coordinates = {long, lat} },
				  properties = {
				  	['marker-size'] = "medium",
				  	['marker-color'] = "006699"
				  },
				},
			}
			local area = getSingleValue( entityOrId, 'P2046' )
			out = frame:extensionTag( 'mapframe', mw.text.jsonEncode(geojson), {
				frameless = 1,
				lang = MYLANG,
				width = config.mapwidth,
				height = config.mapheight,
				zoom = autoMapZoom( area ),
				align = 'center',
			})
			if config.trackingcats then
				out = out ..'[[Category:Uses of Wikidata Infobox with maps]]'
			end
			if config.coordtemplate == 1 then
				-- mw:Extension:GeoData and GeoHack
				local gd = frame:callParserFunction('#coordinates:primary', lat, long)
				local gh = require('Module:Coordinates')._GeoHack_link{ lat=lat, lon=long, lang=MYLANG }
				out = out .. gd .. '<small>'..gh..'</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>'
	else
		if config.trackingcats and (CLAIMS['P706'] or CLAIMS['P131']) then
			return '[[Category:Uses of Wikidata Infobox with no coordinate]]'
		end
	end
end

local function getCelestialCoordinates()
	local ra = getSingleValue( ITEM, 'P6257' )
	local de = getSingleValue( ITEM, 'P6258' )
	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 text = LANG:ucfirst( getLabel('P6257') )..' '..ra.amount..' '..ra_unit..
		     '<br>'..LANG:ucfirst( getLabel('P6258') )..' '..de.amount..' '..de_unit
		return '<tr class="wdinfo_nomobile"><td colspan=2 style="text-align:center">['..url..text..']</td></tr>'
	end
end

--- 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   = defaultFuncMobile,           -- 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
	-- P937   = { sorted='yes' },         -- work location, CHANGE: sorting unnecessary
	-- P800   = { sorted='yes' },         -- notable work, CHANGE: alphabetical sorting worse than Wikidata order
	-- P1441   = { sorted='yes' },        -- present in work, CHANGE: alphabetical sorting worse than Wikidata order
	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  = { list='', maxvals='' },     -- 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
	P6375  = getAddress,                  -- street address
	P276   = getLocation,                 -- location
	P50    = getAuthors,                  -- author
	P2789  = { qual='' },                 -- connects with
	P85    = { qual='DATES' },            -- anthem
	P953   = { qual='P407', prefix="[", postfix="]" }, -- full work at
	-- P1441  = { sorted='yes' },         -- present in work, CHANGE
	-- P1080  = { sorted='yes' },         -- from narrative universe, CHANGE
	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
	P1990  = { collapse=5 },              -- species kept
	P1923  = { collapse=5, maxvals=10 },  -- participating team
	P1346  = { collapse=5, maxvals=20 },  -- winner
	P112   = { maxvals=20 },              -- founded by
	P577   = { rank='preferred normal' }, -- See [[d:Property_talk:P577#Constraint_about_unique_best_value]]
	P140   = { 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  = getOnlyLinkedValues,         -- 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
	P7457  = getSignature,                -- creator's signature
	P625   = getCoordinates,              -- coordinate location
	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 = {'P18','P117','P8224','P1442','P1801','P2716','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'},
	},
	{ 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','P54','P108','P463','P102','P39','P101','P135','P66','P103','P97','P2962','P2522','P53','P22','P25','P3373','P40','P26','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','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,
	  -- CHANGE: P706 does not get its own infobox row anymore because
	  -- Wikidata.location can handle P706
	},
	{ groupname = 'Miscellaneous 2',
	  pids = {'P1001','P206','P5353','P4856','P9759'},
	},
	{ groupname = 'Address',
	  comment = 'If {{P|6375}} does not exist, the address will be formed from {{P|670}}, {{P|669}}, and {{P|281}}.',
	  pids = {'P6375'},
	  bypass_property_exists_check = true,
	},
	{ groupname = 'Miscellaneous 3',
	  pids = {'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 4',
	  pids = {'P655','P123','P1433','P84','P193','P170','P86','P676','P87','P61','P189','P98','P58','P110','P162','P175','P393','P291','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','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','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','P140','P1082','P2052','P2217','P2046','P2044','P2050','P2047'},
	  logic = { unitabbr='yes' },
	},
	{ groupname = 'Miscellaneous 5',
	  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 6',
	  pids = {'P155','P156','P1365','P1366','P3730','P3729'},
	},
	{ groupname = 'Elections',
	  pids = {'P991','P726','P1831','P1867','P1868','P1697','P5043','P5045','P5044'},
	},
	{ groupname = 'Miscellaneous 7',
	  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 8',
	  pids = {'P6591','P7422','P2078','P856','P7457','P625','P6257'},
	},
}
--- @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
		'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

-- TODO: not fully implemented
local function autocat()
	local out = {}

	if config.trackingcats then
		out[#out+1] = noImage()
	end

	return table.concat( out )
end

--- @return string|nil
local function getImage( pid )
	local claim = ITEM:getBestStatements( pid )[1]
	local ms = claim and claim.mainsnak
	local file = 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
		if CLAIMS[imgPid] then
			imgs[#imgs+1] = { imgPid, getImage(imgPid) }
		end
	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 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 },
		}
		-- Get sitelinks for other wikis
		for _, v in ipairs( wikis ) do
			local wikiId, prefix, logo, qid, multilang = unpack( v )
			logo = '[[File:'..logo..'.svg|18px|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

--- Loops over fallback languages. Returns translated message if the given message
--- is available in one of these languages. Returns English message otherwise.
--- @param msgKey "editlink-alttext"|"newitem"|"noid"|"search-depicted"|"search"
--- @return string message, string msgLang
local function i18n( msgKey )
	local msgLang
	for _, lang in ipairs( FALLBACKLANGS ) do
		if mw.title.makeTitle( 'Translations', 'Template:Wikidata Infobox/i18n/msg-'..msgKey..'/'..lang ).exists then
			msgLang = lang
			break
		end
	end
	msgLang = msgLang or "en"
	local message = frame:expandTemplate{
		title = 'Translations:Template:Wikidata Infobox/i18n/msg-editlink-alttext/'..msgLang
	}
	return message, msgLang
end

--- Returns "Edit at Wikidata" pencil
local function pencil()
	local msg, lang = i18n( 'editlink-alttext' )
	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

--- Returns the infobox's main content
local function properties()
	if not CLAIMS then return '' end
	local out = {}

	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
						out[#out+1] = x( pid )
					else -- type(x) == 'table'
						out[#out+1] = defaultFunc( pid, x )
					end
				end
			end
		end
	end

	-- 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, p31 = QID, ITEM, CLAIMS, INSTANCEOF

	local map
	for _, claim in ipairs( topics ) do
		QID = claim.mainsnak.datavalue.value.id

		INSTANCEOF = {}
		for _, v in ipairs( getBestStatements(QID, 'P31') ) do
			local dv = v.mainsnak.datavalue
			local id = dv and dv.value.id
			if id then INSTANCEOF[id] = true end
		end

		local skip
		for _, geoEnt in ipairs( geoEntities ) do
			if INSTANCEOF[geoEnt] then
				skip = true
				map = getCoordinates( 'P625', QID )
				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
			ITEM = mw.wikibase.getEntity( QID )
			CLAIMS = ITEM.claims

			out[#out+1] = '<tr><th colspan=2>'..(ITEM:getLabel() or QID)..'</th></tr>'
			out[#out+1] = header( false )

			for i, group in ipairs( property_groups ) do
				if i > 1 and groupIsAllowed( group ) then
					for _, pid in ipairs( group.pids ) do
						local c = CLAIMS[pid]
						if (c and #c < config.collapse) or group.bypass_property_exists_check then
							-- properties with many values are bad for performance
							local x = property_logic[pid] or group.logic or defaultFunc
							if type(x) == 'function' then
								out[#out+1] = x( pid )
							else -- type(x) == 'table'
								out[#out+1] = defaultFunc( pid, x )
							end
						end
					end
				end
			end
			out[#out+1] = pencil()
		end
	end
	out[#out+1] = map

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

local function footer()
	return '' --TODO
end

-- Get a list of all properties in the current item
function p.preloadWikidataProperties(frame)
	local qid = frame.args[1] or ''
	local proplist = ''

	local frame = mw.getCurrentFrame()
	if mw.text.trim(qid or '') ~= '' then
		local entity = mw.wikibase.getEntity(qid)
		local properties = entity:getProperties()
		for i, v in ipairs(properties) do
			proplist = proplist .. v .. ", "
		end
	end
	if proplist == '' then
		proplist = 'None'
	end
	return proplist
end

-- check if it is on the list, fork of WikidataIB's checkBlacklist function
function p.checkProplist(frame)
	local proplist = frame.args.fetchwikidata or frame.args.fwd or ""
	local fieldname = frame.args.name or ""
	if proplist ~= "" and fieldname ~= "" then
		if proplist:find(fieldname .. ",") then
			return true
		else
			return ''
		end
	else
		-- one of the fields is missing: let's call that "on the list"
		return true
	end
end

function p.ifThenShow(frame)
	if mw.text.trim(frame.args[1] or '') ~= '' then
		return (frame.args[3] or '') .. (frame.args[1] or '') .. (frame.args[4] or '')
	else
		return (frame.args[2] or '')
	end
end

-- Example call: {{#invoke:Wikidata Infobox|formatLine | P509 | {{#invoke:WikidataIB | getValue | rank=best | P509 | name=P509 | linkprefix=":" | qlinkprefix=":" | list={{{liststyle|ubl}}} | qid={{{qid|}}} | spf={{{spf|}}} | fwd={{{fwd|ALL}}} | osd={{{osd|no}}} | noicon={{{noicon|yes}}} | qual=MOST}}}}
function p.formatLine(frame)
	local pid = frame.args[1] -- wikidata property id
	local content = mw.text.trim(frame.args[2])
	local nomobile = frame.args.mobile == 'y'
	return formatLine(pid, content, nomobile)
end

--[[
convertChar returns the non-diacritic version of the supplied character.
stripDiacrits replaces words with diacritical characters with their non-diacritic equivalent.
strip_diacrits is available for export to other modules.
stringIsLike tests two words, returning true if they only differ in diacritics, false otherwise.
stringIs_like is available for export to other modules.
--]]

local function characterMap()
	-- table with characters with diacrits and their equivalent basic latin characters
	-- TODO: expand from [[w:Module:Latin]]
	local charMap_from, charMap_to
	charMap_from =  'ÁÀÂÄǍĂĀÃÅẠĄƏĆĊĈČÇĎĐḌÐÉÈĖÊËĚĔĒẼĘẸĠĜĞĢĤĦḤİÍÌÎÏǏĬĪĨĮỊĴĶĹĿĽĻŁḶḸṂŃŇÑŅṆŊÓÒÔÖǑŎŌÕǪỌŐØŔŘŖṚṜŚŜŠŞȘṢŤŢȚṬÚÙÛÜǓŬŪŨŮŲỤŰǗǛǙǕŴÝŶŸỸȲŹŻŽ'..
					'áàâäǎăāãåạąəćċĉčçďđḍðéèėêëěĕēẽęẹġĝğģĥħḥıíìîïǐĭīĩįịĵķĺŀľļłḷḹṃńňñņṇŋóòôöǒŏōõǫọőøŕřŗṛṝśŝšşșṣťţțṭúùûüǔŭūũůųụűǘǜǚǖŵýŷÿỹȳźżž'
	charMap_to   =  'AAAAAAAAAAAACCCCCDDDDEEEEEEEEEEEGGGGHHHIIIIIIIIIIIJKLLLLLLLMNNNNNNOOOOOOOOOOOORRRRRSSSSSSTTTTUUUUUUUUUUUUUUUUWYYYYYZZZ'..
					'aaaaaaaaaaaacccccddddeeeeeeeeeeegggghhhiiiiiiiiiiijklllllllmnnnnnnoooooooooooorrrrrssssssttttuuuuuuuuuuuuuuuuwyyyyyzzz'
	local charMap = {}
	for i = 1,mw.ustring.len(charMap_from) do
		charMap[mw.ustring.sub(charMap_from, i, i)] = mw.ustring.sub(charMap_to, i, i)
	end
	charMap['ß'] = 'ss'
	return charMap
end

function p.strip_diacrits(wrd)
	if wrd then
		local charMap = characterMap()
		wrd = string.gsub(wrd, "[^\128-\191][\128-\191]*", charMap )
	end
	return wrd
end

function p.stripDiacrits(frame)
	return p.strip_diacrits(frame.args.word or mw.text.trim(frame.args[1]))
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
	return table.concat( out, '\n\n' )
end

local function configure( t )
	if t['defaultsort']           == 'y'   then config.defaultsort      = true end
	if t['autocat']               == 'yes' then config.autocat          = true end
	if t['trackingcats']          == 'yes' then config.trackingcats     = true end
	if t['conf_upload']           == 'yes' then config.uploadlink       = true end
	if t['conf_sitelinks']        == 'yes' then config.sitelinks        = true end
	if t['conf_authoritycontrol'] == 'yes' then config.authoritycontrol = true end
	if t['conf_helperlinks']      == 'yes' then config.helperlinks      = true end
	if t['conf_coordhelperlinks'] == 'yes' then config.coordhelperlinks = true end

	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 )
	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
	MYLANG = frame:callParserFunction( 'int', 'lang' ) or "en"
	LANG = mw.language.new( MYLANG )
	FALLBACKLANGS = { MYLANG, unpack(mw.language.getFallbacksFor(MYLANG)) }

	local parentframe = frame:getParent()
	if parentframe then
		configure( parentframe.args )
	end

	for _, v in ipairs( ITEM:getBestStatements('P31') ) do
		local dv = v.mainsnak.datavalue
		local id = dv and dv.value.id
		if id then INSTANCEOF[id] = true end
	end

	local out = {
		autocat(),
		'<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 ),
		properties(),
		footer(),
	}
	if config.trackingcats and os.clock() > 4.0 then -- longer than 4 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 ) .. '</table>'
end

return p

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