Módulo:Wikidata Infobox/Testes

Origem: Wikipédia, a enciclopédia livre.
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 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

--- 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 {}

    -- linking many values harms performance if the value items are big and the sitelink needs to be taken from P910, P1754 or P373
    local collapse = args.collapse or config.collapse
    local linked
    if pid ~= 'P2789' and pid ~= 'P527' and pid ~= 'P1382' then -- See [[Template_talk:Wikidata_Infobox#P2789_-_connects_with]]
        linked = #getBestStatements(args.qid or QID, pid) <= collapse
    end

    return WikidataIB._getValue{
        pid,
        name = pid,
        qid = args.qid or QID,
        linked = args.linked or 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, 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:!Utiliza Wikidata Infobox com mapas]]'
            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:Coordenadas')._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:Coordenadas')._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:!Utiliza Wikidata Infobox sem coordenadas]]'
    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:!Utiliza Wikidata Infobox com mapas]]'
    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]]
    },
    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  = 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 = {'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','P11101'},
    },
    { 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','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','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','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','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','P1212','P1882','P1901','P3293','P3634','P4399','P4659','P4701','P5950','P6506','P6631','P7704','P8386',}
    },
    { groupname = 'Culture',
      pids = {'P345','P539','P1219','P1220','P1248','P1362','P6113','P6132',}
    },
    { 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','P481','P649','P709','P718','P757','P758','P762','P808','P1216','P1459','P1483','P1600','P1700','P1702','P1708','P1764','P1769','P2424','P2783','P2081','P2917','P3038','P3177','P3178','P3318','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',}
    },
    { groupname = 'Protected areas',
      pids = {'P809','P3425','P3613','P3974','P5965','P6602','P6230','P6280','P6478','P6560','P6659','P3296','P677',}
    },
    { groupname = 'Places and geographical features',
      pids = {'P402','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
        '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:!Utiliza Wikidata Infobox sem imagem]]'
    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,
    sourceswiki  = true, wikimaniawiki = true, incubatorwiki = 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{ 'P2354', 'P1753', 'P1753', 'P1420' } do -- 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:!Utiliza Wikidata Infobox sem nome de família]]'
        end
        if givennames == '' then
--            out[#out+1] = '[[Category:!Utiliza Wikidata Infobox sem nome próprio]]'
        end
    end

    if config.autocat then
        for _, pid in ipairs{ 'P734', 'P1950', 'P9139' } do
            for name in iclaims( ITEM:getBestStatements(pid) ) do
                name = mw.wikibase.getLabelByLang( name.id, 'en' )
                if givennames == '' then
                    out[#out+1] = name and string.format('[[Category:%s (surname)]]', name)
                else
                    out[#out+1] = name and string.format('[[Category:%s (surname)|%s]]', name, stripDiacritics(givennames))
                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:!Utiliza Wikidata Infobox com ordem padrão suprimida]]'
    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( 'Categorização sobre [[:'..cat..']] suprimida' )
--            out[#out+1] = '[[Category:!Utiliza Wikidata Infobox com categoria de '..event..' desconhecida|'..year..']]'
        end
    end
end

local function countrycat( out )
    local exceptions = {
        Q30 = 'United States',
    }
    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( 'Categorização sobre [[:'..cat2..']] suprimida' )
--                    out[#out+1] = '[[Category:!Utiliza Wikidata Infobox com categoria de país desconhecida|'..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:!Utiliza Wikidata Infobox sem intância de]]'
        end
        if INSTANCEOF['Q5'] and not CLAIMS['P569'] then
--            out[#out+1] = '[[Category:Utiliza Wikidata Infobox sem data de nascimento]]'
        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
            })

            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:!Utiliza Wikidata Infobox para páginas de desambiguação]]'
        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:!Utiliza Wikidata Infobox para items apagados do Wikidata]]'
            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:!Utiliza Wikidata Infobox com sub-infocaixas]]'
            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] = '[https://scholia.toolforge.org/'..QID..' '..getLabel('Q45340488')..']'

    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://iw.toolforge.org/kmlexport?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];node[wikidata='..QID..'];if(count(nodes)==0){way[wikidata='..QID..'];};if(count(ways)==0){rel[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:!Utiliza Wikidata Infobox para items apagado do Wikidata]]'
    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..'</td></strong></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