# Module:Commons link

require('Module:No globals')  

-- Module to find commons galleries and categories based on wikidata entries  
local getArgs = require('Module:Arguments').getArgs  
local yesNo = require('Module:Yesno')  
local generateWarning = require('Module:If preview')._warning  
local p = {}  

-- Check if string is a valid QID  
-- Argument: QID to check  
-- Returns: valid (bool)  
local function _validQID(qid)  
return qid and mw.ustring.find(qid,"^[Qq]%d+$")  
end  

-- Check if string is a valid wikidata property string  
-- Argument: property string to check  
-- Returns: valid (bool)  
local function _validProp(prop)  
return prop and mw.ustring.find(prop,"^[Pp]%d+$")  
end  

local function _lcfirst(s)  
return mw.ustring.lower(mw.ustring.sub(s,1,1))..mw.ustring.sub(s,2)  
end  

-- Format displayed linktext  
-- Arguments:  
-- s = string to display  
-- formatting = formatting table:  
-- formatting.linktext = if defined, override s  
-- formatting.lcfirst = lower case the first letter in display  
-- formatting.bold = whether to bold the display  
-- formatting.italic = whether to italicize the display  
-- formatting.nowrap = set nowrapping  
-- Returns:  
-- formatted string  
local function _formatResult(s, formatting)  
local resultVal = formatting.linktext or s  
if formatting.lcfirst then  
resultVal = _lcfirst(resultVal)  
end  
local  
if formatting.italic then end  
if formatting.bold then style = style.."font-weight:bold; " end  
if formatting.nowrap then style = style.."white-space:nowrap; " end  
if style ~= "" then  
resultVal = '<span>'..resultVal..'</span>'  
end  
return resultVal  
end  

-- Get title, namespace, and QID for current page  
-- Arguments:  
-- qid = testing only: get title of alternative page with QID=qid  
-- nsQid = whether to return the ns of the qid page or current  
-- Returns:  
-- title, namespace (string), qid of current page (or test page)  
local function _getTitleQID(qid,nsQid)  
local titleObject = mw.title.getCurrentTitle  
-- look up qid for current page (if not testing)  
local nsText = mw.ustring.gsub(titleObject.nsText,"_"," ")  
if not _validQID(qid) then  
qid = mw.wikibase.getEntityIdForCurrentPage  
return titleObject.text, nsText, qid  
end  
-- testing-only path: given a qid, determine title  
-- always use namespace from current page (to suppress tracking cat)  
qid = qid:upper  
local title = mw.wikibase.getSitelink(qid) or ""  
-- strip any namespace from sitelink  
local firstColon = mw.ustring.find(title,':',1,true)  
local qidNsText = ""  
if firstColon then  
qidNsText = mw.ustring.sub(title,1,firstColon-1)  
title = mw.ustring.sub(title,firstColon+1)  
end  
if nsQid then  
return title, qidNsText, qid  
end  
return title, nsText, qid  
end  

-- Lookup Commons gallery in Wikidata  
-- Arguments:  
-- qid = QID of current article  
-- fetch = whether to lookup Commons sitelink (bool)  
-- commonsSitelink = default value for Commons sitelink  
-- Returns:  
-- categoryLink = name of Commons category, nil if nothing is found  
-- consistent = multiple wikidata fields are examined: are they consistent?  
-- commonsSitelink = commons sitelink for current article  
local function _lookupGallery(qid,fetch,commonsSitelink)  
if not _validQID(qid) then  
return nil, true, nil  
end  
qid = qid:upper  
local galleryLink = nil  
local consistent = true  
-- look up commons sitelink for article, use if not category  
if fetch then  
commonsSitelink = mw.wikibase.getSitelink(qid,"commonswiki") or commonsSitelink  
end  
if commonsSitelink and mw.ustring.sub(commonsSitelink,1,9) ~= "Category:" then  
galleryLink = commonsSitelink  
end  
-- P935 is the "commons gallery" property for this article  
local P935 = mw.wikibase.getBestStatements(qid, "P935")[1]  
if P935 and P935.mainsnak.datavalue then  
local gallery = P935.mainsnak.datavalue.value  
if galleryLink and galleryLink ~= gallery then  
consistent = false  
else  
galleryLink = gallery  
end  
end  
return galleryLink, consistent, commonsSitelink  
end  

-- Find fallback category by looking up Commons sitelink of different page  
-- Arguments:  
-- qid = QID for current article  
-- property = property that refers to other article whose sitelink to return  
-- Returns: either category-stripped name of article, or nil  
local function _lookupFallback(qid,property)  
if not _validQID(qid) or not _validProp(property) then  
return nil  
end  
qid = qid:upper  
property = property:upper  
-- If property exists on current article, get value (other article qid)  
local value = mw.wikibase.getBestStatements(qid, property)[1]  
if value and value.mainsnak.datavalue and value.mainsnak.datavalue.value.id then  
-- Look up Commons sitelink of other article  
local sitelink = mw.wikibase.getSitelink(value.mainsnak.datavalue.value.id,"commonswiki")  
-- Check to see if it starts with "Category:". If so, strip it and return  
if sitelink and mw.ustring.sub(sitelink,1,9) == "Category:" then  
return mw.ustring.sub(sitelink,10)  
end  
end  
return nil  
end  

-- Find Commons category by looking in wikidata  
-- Arguments:  
-- qid = QID of current article  
-- fetch = whether to lookup Commons sitelink (bool)  
-- commonsSitelink = default value for Commons sitelink  
-- Returns:  
-- categoryLink = name of Commons category, nil if nothing is found  
-- consistent = multiple wikidata fields are examined: are they consistent?  
-- commonsSitelink = commons sitelink for current article  
local function _lookupCategory(qid, fetch, commonsSitelink)  
if not _validQID(qid) then  
return nil, true, nil  
end  
qid = qid:upper  
local categoryLink = nil  
local consistent = true  
-- look up commons sitelink for article, use if starts with "Category:"  
if fetch then  
commonsSitelink = mw.wikibase.getSitelink(qid,"commonswiki") or commonsSitelink  
end  
if commonsSitelink and mw.ustring.sub(commonsSitelink,1,9) == "Category:" then  
categoryLink = mw.ustring.sub(commonsSitelink,10)  
end  
-- P910 is the "topic's main category". Look for commons sitelink there  
local fallback = _lookupFallback(qid,"P910")  
if fallback then  
if categoryLink and categoryLink ~= fallback then  
consistent = false  
qid = nil  
else  
categoryLink = fallback  
end  
end  
-- P1754 is the "list's main category". Look for commons sitelink there  
fallback = _lookupFallback(qid,"P1754")  
if fallback then  
if categoryLink and categoryLink ~= fallback then  
consistent = false  
qid = nil  
else  
categoryLink = fallback  
end  
end  
-- P373 is the "commons category" property for this article. This is  
-- a low-quality field, so should only be used as a last resort.  
if categoryLink == nil and _validQID(qid) then  
local P373 = mw.wikibase.getBestStatements(qid, "P373")[1]  
if P373 and P373.mainsnak.datavalue then  
categoryLink = P373.mainsnak.datavalue.value  
consistent = true -- P373 is never used if anything else is available  
end  
end  
return categoryLink, consistent, commonsSitelink  
end  

-- Does the article have a Commons gallery, and is it consistent?  
-- Arguments:  
-- qid = QID to lookup in wikidata (for testing only)  
-- Returns:  
-- filename at Commons, bool: is wikidata consistent for this article?  
function p._hasGalleryConsistent(qid)  
local wp_title, wp_ns  
wp_title, wp_ns, qid = _getTitleQID(qid)  
return _lookupGallery(qid,true)  
end  

-- Does the article have a corresponding Commons gallery?  
-- Arguments:  
-- qid = QID to lookup in wikidata (for testing only)  
-- Returns:  
-- filename at Commons if so, false if not  
function p._hasGallery(qid)  
local galleryLink, consistent = p._hasGalleryConsistent(qid)  
return consistent and galleryLink  
end  

-- Does the article have a Commons category? Is wikidata consistent for that?  
-- Arguments:  
-- qid = QID to lookup in wikidata (for testing only)  
-- prefix = whether to add "Category:" to return string (default true)  
-- Returns:  
-- filename at Commons, bool: consistent  
function p._hasCategoryConsistent(qid,prefix)  
if prefix == nil then  
prefix = true  
end  
local wp_title, wp_ns  
wp_title, wp_ns, qid = _getTitleQID(qid)  
local categoryLink, consistent = _lookupCategory(qid,true)  
if categoryLink and prefix then  
categoryLink = "Category:"..categoryLink  
end  
return categoryLink, consistent  
end  

-- Does the article have a corresponding Commons category?  
-- Arguments:  
-- qid = QID to lookup in wikidata (for testing only)  
-- prefix = whether to add "Category:" to return string (default true)  
-- Returns:  
-- filename at Commons if so, blank if not  
function p._hasCategory(qid,prefix)  
local categoryLink, consistent = p._hasCategoryConsistent(qid,prefix)  
return consistent and categoryLink  
end  

-- Create Commons link corresponding to current article  
-- Arguments:  
-- namespace = namespace in Commons ("" for galleries)  
-- default = use as Commons link, don't access wikidata  
-- search = string to search for  
-- fallback = string to search for if wikidata fails  
-- formatting = formatting parameters  
-- qid = QID to lookup in wikidata (for testing only)  
-- Returns:  
-- formatted wikilink to Commons in specified namespace  
function p._getCommons(namespace,default,search,fallback,formatting,qid)  
local nsColon  
if not namespace or namespace == "" then  
nsColon = ""  
else  
nsColon = namespace..":"  
end  
if default then  
return "".._formatResult(default,formatting)..""  
end  
if search then  
return "".._formatResult(search,formatting)..""  
end  
local wp_title, wp_ns  
wp_title, wp_ns, qid = _getTitleQID(qid)  
local commonsLink = nil  
local consistent = true  
if nsColon == "" then  
commonsLink, consistent = _lookupGallery(qid,true)  
elseif namespace:lower == "category" then  
commonsLink, consistent = _lookupCategory(qid,true)  
end  
-- use wikidata if consistent  
if commonsLink and consistent then  
return "".._formatResult(commonsLink,formatting)..""  
end  
-- if not consistent, fall back to search and add to tracking cat  
-- construct default result (which searches for title)  
local searchResult = "".._formatResult(fallback or wp_title,formatting)..""  
if not consistent and wp_ns == "" then  
local friendlyNS  
if nsColon == "" then  
friendlyNS = "gallery"  
else  
friendlyNS = namespace:lower  
end  
searchResult = searchResult..""  
end  
return searchResult  
end  

-- Returns "best" Commons link: first look for gallery, then try category  
-- Arguments:  
-- default = use as Commons link, don't access wikidata  
-- search = string to search for  
-- fallback = string to search for if wikidata lookup fails  
-- formatting = formatting parameters  
-- qid = QID to lookup in wikidata (for testing only)  
-- Returns:  
-- formatted wikilink to Commons "best" landing page  
function p._getGalleryOrCategory(default, search, fallback, formatting, qid)  
if default then  
return "".._formatResult(default,formatting)..""  
end  
if search then  
return "".._formatResult(search,formatting)..""  
end  
local wp_title, wp_ns  
wp_title, wp_ns, qid = _getTitleQID(qid)  
local trackingCats = ""  
local galleryLink, consistent, commonsSitelink = _lookupGallery(qid,true)  
-- use wikidata if either sitelink or P935 exist, and they both agree  
if galleryLink and consistent then  
return "".._formatResult(galleryLink,formatting)..""  
end  
if not consistent and wp_ns == "" then  
trackingCats = ""  
end  
-- if gallery is not good, fall back looking for category  
local categoryLink  
categoryLink, consistent = _lookupCategory(qid,false,commonsSitelink)  
if categoryLink and consistent then  
return "".._formatResult(categoryLink,formatting)..""..trackingCats  
end  
if not consistent and wp_ns == "" then  
trackingCats = trackingCats..""  
end  
-- return search result looking for title as last attempt  
return "" .. _formatResult(fallback or wp_title,formatting) .. "" .. trackingCats  
end  

-- Return link(s) Commons gallery, or category, or both from wikidata  
-- Arguments:  
-- defaultGallery = default gallery link to use, instead of wikidata  
-- defaultCategory = default category link to use, instead of wikidata  
-- categoryText = if both gallery and category, text to use in category link ("category" by default)  
-- oneSearch = only emit one search result  
-- formatting = formatting parameters  
-- qid = qid of page to lookup in wikidata (testing only)  
function p._getGalleryAndCategory(defaultGallery, defaultCategory,  
categoryText, oneSearch, formatting, qid  
local wp_title, wp_ns  
wp_title, wp_ns, qid = _getTitleQID(qid)  
categoryText = categoryText or "category"  
local trackingCats = ""  
local galleryLink, galleryConsistent  
local commonsSitelink = nil  
if defaultGallery then  
galleryLink = defaultGallery  
galleryConsistent = true  
else  
galleryLink, galleryConsistent, commonsSitelink = _lookupGallery(qid,true)  
end  
local galleryGood = galleryLink and galleryConsistent  
if not galleryConsistent and wp_ns == "" then  
trackingCats = ""  
end  
local categoryLink, categoryConsistent  
if defaultCategory then  
categoryLink = defaultCategory  
categoryConsistent = true  
else  
categoryLink, categoryConsistent = _lookupCategory(qid,defaultGallery,commonsSitelink)  
end  
local categoryGood = categoryLink and categoryConsistent  
if not categoryConsistent and wp_ns == "" then  
trackingCats = trackingCats..""  
end  
local firstLink  
-- construct default result (which searches for title)  
local searchResult = "".._formatResult(wp_title,formatting)..""  
if not oneSearch then  
searchResult = searchResult.." ("..categoryText..")"  
end  
local linkText = nil  
if galleryGood then  
firstLink = galleryLink  
linkText = galleryLink  
elseif categoryGood then  
firstLink = "Category:"..categoryLink  
linkText = categoryLink  
else  
return searchResult..trackingCats  
end  
local resultVal = "".._formatResult(linkText,formatting)..""  
if galleryGood and categoryGood then  
resultVal = resultVal.." ("..categoryText..")"  
end  
return resultVal..trackingCats  
end  

-- Compare two titles with their namespaces stripped  
local function titleMatch(s1,s2)  
s1 = s1 or ""  
s2 = s2 or ""  
s1 = mw.ustring.gsub(s1,"^[^:]+:","")  
s2 = mw.ustring.gsub(s2,"^[^:]+:","")  
return s1 == s2  
end  

local galleryTrackingCats = {  
commons_link_on_wikidata = '',  
commons_link_defined_as_pagename = '',  
commons_link_locally_defined = '',  
commons_link_from_wikidata = '',  
commons_link_is_pagename = '',  
inconsistent = ''  
}  

local categoryTrackingCats = {  
commons_link_on_wikidata = '',  
commons_link_defined_as_pagename = '',  
commons_link_locally_defined = '',  
commons_link_from_wikidata = '',  
commons_link_is_pagename = '',  
inconsistent = ''  
}  

local function selectTrackingCat(trackingCats,wikidata,consistent,default,title)  
if not consistent then  
return trackingCats.inconsistent  
end  
if default then  
-- construct warning message  
if default == wikidata then  
return trackingCats.commons_link_on_wikidata  
end  
local warning = ""  
if wikidata then  
warning = generateWarning({  
"Commons link does not match Wikidata – please check"  
})  
end  
if titleMatch(default,title) then  
return trackingCats.commons_link_defined_as_pagename .. warning  
end  
return trackingCats.commons_link_locally_defined .. warning  
end  
if wikidata then  
return trackingCats.commons_link_from_wikidata  
end  
return trackingCats.commons_link_is_pagename  
end  

-- Figure out tracking categories and editor warnings  
-- Arguments:  
-- default = Commons link argument passed to template  
-- fetchGallery = whether to fetch a gallery from Wikidata  
-- fetchCategory = whether to fetch a category from Wikidata  
-- qid = force a qid for testing  
-- Returns:  
-- tracking category and possible user warning  
---
-- Note: the logic for the tracking is quite different than the logic  
-- for generating Commons links (above). Thus, it is separated into another  
-- function for code clarity and maintainability. This should not seriously  
-- affect performance: server time is dominated by fetching wikidata entities,  
-- and those entities should be cached and shared between the Commons generating  
-- code and this tracking code.  
function p._tracking(default, fetchGallery, fetchCategory, qid)  
local title, wp_ns, wp_qid = _getTitleQID(qid,true)  
if wp_ns ~= "" then  
title = wp_ns..":"..title  
end  
-- only track if test or namespace=article or namespace=category  
if not (qid or wp_ns == "" or wp_ns == "Category") then  
return ""  
end  

-- determine title and namespace of wikidata and wp article  
local wikidata = nil  
local consistent = nil  
-- Tracking code works for all 4 cases of states of fetchGallery/Category  
-- fetchGallery takes precedence  
if fetchGallery then  
wikidata, consistent = p._hasGalleryConsistent(qid)  
if default or not fetchCategory or (consistent and wikidata) then  
return selectTrackingCat(galleryTrackingCats,wikidata,consistent,  
default,title)  
end  
end  
if fetchCategory then  
local cat_wikidata, cat_consistent = p._hasCategoryConsistent(qid,true)  
if not fetchGallery or (cat_consistent and cat_wikidata) then  
return selectTrackingCat(categoryTrackingCats,cat_wikidata,  
cat_consistent,default,title)  
end  
return selectTrackingCat(galleryTrackingCats,wikidata,consistent,  
default,title)  
end  
return "" -- nothing fetched, nothing tracked  
end  

local function _createFormatting(args)  
local formatting = {}  
formatting.linktext = args.linktext  
formatting.lcfirst = yesNo(args.lcfirst)  
formatting.bold = yesNo(args.bold)  
formatting.italic = yesNo(args.italic)  
formatting.nowrap = yesNo(args.nowrap)  
return formatting  
end  

-- Testing-only entry point for _getTitleQID  
function p.getTitleQID  
local args = getArgs(,{frameOnly=true,parentOnly=false,parentFirst=false})  
local text, ns, qid = _getTitleQID(args[1],args[2])  
return text..","..ns..","..(qid or "nil")  
end  

-- Testing-only entry point for _lookupFallback  
function p.lookupFallback  
local args = getArgs(,{frameOnly=true,parentOnly=false,parentFirst=false})  
local fallback = _lookupFallback(args[1],args[2])  
return fallback or "nil"  
end  

-- Find the Commons gallery page associated with article  
function p.getGallery  
local args = getArgs(,{frameOnly=true,parentOnly=false,parentFirst=false})  
return p._getCommons("",args[1],args.search,args.fallback,_createFormatting(args),args.qid)  
end  

-- Find the Commons category page associated with article  
function p.getCategory  
local args = getArgs(,{frameOnly=true,parentOnly=false,parentFirst=false})  
local retval = p._getCommons("Category", args[1],  
args.search, args.fallback, _createFormatting(args), args.qid  
if args.tracking then  
local default = nil  
if args[1] then  
default = "Category:"..args[1]  
end  
retval = retval..p._tracking(default, false, true, args.qid)  
end  
return retval  
end  

function p.getGalleryOrCategory  
local args = getArgs(,{frameOnly=true,parentOnly=false,parentFirst=false})  
local retval = p._getGalleryOrCategory(  
args[1], args.search, args.fallback, _createFormatting(args), args.qid  
if args.tracking then  
retval = retval..p._tracking(args[1],true,true,args.qid)  
end  
return retval  
end  

function p.hasGallery  
local args = getArgs(,{frameOnly=true,parentOnly=false,parentFirst=false})  
return p._hasGallery(args.qid) or ""  
end  

function p.hasCategory  
local args = getArgs(,{frameOnly=true,parentOnly=false,parentFirst=false})  
return p._hasCategory(args.qid) or ""  
end  

function p.hasGalleryOrCategory  
local args = getArgs(,{frameOnly=true,parentOnly=false,parentFirst=false})  
return p._hasGallery(args.qid) or p._hasCategory(args.qid) or ""  
end  

function p.getGalleryAndCategory  
local args = getArgs(,{frameOnly=true,parentOnly=false,parentFirst=false})  
return p._getGalleryAndCategory(args[1], args[2],  
args.categoryText, args.oneSearch, _createFormatting(args), args.qid)  
end  

function p.tracking  
local args = getArgs(,{frameOnly=true,parentOnly=false,parentFirst=false})  
return p._tracking(args[1], args.fetchGallery, args.fetchCategory, args.qid)  
end  

return p