Jump to content

Module:Wiki Loves Living Heritage

Permanently protected module
From Meta, a Wikimedia project coordination wiki
Module documentation

-- This module includes little helper functions for the Wiki Loves Living Heritage pages

local p = {}

-- Any time you use {{#invoke:}}, the object passed in is a frame (representing the page using the module as well as arguments passed to the {{#invoke:}} call).

function p.wikidata_source_link(frame)
    local entity_id = frame.args[1] -- Wikidata entity ID
    local property_id = "P856" -- Wikidata property ID

    local value = mw.wikibase.getEntity(entity_id):getPropertyValue(property_id)
    local source_url = mw.wikibase.getBestStatements(entity_id, property_id)[1]:getSourceUrl()

    return string.format("[[%s|%s]]", source_url, value)
end

function p.wikipediaLink(frame)
    local entity = frame.args[1] -- Wikidata entity ID
    local lang = frame.args[2] -- Wikidata property ID
	local link
	if type(entity) == 'table' then
		link = entity:getSitelink(lang .. 'wiki')
	else
		link = mw.wikibase.getSitelink(entity, lang .. 'wiki')
	end
	if link then
		linktxt = ':' .. lang .. ':' .. link
		return string.format("[[%s|%s]]", linktxt, link)
	end
	return nil
end

function p.organizerlistsimple(frame)
    local output = ""
    -- Get the first argument passed to the {{#invoke:}} call (after the name of the module and the function name).
    local input = frame.args[1]
    -- Use 'mw.ustring' rather than 'string', since strings that are passed in are almost always Unicode text.
    for i in mw.ustring.gmatch(input, "[^,]+") do
        -- Get the result from using the {{label}} template, with i as an argument, on this page.
        current_label = frame:expandTemplate{
          title='Wiki Loves Living Heritage/Organizerlinksimple',
          args={i}
        }
        -- Add a bullet point before every label except the first.
        if output ~= "" then
            output = output .. ", "
        end
        -- Add the current label to the output.
        output = output .. current_label
    end
    return output
end

function p.organizerlist(frame)
    local output = ""
    -- Get the first argument passed to the {{#invoke:}} call (after the name of the module and the function name).
    local input = frame.args[1]
    -- Use 'mw.ustring' rather than 'string', since strings that are passed in are almost always Unicode text.
    for i in mw.ustring.gmatch(input, "[^,]+") do
        -- Get the result from using the {{label}} template, with i as an argument, on this page.
        current_label = frame:expandTemplate{
          title='Wiki Loves Living Heritage/Organizerlink',
          args={i}
        }
        -- Add a bullet point before every label except the first.
        if output ~= "" then
            output = output .. "<br />"
        end
        -- Add the current label to the output.
        output = output .. current_label
    end
    return output
end

local function getPreferredFlag(entity)
    if not entity or not entity.claims or not entity.claims.P41 then
        return nil
    end

    local normal = nil

    for _, claim in ipairs(entity.claims.P41) do
        if claim.rank == "preferred"
           and claim.mainsnak
           and claim.mainsnak.datavalue then
            return claim.mainsnak.datavalue.value
        end

        -- remember first normal-rank claim as fallback
        if not normal
           and claim.rank == "normal"
           and claim.mainsnak
           and claim.mainsnak.datavalue then
            normal = claim.mainsnak.datavalue.value
        end
    end

    return normal
end

function p.labellist(frame)
    local input = frame.args[1]
    local showFlags = frame.args.flags == "yes"

    if not input or input == "" then
        return ""
    end

    local output = ""

    for id in mw.ustring.gmatch(input, "[^,]+") do
        id = mw.text.trim(id)

        local current_label = frame:expandTemplate{
            title = 'Wiki Loves Living Heritage/Originlink',
            args = { id }
        }

        local flag = nil

        if showFlags then
            local entity = mw.wikibase.getEntity(id)
			if entity then
			    flag = getPreferredFlag(entity)
			end
        end

        if output ~= "" then
            output = output .. " • "
        end

        if flag then
            output = output .. string.format(
                '[[File:%s|20px|border]] %s',
                flag,
                current_label
            )
        else
            output = output .. current_label
        end
    end

    return '<div class="labellist">' .. output .. '</div>'
end

function p.ichlinksFromItem(frame)
    local args = frame.args
    local qid = args.item
    local userLang = args.lang or "en"

    -- UNESCO ICH lists handled by this function
    local allowedLists = {
        Q110319947 = true, -- Representative List
        Q17323370  = true, -- Urgent Safeguarding List
        Q17325849  = true  -- Register of Good Safeguarding Practices
    }

    if not qid then
        return ""
    end

    local entity = mw.wikibase.getEntity(qid)
    if not entity or not entity.claims or not entity.claims.P3259 then
        return ""
    end

    local output = {}

    for _, claim in ipairs(entity.claims.P3259) do
        -- Skip deprecated statements
        if claim.rank ~= "deprecated" then
            local valueQid = claim.mainsnak.datavalue.value.id

            -- Only handle allowed UNESCO ICH lists
            if allowedLists[valueQid] then
                local processStatement = false
                local statementIchid = nil
                local urlCandidate = nil

                -- 1️⃣ P10221 on statement
                if claim.qualifiers
                    and claim.qualifiers.P10221
                    and claim.qualifiers.P10221[1]
                    and claim.qualifiers.P10221[1].datavalue then
                    statementIchid = claim.qualifiers.P10221[1].datavalue.value
                    processStatement = true
                end

                -- 2️⃣ P10221 in references
                if not statementIchid and claim.references then
                    for _, ref in pairs(claim.references) do
                        local snaks = ref.snaks or {}
                        if snaks.P10221 and snaks.P10221[1] and snaks.P10221[1].datavalue then
                            statementIchid = snaks.P10221[1].datavalue.value
                            processStatement = true
                            break
                        end
                    end
                end

                -- 3️⃣ P854-only fallback (UNESCO URL)
                if not statementIchid and claim.references then
                    for _, ref in pairs(claim.references) do
                        local snaks = ref.snaks or {}
                        if snaks.P854 and snaks.P854[1] and snaks.P854[1].datavalue then
                            local refUrl = snaks.P854[1].datavalue.value
                            if refUrl:match("^https://ich%.unesco%.org/") then
                                urlCandidate = refUrl
                                processStatement = true
                                break
                            end
                        end
                    end
                end

                if processStatement then
                    -- Determine prefix/number for ICH URL if P10221 exists
                    local prefix, num
                    if statementIchid then
                        prefix = mw.ustring.match(statementIchid, "^(%u+)")
                        num    = mw.ustring.match(statementIchid, "/(%d+)$")
                    end

                    -- 4️⃣ Title lookup
                    local bestMatch
                    local fallbackMatch

                    if claim.references then
                        for _, ref in pairs(claim.references) do
                            local snaks = ref.snaks or {}
                            local url, title, titleLang

                            -- Use P854 URL if present
                            if snaks.P854 and snaks.P854[1] and snaks.P854[1].datavalue then
                                local refUrl = snaks.P854[1].datavalue.value
                                if refUrl:match("^https://ich%.unesco%.org/") then
                                    url = refUrl
                                end
                            end

                            -- Reference P1476
                            if snaks.P1476 and snaks.P1476[1] and snaks.P1476[1].datavalue then
                                local val = snaks.P1476[1].datavalue.value
                                title = val.text
                                titleLang = val.language
                            end

                            if url and title then
                                if titleLang == userLang then
                                    bestMatch = { url = url, title = title }
                                    break
                                elseif titleLang == "en" then
                                    fallbackMatch = { url = url, title = title }
                                end
                            end
                        end
                    end

                    -- Statement qualifier P1476
                    if not bestMatch and claim.qualifiers and claim.qualifiers.P1476 then
                        for _, qual in ipairs(claim.qualifiers.P1476) do
                            if qual.datavalue then
                                local val = qual.datavalue.value
                                if val.language == userLang then
                                    bestMatch = { url = nil, title = val.text }
                                    break
                                elseif val.language == "en" then
                                    fallbackMatch = { url = nil, title = val.text }
                                end
                            end
                        end
                    end

                    local chosen = bestMatch or fallbackMatch

                    -- Fallback to inventory label
                    if not chosen then
                        local inventoryLabel =
                            mw.wikibase.getLabelByLang(valueQid, userLang)
                            or mw.wikibase.getLabelByLang(valueQid, "en")
                            or ""
                        chosen = { url = nil, title = inventoryLabel }
                    end

                    -- Determine final URL
                    if not chosen.url then
                        if statementIchid then
                            chosen.url = string.format(
                                "https://ich.unesco.org/%s/%s/%s",
                                userLang, prefix, num
                            )
                        elseif urlCandidate then
                            chosen.url = urlCandidate
                        end
                    end

                    if chosen.url and chosen.title then
                        table.insert(
                            output,
                            string.format(
                                '[[File:UNESCO-ICH-blue.svg|20px]] [%s %s]',
                                chosen.url,
                                chosen.title
                            )
                        )
                    end
                end
            end
        end
    end

    return table.concat(output, "<br />")
end

local function getPreferredReferenceUrl(claim, lang)
    if not claim.references then
        claim.references = {} -- ensure we can iterate
    end

    -- Map lang code to Wikidata language item, if possible
    local langQid = nil
    if lang and mw.wikibase.getEntityIdForLanguageCode then
        langQid = mw.wikibase.getEntityIdForLanguageCode(lang)
    end

    local fallbackNoLang = nil
    local fallbackAny = nil

    -- 1️⃣ Try URLs from references with language match
    for _, ref in ipairs(claim.references) do
        if ref.snaks and ref.snaks.P854 and ref.snaks.P854[1].datavalue then
            local url = ref.snaks.P854[1].datavalue.value
            fallbackAny = fallbackAny or url

            if ref.snaks.P407 and ref.snaks.P407[1].datavalue then
                local refLangQid = ref.snaks.P407[1].datavalue.value.id
                if langQid and refLangQid == langQid then
                    return url
                end
            else
                fallbackNoLang = fallbackNoLang or url
            end
        end
    end

    -- 2️⃣ Return fallback URL from references if available
    if fallbackNoLang or fallbackAny then
        return fallbackNoLang or fallbackAny
    end

    -- 3️⃣ Last-resort: fetch P856 of the P3259 entity
    if claim.mainsnak and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value then
        local p3259Qid = claim.mainsnak.datavalue.value.id
        if p3259Qid then
            local p3259Entity = mw.wikibase.getEntity(p3259Qid)
            if p3259Entity and p3259Entity.claims and p3259Entity.claims.P856 then
                local p856Claim = p3259Entity.claims.P856[1]
                if p856Claim and p856Claim.mainsnak and p856Claim.mainsnak.datavalue then
                    return p856Claim.mainsnak.datavalue.value
                end
            end
        end
    end

    return nil
end

local function getInventoryFlags(invId)
    local invEntity = mw.wikibase.getEntity(invId)
    if not invEntity or not invEntity.claims or not invEntity.claims.P17 then
        return ''
    end

    local flags = {}
    local seenFlags = {}  -- track which flag files we've already added

    for _, countryClaim in ipairs(invEntity.claims.P17) do
        if countryClaim.mainsnak
            and countryClaim.mainsnak.datavalue
            and countryClaim.mainsnak.datavalue.value
        then
            local countryId = countryClaim.mainsnak.datavalue.value.id
            local countryEntity = mw.wikibase.getEntity(countryId)

            if countryEntity
                and countryEntity.claims
                and countryEntity.claims.P41
                and countryEntity.claims.P41[1].mainsnak.datavalue
            then
                local flagFile =
                    countryEntity.claims.P41[1].mainsnak.datavalue.value

                if not seenFlags[flagFile] then
                    table.insert(
                        flags,
                        "<span class='flag'>[[File:" ..
                        flagFile ..
                        "|frameless|20x20px|link=]]</span>"
                    )
                    seenFlags[flagFile] = true
                end
            end
        end
    end

    if #flags > 0 then
        return table.concat(flags, ' ') .. ' '
    end

    return ''
end

function p.inventorylist(frame)
    local item = frame.args.item
    if not item then return '' end

    local lang = frame.args.lang

    local entity = mw.wikibase.getEntity(item)
    if not entity or not entity.claims or not entity.claims.P3259 then return '' end

    local unescoQids = {
        ["Q110319947"] = true,
        ["Q17323370"]  = true,
        ["Q17325849"]  = true
    }
    local deprecatedUnescoQid = "Q877988"

    local output = {}

    for _, claim in ipairs(entity.claims.P3259) do
        local invId = claim.mainsnak.datavalue.value.id
        local isExcludedUnesco = unescoQids[invId]
        local isDeprecatedUnesco = (invId == deprecatedUnescoQid)

        if not isExcludedUnesco or isDeprecatedUnesco then
            local label = mw.wikibase.getLabel(invId) or invId

            -- language-aware URL
            local url = getPreferredReferenceUrl(claim, lang)

            -- prefix: either ICH logo or multiple flags
            local prefix = ''
            if isDeprecatedUnesco then
                prefix = "<span class='ich-logo'>[[File:UNESCO-ICH-blue.svg|20px|link=]]</span> "
            else
                prefix = getInventoryFlags(invId)
            end

            local rendered
            if url then
                rendered = prefix .. string.format('[%s %s]', url, label)
            else
                rendered = prefix .. label
            end

            table.insert(output, "<div class='boxrow'>" .. rendered .. "</div>")
        end
    end

    return table.concat(output)
end

function p.participantlist(frame)
    local output = ""
    local arguments = {}
	local input = frame.args[1]
    for i in mw.ustring.gmatch(input, "[^,]+") do
        arguments['item'] = i
        current_label = frame:expandTemplate{
          title='Wiki Loves Living Heritage/Contact',
          args = arguments
        }
        output = output .. current_label
    end
    return output
end

-- Function to convert Solar Hijri date to Gregorian and return in ISO format
local function hijriToGregorianISO(hijriYear, hijriMonth, hijriDay)
    -- Constants for Solar Hijri Calendar (Persian Calendar)
    local baseYearGregorian = 621  -- Base year for Hijri-Gregorian correspondence
    local hijriNewYearOffset = 79  -- Offset for Persian New Year around March 21

    -- Calculate the Gregorian year
    local gregorianYear = hijriYear + baseYearGregorian
    local approximateGregorianDayOfYear = (hijriMonth - 1) * 30 + hijriDay + hijriNewYearOffset

    -- Adjust to convert the day and month accurately
    local day = approximateGregorianDayOfYear
    local month = 3  -- Start around March

    while day > 30 do
        day = day - 30
        month = month + 1
        if month > 12 then
            month = 1
            gregorianYear = gregorianYear + 1
        end
    end

    -- Format the date in ISO format (YYYY-MM-DD)
    local isoDate = string.format("%04d-%02d-%02d", gregorianYear, month, day)
    return isoDate
end

function p.officialWebsiteLanguageLinks(frame)
    local qid = frame.args.qid
    if not qid then return "" end

    local entity = mw.wikibase.getEntity(qid)
    if not entity or not entity.claims then
        return ""
    end

    local fallbackLabel = mw.wikibase.getLabel(qid)
    local links = {}

    -- === Existing P856 logic ===
    if entity.claims.P856 then
        for _, claim in ipairs(entity.claims.P856) do
            local snak = claim.mainsnak
            if snak and snak.datavalue then
                local url = snak.datavalue.value
                local label = nil

                -- Preferred: language of work (P407)
                if claim.qualifiers and claim.qualifiers.P407 then
                    local langSnak = claim.qualifiers.P407[1]
                    if langSnak and langSnak.datavalue then
                        label = mw.wikibase.getLabel(langSnak.datavalue.value.id)
                    end
                end

                -- Fallback: item label
                if not label then
                    label = fallbackLabel
                end

                if label then
                    table.insert(
                        links,
                        string.format("[%s %s]", url, label)
                    )
                end
            end
        end
    end

    local output = {}

    -- Add P856 links row if any
    if #links > 0 then
        local icon = '🌐  '
        table.insert(output, '<div class="boxrow">' .. icon .. table.concat(links, " • ") .. '</div>')
    end

    -- === Enhanced P1325 logic ===
    local p1325links = {}
    if entity.claims.P1325 then
        for _, claim in ipairs(entity.claims.P1325) do
            local snak = claim.mainsnak
            if snak and snak.datavalue then
                local url = snak.datavalue.value
                local labels = {}

                -- Collect all P407 qualifier labels
                if claim.qualifiers and claim.qualifiers.P407 then
                    for _, qual in ipairs(claim.qualifiers.P407) do
                        if qual.datavalue then
                            local langLabel = mw.wikibase.getLabel(qual.datavalue.value.id)
                            if langLabel then
                                table.insert(labels, langLabel)
                            end
                        end
                    end
                end

                -- Determine link text based on number of P407 qualifiers
                local linkText
                if #labels == 0 then
                    -- No P407: just "Data"
                    linkText = string.format("[%s Elements]", url)
                elseif #labels == 1 then
                    -- Single P407
                    linkText = string.format("[%s %s]", url, labels[1])
                else
                    -- Multiple P407
                    linkText = string.format("[%s %s]", url, table.concat(labels, " / "))
                end

                table.insert(p1325links, linkText)
            end
        end
    end

    -- Add P1325 links row if any
    if #p1325links > 0 then
        local icon = '🗂️  '
        table.insert(output, '<div class="boxrow">' .. icon .. table.concat(p1325links, " • ") .. '</div>')
    end

    return table.concat(output)
end

function p.originStats(frame)
	-- Everything lives inside this function
	local sparql = require('mw.wikibase.sparql')

	local args = frame.args
	local countryQid = args[1] or args.country
	if not countryQid or countryQid == '' then
		return ''
	end

	local query = string.format([[
SELECT 
  (COUNT(DISTINCT ?item) AS ?items)
  (COUNT(?rlStatement)  AS ?rls)
  (COUNT(?uslStatement) AS ?usls)
  (COUNT(?bspStatement) AS ?bsps)
  (COUNT(DISTINCT ?invStatement) AS ?invs)
WHERE {
  ?item p:P3259 ?p3259 .

  FILTER EXISTS {
    ?item
      wdt:P17
      | wdt:P495
      | wdt:P2341
      | p:P3259/p:P17
      | p:P3259/ps:P3259/wdt:P17
      wd:%s .
  }

  FILTER NOT EXISTS { ?p3259 wikibase:rank wikibase:DeprecatedRank }

  OPTIONAL { ?p3259 ps:P3259 wd:Q110319947 . BIND(?p3259 AS ?rlStatement) }
  OPTIONAL { ?p3259 ps:P3259 wd:Q17323370  . BIND(?p3259 AS ?uslStatement) }
  OPTIONAL { ?p3259 ps:P3259 wd:Q17325849  . BIND(?p3259 AS ?bspStatement) }

  OPTIONAL { 
    ?p3259 ps:P3259 ?inv .
    FILTER(?inv NOT IN (wd:Q110319947, wd:Q17323370, wd:Q17325849))
    BIND(?p3259 AS ?invStatement)
  }
}
]], countryQid)

	local data = sparql.select(query)
	if not data or not data[1] then
		return ''
	end

	local row = data[1]

	-- Expand the existing template with numeric values
	return frame:expandTemplate{
		title = 'Wiki_Loves_Living_Heritage/Originstats',
		args = {
			items = tonumber(row.items) or 0,
			rls   = tonumber(row.rls)   or 0,
			usls  = tonumber(row.usls)  or 0,
			bsps  = tonumber(row.bsps)  or 0,
			invs  = tonumber(row.invs)  or 0
		}
	}
end

-- Once you've finished adding functions to 'p', you need to return it.
return p