Module:Commons link

require('strict')

-- Module to find commons galleries and categories based on wikidata entries

local getArgs = require('Module:Arguments').getArgs

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 style = ""

if formatting.italic then style = "font-style:italic; " 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 = ''..resultVal..''

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 = string.gsub(titleObject.nsText,"_"," ") -- phab:T369784

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 = "[[Commons:Special:Search/"..nsColon..(fallback or wp_title)

.."|".._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.."Category:Inconsistent wikidata for Commons "..friendlyNS..""

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 = "Category:Inconsistent wikidata for Commons gallery"

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.."Category:Inconsistent wikidata for Commons category"

end

-- return search result looking for title as last attempt

return "[[Commons:Special:Search/" .. (fallback or wp_title) ..

"|" .. _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 = "Category:Inconsistent wikidata for Commons gallery"

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.."Category:Inconsistent wikidata for Commons category"

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 = 'Category:Commons link is on Wikidata',

commons_link_defined_as_pagename = 'Category:Commons link is defined as the pagename',

commons_link_locally_defined = 'Category:Commons link is locally defined',

commons_link_from_wikidata = 'Category:Commons link from Wikidata',

commons_link_is_pagename = 'Category:Commons link is the pagename',

inconsistent = 'Category:Inconsistent wikidata for Commons gallery'

}

local categoryTrackingCats = {

commons_link_on_wikidata = 'Category:Commons category link is on Wikidata',

commons_link_defined_as_pagename = 'Category:Commons category link is defined as the pagename',

commons_link_locally_defined = 'Category:Commons category link is locally defined',

commons_link_from_wikidata = 'Category:Commons category link from Wikidata',

commons_link_is_pagename = 'Category:Commons category link is the pagename',

inconsistent = 'Category:Inconsistent wikidata for Commons category'

}

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

local generateWarning = require('Module:If preview')._warning

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

local yesNo = require('Module:Yesno')

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(frame)

local args = getArgs(frame,{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(frame)

local args = getArgs(frame,{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(frame)

local args = getArgs(frame,{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(frame)

local args = getArgs(frame,{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(frame)

local args = getArgs(frame,{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(frame)

local args = getArgs(frame,{frameOnly=true,parentOnly=false,parentFirst=false})

return p._hasGallery(args.qid) or ""

end

function p.hasCategory(frame)

local args = getArgs(frame,{frameOnly=true,parentOnly=false,parentFirst=false})

return p._hasCategory(args.qid) or ""

end

function p.hasGalleryOrCategory(frame)

local args = getArgs(frame,{frameOnly=true,parentOnly=false,parentFirst=false})

return p._hasGallery(args.qid) or p._hasCategory(args.qid) or ""

end

function p.getGalleryAndCategory(frame)

local args = getArgs(frame,{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(frame)

local args = getArgs(frame,{frameOnly=true,parentOnly=false,parentFirst=false})

return p._tracking(args[1], args.fetchGallery, args.fetchCategory, args.qid)

end

return p