Mô đun:Wd

-- Original module located at :en:Module:Wd and :en:Module:Wd/i18n.

require("strict")
local p = {}
local module_arg = ...
local i18n
local i18nPath

local function loadI18n(aliasesP, frame)
local title

if frame then
-- current module invoked by page/template, get its title from frame
title = frame:getTitle()
else
-- current module included by other module, get its title from ...
title = module_arg
end

if not i18n then
i18nPath = title .. "/i18n"
i18n = require(i18nPath).init(aliasesP)
end
end

p.claimCommands = {
property = "property",
properties = "properties",
qualifier = "qualifier",
qualifiers = "qualifiers",
reference = "reference",
references = "references"
}

p.generalCommands = {
label = "label",
title = "title",
description = "description",
alias = "alias",
aliases = "aliases",
badge = "badge",
badges = "badges"
}

p.flags = {
linked = "linked",
short = "short",
raw = "raw",
multilanguage = "multilanguage",
unit = "unit",
-------------
preferred = "preferred",
normal = "normal",
deprecated = "deprecated",
best = "best",
future = "future",
current = "current",
former = "former",
edit = "edit",
editAtEnd = "edit@end",
mdy = "mdy",
single = "single",
sourced = "sourced"
}

p.args = {
eid = "eid",
page = "page",
date = "date",
globalSiteId = "globalSiteId"
}

local aliasesP = {
coord = "P625",
-----------------------
image = "P18",
author = "P50",
authorNameString = "P2093",
publisher = "P123",
importedFrom = "P143",
wikimediaImportURL = "P4656",
statedIn = "P248",
pages = "P304",
language = "P407",
hasPart = "P527",
publicationDate = "P577",
startTime = "P580",
endTime = "P582",
chapter = "P792",
retrieved = "P813",
referenceURL = "P854",
sectionVerseOrParagraph = "P958",
archiveURL = "P1065",
title = "P1476",
formatterURL = "P1630",
quote = "P1683",
shortName = "P1813",
definingFormula = "P2534",
archiveDate = "P2960",
inferredFrom = "P3452",
typeOfReference = "P3865",
column = "P3903",
subjectNamedAs = "P1810",
wikidataProperty = "P1687",
publishedIn = "P1433"
}

local aliasesQ = {
percentage = "Q11229",
prolepticJulianCalendar = "Q1985786",
citeWeb = "Q5637226",
citeQ = "Q22321052"
}

local parameters = {
property = "%p",
qualifier = "%q",
reference = "%r",
alias = "%a",
badge = "%b",
separator = "%s",
general = "%x"
}

local formats = {
property = "%p[%s][%r]",
qualifier = "%q[%s][%r]",
reference = "%r",
propertyWithQualifier = "%p[ (%q)][%s][%r]",
alias = "%a[%s]",
badge = "%b[%s]"
}

local hookNames = { -- {level_1, level_2}
[parameters.property] = {"getProperty"},
[parameters.reference] = {"getReferences", "getReference"},
[parameters.qualifier] = {"getAllQualifiers"},
[parameters.qualifier.."\\d"] = {"getQualifiers", "getQualifier"},
[parameters.alias] = {"getAlias"},
[parameters.badge] = {"getBadge"}
}

-- default value objects, should NOT be mutated but instead copied
local defaultSeparators = {
["sep"] = {" "},
["sep%s"] = {","},
["sep%q"] = {"; "},
["sep%q\\d"] = {", "},
["sep%r"] = nil, -- none
["punc"] = nil -- none
}

local rankTable = {
["preferred"] = 1,
["normal"] = 2,
["deprecated"] = 3
}

local function replaceAlias(id)
if aliasesP[id] then
id = aliasesP[id]
end

return id
end

local function errorText(code, ...)
local text = i18n["errors"][code]
if arg then text = mw.ustring.format(text, unpack(arg)) end
return text
end

local function throwError(errorMessage, ...)
error(errorText(errorMessage, unpack(arg)))
end

local function replaceDecimalMark(num)
return mw.ustring.gsub(num, "[.]", i18n['numeric']['decimal-mark'], 1)
end

local function padZeros(num, numDigits)
local numZeros
local negative = false

if num bY then
return false
end

if aM bM then
return false
end

if aD 2 then
return hookNames[param:sub(1, 2).."\\d"][index]
else
return nil
end
end

local function alwaysTrue()
return true
end

-- The following function parses a format string.
--
-- The example below shows how a parsed string is structured in memory.
-- Variables other than 'str' and 'child' are left out for clarity's sake.
--
-- Example:
-- "A %p B [%s[%q1]] C [%r] D"
--
-- Structure:
-- [
-- {
-- str = "A "
-- },
-- {
-- str = "%p"
-- },
-- {
-- str = " B ",
-- child =
-- [
-- {
-- str = "%s",
-- child =
-- [
-- {
-- str = "%q1"
-- }
-- ]
-- }
-- ]
-- },
-- {
-- str = " C ",
-- child =
-- [
-- {
-- str = "%r"
-- }
-- ]
-- },
-- {
-- str = " D"
-- }
-- ]
--
local function parseFormat(str)
local chr, esc, param, root, cur, prev, new
local params = {}

local function newObject(array)
local obj = {} -- new object
obj.str = ""

array[array + 1] = obj -- array{object}
obj.parent = array

return obj
end

local function endParam()
if param > 0 then
if cur.str ~= "" then
cur.str = "%"..cur.str
cur.param = true
params[cur.str] = true
cur.parent.req[cur.str] = true
prev = cur
cur = newObject(cur.parent)
end
param = 0
end
end

root = {} -- array
root.req = {}
cur = newObject(root)
prev = nil

esc = false
param = 0

for i = 1, str do
chr = str:sub(i,i)

if not esc then
if chr == '\\' then
endParam()
esc = true
elseif chr == '%' then
endParam()
if cur.str ~= "" then
cur = newObject(cur.parent)
end
param = 2
elseif chr '[' then
endParam()
if prev and cur.str "" then
table.remove(cur.parent)
cur = prev
end
cur.child = {} -- new array
cur.child.req = {}
cur.child.parent = cur
cur = newObject(cur.child)
elseif chr == ']' then
endParam()
if cur.parent.parent then
new = newObject(cur.parent.parent.parent)
if cur.str == "" then
table.remove(cur.parent)
end
cur = new
end
else
if param > 1 then
param = param - 1
elseif param == 1 then
if not chr:match('%d') then
endParam()
end
end

cur.str = cur.str .. replaceSpecialChar(chr)
end
else
cur.str = cur.str .. chr
esc = false
end

prev = nil
end

endParam()

-- make sure that at least one required parameter has been defined
if not next(root.req) then
throwError("missing-required-parameter")
end

-- make sure that the separator parameter "%s" is not amongst the required parameters
if root.req[parameters.separator] then
throwError("extra-required-parameter", parameters.separator)
end

return root, params
end

local function sortOnRank(claims)
local rankPos
local ranks = {{}, {}, {}, {}} -- preferred, normal, deprecated, (default)
local sorted = {}

for _, v in ipairs(claims) do
rankPos = rankTable[v.rank] or 4
ranks[rankPos][ranks[rankPos] + 1] = v
end

sorted = ranks[1]
sorted = mergeArrays(sorted, ranks[2])
sorted = mergeArrays(sorted, ranks[3])

return sorted
end

local function isValueInTable(searchedItem, inputTable)
for _, item in pairs(inputTable) do
if item == searchedItem then
return true
end
end
return false
end

local Config = {}

-- allows for recursive calls
function Config:new()
local cfg = {}
setmetatable(cfg, self)
self.__index = self

cfg.separators = {
-- single value objects wrapped in arrays so that we can pass by reference
["sep"] = {copyTable(defaultSeparators["sep"])},
["sep%s"] = {copyTable(defaultSeparators["sep%s"])},
["sep%q"] = {copyTable(defaultSeparators["sep%q"])},
["sep%r"] = {copyTable(defaultSeparators["sep%r"])},
["punc"] = {copyTable(defaultSeparators["punc"])}
}

cfg.entity = nil
cfg.entityID = nil
cfg.propertyID = nil
cfg.propertyValue = nil
cfg.qualifierIDs = {}
cfg.qualifierIDsAndValues = {}

cfg.bestRank = true
cfg.ranks = {true, true, false} -- preferred = true, normal = true, deprecated = false
cfg.foundRank = cfg.ranks
cfg.flagBest = false
cfg.flagRank = false

cfg.periods = {true, true, true} -- future = true, current = true, former = true
cfg.flagPeriod = false
cfg.atDate = {parseDate(os.date('!%Y-%m-%d'))} -- today as {year, month, day}

cfg.mdyDate = false
cfg.singleClaim = false
cfg.sourcedOnly = false
cfg.editable = false
cfg.editAtEnd = false

cfg.inSitelinks = false

cfg.langCode = mw.language.getContentLanguage().code
cfg.langName = mw.language.fetchLanguageName(cfg.langCode, cfg.langCode)
cfg.langObj = mw.language.new(cfg.langCode)

cfg.siteID = mw.wikibase.getGlobalSiteId()

cfg.states = {}
cfg.states.qualifiersCount = 0
cfg.curState = nil

cfg.prefetchedRefs = nil

return cfg
end

local State = {}

function State:new(cfg, type)
local stt = {}
setmetatable(stt, self)
self.__index = self

stt.conf = cfg
stt.type = type

stt.results = {}

stt.parsedFormat = {}
stt.separator = {}
stt.movSeparator = {}
stt.puncMark = {}

stt.linked = false
stt.rawValue = false
stt.shortName = false
stt.anyLanguage = false
stt.unitOnly = false
stt.singleValue = false

return stt
end

-- if id == nil then item connected to current page is used
function Config:getLabel(id, raw, link, short)
local label = nil
local prefix, title= "", nil

if not id then
id = mw.wikibase.getEntityIdForCurrentPage()

if not id then
return ""
end
end

id = id:upper() -- just to be sure

if raw then
-- check if given id actually exists
if mw.wikibase.isValidEntityId(id) and mw.wikibase.entityExists(id) then
label = id
end

prefix, title = "d:Special:EntityPage/", label -- may be nil
else
-- try short name first if requested
if short then
label = p._property{aliasesP.shortName, [p.args.eid] = id} -- get short name

if label == "" then
label = nil
end
end

-- get label
if not label then
label = mw.wikibase.getLabel(id)
end
end

if not label then
label = ""
elseif link then
-- build a link if requested
if not title then
if id:sub(1,1) == "Q" then
title = mw.wikibase.getSitelink(id)
elseif id:sub(1,1) == "P" then
-- properties have no sitelink, link to Wikidata instead
prefix, title = "d:Special:EntityPage/", id
end
end

label = mw.text.nowiki(label) -- escape raw label text so it cannot be wikitext markup
if title then
label = buildWikilink(prefix .. title, label)
end
end

return label
end

function Config:getEditIcon()
local value = ""
local prefix = ""
local front = "&nbsp;"
local back = ""

if self.entityID:sub(1,1) == "P" then
prefix = "Property:"
end

if self.editAtEnd then
front =
back =
end

value = "[[File:OOjs UI icon edit-ltr-progressive.svg|frameless|text-top|10px|alt=" .. i18n['info']['edit-on-wikidata'] .. "|link=https://www.wikidata.org/wiki/" .. prefix .. self.entityID .. "?uselang=" .. self.langCode

if self.propertyID then
value = value .. "" .. self.propertyID
elseif self.inSitelinks then
value = value .. "sitelinks-wikipedia"
end

value = value .. "|" .. i18n['info']['edit-on-wikidata'] .. "]]"

return front .. value .. back
end

-- used to create the final output string when it's all done, so that for references the
-- function extensionTag("ref", ...) is only called when they really ended up in the final output
function Config:concatValues(valuesArray)
local outString = ""
local j, skip

for i = 1, valuesArray do
-- check if this is a reference
if valuesArray[i].refHash then
j = i - 1
skip = false

-- skip this reference if it is part of a continuous row of references that already contains the exact same reference
while valuesArray[j] and valuesArray[j].refHash do
if valuesArray[i].refHash == valuesArray[j].refHash then
skip = true
break
end
j = j - 1
end

if not skip then
-- add tag with the reference's hash as its name (to deduplicate references)
outString = outString .. mw.getCurrentFrame():extensionTag("ref", valuesArray[i][1], {name = valuesArray[i].refHash})
end
else
outString = outString .. valuesArray[i][1]
end
end

return outString
end

function Config:convertUnit(unit, raw, link, short, unitOnly)
local space = " "
local label = ""
local itemID

if unit "" or unit "1" then
return nil
end

if unitOnly then
space = ""
end

itemID = parseWikidataURL(unit)

if itemID then
if itemID == aliasesQ.percentage then
return "%"
else
label = self:getLabel(itemID, raw, link, short)

if label ~= "" then
return space .. label
end
end
end

return ""
end

function State:getValue(snak)
return self.conf:getValue(snak, self.rawValue, self.linked, self.shortName, self.anyLanguage, self.unitOnly, false, self.type:sub(1,2))
end

function Config:getValue(snak, raw, link, short, anyLang, unitOnly, noSpecial, type)
if snak.snaktype == 'value' then
local datatype = snak.datavalue.type
local subtype = snak.datatype
local datavalue = snak.datavalue.value

if datatype 'string' then
if subtype 'url' and link then
-- create link explicitly
if raw then
-- will render as a linked number like [1]
return "[" .. datavalue .. "]"
else
return "[" .. datavalue .. " " .. datavalue .. "]"
end
elseif subtype 'commonsMedia' then
if link then
return buildWikilink("c:File:" .. datavalue, datavalue)
elseif not raw then
return ""
else
return datavalue
end
elseif subtype 'geo-shape' and link then
return buildWikilink("c:" .. datavalue, datavalue)
elseif subtype == 'math' and not raw then
local attribute = nil

if (type parameters.property or (type parameters.qualifier and self.propertyID aliasesP.hasPart)) and snak.property aliasesP.definingFormula then
attribute = {qid = self.entityID}
end

return mw.getCurrentFrame():extensionTag("math", datavalue, attribute)
elseif subtype == 'external-id' and link then
local url = p._property{aliasesP.formatterURL, [p.args.eid] = snak.property} -- get formatter URL

if url ~= "" then
url = mw.ustring.gsub(url, "$1", datavalue)
return "[" .. url .. " " .. datavalue .. "]"
else
return datavalue
end
else
return datavalue
end
elseif datatype 'monolingualtext' then
if anyLang or datavalue['language'] self.langCode then
return datavalue['text']
else
return nil
end
elseif datatype == 'quantity' then
local value = ""
local unit

if not unitOnly then
-- get value and strip + signs from front
value = mw.ustring.gsub(datavalue['amount'], "^%+(.+)$", "%1")

if raw then
return value
end

-- replace decimal mark based on locale
value = replaceDecimalMark(value)

-- add delimiters for readability
value = i18n.addDelimiters(value)
end

unit = self:convertUnit(datavalue['unit'], raw, link, short, unitOnly)

if unit then
value = value .. unit
end

return value
elseif datatype == 'time' then
local y, m, d, p, yDiv, yRound, yFull, value, calendarID, dateStr
local yFactor = 1
local sign = 1
local prefix = ""
local suffix = ""
local mayAddCalendar = false
local calendar = ""
local precision = datavalue['precision']

if precision == 11 then
p = "d"
elseif precision == 10 then
p = "m"
else
p = "y"
yFactor = 10^(9-precision)
end

y, m, d = parseDate(datavalue['time'], p)

if y = 6 then
mayAddCalendar = true

if precision = 1 and y ~= 0 then
yFull = yRound yFactor

yReFactor = 1e9
yReDiv = yFull / yReFactor
yReRound = math.floor(yReDiv + 0.5)

if yReDiv == yReRound then
-- change precision to billions of years if we're in that range
precision = 0
yFactor = yReFactor
yRound = yReRound
else
yReFactor = 1e6
yReDiv = yFull / yReFactor
yReRound = math.floor(yReDiv + 0.5)

if yReDiv == yReRound then
-- change precision to millions of years if we're in that range
precision = 3
yFactor = yReFactor
yRound = yReRound
end
end
end

if not raw then
if precision == 3 then
suffix = i18n['datetime']['suffixes']['million-years']
elseif precision == 0 then
suffix = i18n['datetime']['suffixes']['billion-years']
else
yRound = yRound yFactor
if yRound == 1 then
suffix = i18n['datetime']['suffixes']['year']
else
suffix = i18n['datetime']['suffixes']['years']
end
end
else
yRound = yRound yFactor
end
end
else
yRound = y
mayAddCalendar = true
end

if mayAddCalendar then
calendarID = parseWikidataURL(datavalue['calendarmodel'])

if calendarID and calendarID == aliasesQ.prolepticJulianCalendar then
if not raw then
if link then
calendar = " ("..buildWikilink(i18n['datetime']['julian-calendar'], i18n['datetime']['julian'])..")"
else
calendar = " ("..i18n['datetime']['julian']..")"
end
else
calendar = "/"..i18n['datetime']['julian']
end
end
end

if not raw then
local ce = nil

if sign = 1 - (1 / 60) and precision = (1 / 60) - (1 / 3600) and precision = 1 then
unitsPerDegree = 1
elseif precision >= (1 / 60) then
unitsPerDegree = 60
else
unitsPerDegree = 3600
end

numDigits = math.ceil(-math.log10(unitsPerDegree precision))

if numDigits = 1 then
latDegrees = strFormat:format(latitude)
lonDegrees = strFormat:format(longitude)

if not raw then
latDegSym = replaceDecimalMark(latDegrees) .. degSymbol
lonDegSym = replaceDecimalMark(lonDegrees) .. degSymbol
else
latDegSym = latDegrees .. degSymbol
lonDegSym = lonDegrees .. degSymbol
end
else
latConv = math.floor(latitude unitsPerDegree 10^numDigits + 0.5) / 10^numDigits
lonConv = math.floor(longitude unitsPerDegree 10^numDigits + 0.5) / 10^numDigits

if precision >= (1 / 60) then
latMinutes = latConv
lonMinutes = lonConv
else
latSeconds = latConv
lonSeconds = lonConv

latMinutes = math.floor(latSeconds / 60)
lonMinutes = math.floor(lonSeconds / 60)

latSeconds = strFormat:format(latSeconds - (latMinutes 60))
lonSeconds = strFormat:format(lonSeconds - (lonMinutes 60))

if not raw then
latSecSym = replaceDecimalMark(latSeconds) .. secSymbol
lonSecSym = replaceDecimalMark(lonSeconds) .. secSymbol
else
latSecSym = latSeconds .. secSymbol
lonSecSym = lonSeconds .. secSymbol
end
end

latDegrees = math.floor(latMinutes / 60)
lonDegrees = math.floor(lonMinutes / 60)

latDegSym = latDegrees .. degSymbol
lonDegSym = lonDegrees .. degSymbol

latMinutes = latMinutes - (latDegrees 60)
lonMinutes = lonMinutes - (lonDegrees 60)

if precision >= (1 / 60) then
latMinutes = strFormat:format(latMinutes)
lonMinutes = strFormat:format(lonMinutes)

if not raw then
latMinSym = replaceDecimalMark(latMinutes) .. minSymbol
lonMinSym = replaceDecimalMark(lonMinutes) .. minSymbol
else
latMinSym = latMinutes .. minSymbol
lonMinSym = lonMinutes .. minSymbol
end
else
latMinSym = latMinutes .. minSymbol
lonMinSym = lonMinutes .. minSymbol
end
end

latValue = latDegSym .. latMinSym .. latSecSym .. latDirection
lonValue = lonDegSym .. lonMinSym .. lonSecSym .. lonDirection

value = latValue .. separator .. lonValue

if link then
globe = parseWikidataURL(datavalue['globe'])

if globe then
globe = mw.wikibase.getLabelByLang(globe, "en"):lower()
else
globe = "earth"
end

latLink = table.concat({latDegrees, latMinutes, latSeconds}, "_")
lonLink = table.concat({lonDegrees, lonMinutes, lonSeconds}, "_")

value = ""..value..""
end

return value
elseif datatype == 'wikibase-entityid' then
local label
local itemID = datavalue['numeric-id']

if subtype == 'wikibase-item' then
itemID = "Q" .. itemID
elseif subtype == 'wikibase-property' then
itemID = "P" .. itemID
else
return .. errorText('unknown-data-type', subtype) ..
end

label = self:getLabel(itemID, raw, link, short)

if label == "" then
label = nil
end

return label
else
return .. errorText('unknown-data-type', datatype) ..
end
elseif snak.snaktype 'somevalue' and not noSpecial then
if raw then
return " " -- single space represents 'somevalue'
else
return i18n['values']['unknown']
end
elseif snak.snaktype 'novalue' and not noSpecial then
if raw then
return "" -- empty string represents 'novalue'
else
return i18n['values']['none']
end
else
return nil
end
end

function Config:getSingleRawQualifier(claim, qualifierID)
local qualifiers

if claim.qualifiers then qualifiers = claim.qualifiers[qualifierID] end

if qualifiers and qualifiers[1] then
return self:getValue(qualifiers[1], true) -- raw = true
else
return nil
end
end

function Config:snakEqualsValue(snak, value)
local snakValue = self:getValue(snak, true) -- raw = true

if snakValue and snak.snaktype 'value' and snak.datavalue.type 'wikibase-entityid' then value = value:upper() end

return snakValue value
end

function Config:setRank(rank)
local rankPos

if rank p.flags.best then
self.bestRank = true
self.flagBest = true -- mark that 'best' flag was given
return
end

if rank:sub(1,9) == p.flags.preferred then
rankPos = 1
elseif rank:sub(1,6) == p.flags.normal then
rankPos = 2
elseif rank:sub(1,10) == p.flags.deprecated then
rankPos = 3
else
return
end

-- one of the rank flags was given, check if another one was given before
if not self.flagRank then
self.ranks = {false, false, false} -- no other rank flag given before, so unset ranks
self.bestRank = self.flagBest -- unsets bestRank only if 'best' flag was not given before
self.flagRank = true -- mark that a rank flag was given
end

if rank:sub(-1) == "+" then
for i = rankPos, 1, -1 do
self.ranks[i] = true
end
elseif rank:sub(-1) == "-" then
for i = rankPos, self.ranks do
self.ranks[i] = true
end
else
self.ranks[rankPos] = true
end
end

function Config:setPeriod(period)
local periodPos

if period == p.flags.future then
periodPos = 1
elseif period == p.flags.current then
periodPos = 2
elseif period == p.flags.former then
periodPos = 3
else
return
end

-- one of the period flags was given, check if another one was given before
if not self.flagPeriod then
self.periods = {false, false, false} -- no other period flag given before, so unset periods
self.flagPeriod = true -- mark that a period flag was given
end

self.periods[periodPos] = true
end

function Config:qualifierMatches(claim, id, value)
local qualifiers

if claim.qualifiers then qualifiers = claim.qualifiers[id] end
if qualifiers then
for _, v in pairs(qualifiers) do
if self:snakEqualsValue(v, value) then
return true
end
end
elseif value == "" then
-- if the qualifier is not present then treat it the same as the special value 'novalue'
return true
end

return false
end

function Config:rankMatches(rankPos)
if self.bestRank then
return (self.ranks[rankPos] and self.foundRank >= rankPos)
else
return self.ranks[rankPos]
end
end

function Config:timeMatches(claim)
local startTime = nil
local startTimeY = nil
local startTimeM = nil
local startTimeD = nil
local endTime = nil
local endTimeY = nil
local endTimeM = nil
local endTimeD = nil

if self.periods[1] and self.periods[2] and self.periods[3] then
-- any time
return true
end

startTime = self:getSingleRawQualifier(claim, aliasesP.startTime)
if startTime and startTime ~= "" and startTime ~= " " then
startTimeY, startTimeM, startTimeD = parseDate(startTime)
end

endTime = self:getSingleRawQualifier(claim, aliasesP.endTime)
if endTime and endTime ~= "" and endTime ~= " " then
endTimeY, endTimeM, endTimeD = parseDate(endTime)
end

if startTimeY ~= nil and endTimeY ~= nil and datePrecedesDate(endTimeY, endTimeM, endTimeD, startTimeY, startTimeM, startTimeD) then
-- invalidate end time if it precedes start time
endTimeY = nil
endTimeM = nil
endTimeD = nil
end

if self.periods[1] then
-- future
if startTimeY and datePrecedesDate(self.atDate[1], self.atDate[2], self.atDate[3], startTimeY, startTimeM, startTimeD) then
return true
end
end

if self.periods[2] then
-- current
if (startTimeY nil or not datePrecedesDate(self.atDate[1], self.atDate[2], self.atDate[3], startTimeY, startTimeM, startTimeD)) and
(endTimeY nil or datePrecedesDate(self.atDate[1], self.atDate[2], self.atDate[3], endTimeY, endTimeM, endTimeD)) then
return true
end
end

if self.periods[3] then
-- former
if endTimeY and not datePrecedesDate(self.atDate[1], self.atDate[2], self.atDate[3], endTimeY, endTimeM, endTimeD) then
return true
end
end

return false
end

function Config:processFlag(flag)
if not flag then
return false
end

if flag == p.flags.linked then
self.curState.linked = true
return true
elseif flag == p.flags.raw then
self.curState.rawValue = true

if self.curState == self.states[parameters.reference] then
-- raw reference values end with periods and require a separator (other than none)
self.separators["sep%r"][1] = {" "}
end

return true
elseif flag == p.flags.short then
self.curState.shortName = true
return true
elseif flag == p.flags.multilanguage then
self.curState.anyLanguage = true
return true
elseif flag == p.flags.unit then
self.curState.unitOnly = true
return true
elseif flag == p.flags.mdy then
self.mdyDate = true
return true
elseif flag == p.flags.single then
self.singleClaim = true
return true
elseif flag == p.flags.sourced then
self.sourcedOnly = true
return true
elseif flag == p.flags.edit then
self.editable = true
return true
elseif flag == p.flags.editAtEnd then
self.editable = true
self.editAtEnd = true
return true
elseif flag p.flags.best or flag:match('^'..p.flags.preferred..'[+-]?$') or flag:match('^'..p.flags.normal..'[+-]?$') or flag:match('^'..p.flags.deprecated..'[+-]?$') then
self:setRank(flag)
return true
elseif flag p.flags.future or flag p.flags.current or flag p.flags.former then
self:setPeriod(flag)
return true
elseif flag == "" then
-- ignore empty flags and carry on
return true
else
return false
end
end

function Config:processFlagOrCommand(flag)
local param = ""

if not flag then
return false
end

if flag p.claimCommands.property or flag p.claimCommands.properties then
param = parameters.property
elseif flag p.claimCommands.qualifier or flag p.claimCommands.qualifiers then
self.states.qualifiersCount = self.states.qualifiersCount + 1
param = parameters.qualifier .. self.states.qualifiersCount
self.separators["sep"..param] = {copyTable(defaultSeparators["sep%q\\d"])}
elseif flag p.claimCommands.reference or flag p.claimCommands.references then
param = parameters.reference
else
return self:processFlag(flag)
end

if self.states[param] then
return false
end

-- create a new state for each command
self.states[param] = State:new(self, param)

-- use "%x" as the general parameter name
self.states[param].parsedFormat = parseFormat(parameters.general) -- will be overwritten for param=="%p"

-- set the separator
self.states[param].separator = self.separators["sep"..param] -- will be nil for param"%p", which will be set separately

if flag p.claimCommands.property or flag p.claimCommands.qualifier or flag p.claimCommands.reference then
self.states[param].singleValue = true
end

self.curState = self.states[param]

return true
end

function Config:processSeparators(args)
local sep

for i, v in pairs(self.separators) do
if args[i] then
sep = replaceSpecialChars(args[i])

if sep ~= "" then
self.separators[i][1] = {sep}
else
self.separators[i][1] = nil
end
end
end
end

function Config:setFormatAndSeparators(state, parsedFormat)
state.parsedFormat = parsedFormat
state.separator = self.separators["sep"]
state.movSeparator = self.separators["sep"..parameters.separator]
state.puncMark = self.separators["punc"]
end

-- determines if a claim has references by prefetching them from the claim using getReferences,
-- which applies some filtering that determines if a reference is actually returned,
-- and caches the references for later use
function State:isSourced(claim)
self.conf.prefetchedRefs = self:getReferences(claim)
return (self.conf.prefetchedRefs > 0)
end

function State:resetCaches()
-- any prefetched references of the previous claim must not be used
self.conf.prefetchedRefs = nil
end

function State:claimMatches(claim)
local matches, rankPos

-- first of all, reset any cached values used for the previous claim
self:resetCaches()

-- if a property value was given, check if it matches the claim's property value
if self.conf.propertyValue then
matches = self.conf:snakEqualsValue(claim.mainsnak, self.conf.propertyValue)
else
matches = true
end

-- if any qualifier values were given, check if each matches one of the claim's qualifier values
for i, v in pairs(self.conf.qualifierIDsAndValues) do
matches = (matches and self.conf:qualifierMatches(claim, i, v))
end

-- check if the claim's rank and time period match
rankPos = rankTable[claim.rank] or 4
matches = (matches and self.conf:rankMatches(rankPos) and self.conf:timeMatches(claim))

-- if only claims with references must be returned, check if this one has any
if self.conf.sourcedOnly then
matches = (matches and self:isSourced(claim)) -- prefetches and caches references
end

return matches, rankPos
end

function State:out()
local result -- collection of arrays with value objects
local valuesArray -- array with value objects
local sep = nil -- value object
local out = {} -- array with value objects

local function walk(formatTable, result)
local valuesArray = {} -- array with value objects

for i, v in pairs(formatTable.req) do
if not result[i] or not result[i][1] then
-- we've got no result for a parameter that is required on this level,
-- so skip this level (and its children) by returning an empty result
return {}
end
end

for _, v in ipairs(formatTable) do
if v.param then
valuesArray = mergeArrays(valuesArray, result[v.str])
elseif v.str ~= "" then
valuesArray[valuesArray + 1] = {v.str}
end

if v.child then
valuesArray = mergeArrays(valuesArray, walk(v.child, result))
end
end

return valuesArray
end

-- iterate through the results from back to front, so that we know when to add separators
for i = self.results, 1, -1 do
result = self.results[i]

-- if there is already some output, then add the separators
if out > 0 then
sep = self.separator[1] -- fixed separator
result[parameters.separator] = {self.movSeparator[1]} -- movable separator
else
sep = nil
result[parameters.separator] = {self.puncMark[1]} -- optional punctuation mark
end

valuesArray = walk(self.parsedFormat, result)

if valuesArray > 0 then
if sep then
valuesArray[valuesArray + 1] = sep
end

out = mergeArrays(valuesArray, out)
end
end

-- reset state before next iteration
self.results = {}

return out
end

-- level 1 hook
function State:getProperty(claim)
local value = {self:getValue(claim.mainsnak)} -- create one value object

if value > 0 then
return {value} -- wrap the value object in an array and return it
else
return {} -- return empty array if there was no value
end
end

-- level 1 hook
function State:getQualifiers(claim, param)
local qualifiers

if claim.qualifiers then qualifiers = claim.qualifiers[self.conf.qualifierIDs[param]] end
if qualifiers then
-- iterate through claim's qualifier statements to collect their values;
-- return array with multiple value objects
return self.conf.states[param]:iterate(qualifiers, {[parameters.general] = hookNames[parameters.qualifier.."\\d"][2], count = 1}) -- pass qualifier state with level 2 hook
else
return {} -- return empty array
end
end

-- level 2 hook
function State:getQualifier(snak)
local value = {self:getValue(snak)} -- create one value object

if value > 0 then
return {value} -- wrap the value object in an array and return it
else
return {} -- return empty array if there was no value
end
end

-- level 1 hook
function State:getAllQualifiers(claim, param, result, hooks)
local out = {} -- array with value objects
local sep = self.conf.separators["sep"..parameters.qualifier][1] -- value object

-- iterate through the output of the separate "qualifier(s)" commands
for i = 1, self.conf.states.qualifiersCount do

-- if a hook has not been called yet, call it now
if not result[parameters.qualifier..i] then
self:callHook(parameters.qualifier..i, hooks, claim, result)
end

-- if there is output for this particular "qualifier(s)" command, then add it
if result[parameters.qualifier..i] and result[parameters.qualifier..i][1] then

-- if there is already some output, then add the separator
if out > 0 and sep then
out[out + 1] = sep
end

out = mergeArrays(out, result[parameters.qualifier..i])
end
end

return out
end

-- level 1 hook
function State:getReferences(claim)
if self.conf.prefetchedRefs then
-- return references that have been prefetched by isSourced
return self.conf.prefetchedRefs
end

if claim.references then
-- iterate through claim's reference statements to collect their values;
-- return array with multiple value objects
return self.conf.states[parameters.reference]:iterate(claim.references, {[parameters.general] = hookNames[parameters.reference][2], count = 1}) -- pass reference state with level 2 hook
else
return {} -- return empty array
end
end

-- level 2 hook
function State:getReference(statement)
local citeParamMapping = i18n['cite']['param-mapping']
local citeConfig = i18n['cite']['config']
local citeTypes = i18n['cite']['output-types']

-- will hold rendered properties of the reference which are not directly from statement.snaks,
-- Namely, these are a backup title from "subject named as" and a URL generated from an external ID.
local additionalProcessedProperties = {}
-- for each citation type, there will be an associative array that associates lists of rendered properties
-- to citation-template parameters
local groupedProcessedProperties = {}
-- like above, but only associates one rendered property to each parameter; if the above variable
-- contains more strings for a parameter, the strings will be assigned to numbered params (e.g. "author1")
local citeParams = {}

local citeErrors = {}
local referenceEmpty = true -- will be set to false if at least one parameter is left unremoved

local version = 11 -- increment this each time the below logic is changed to avoid conflict errors

if not statement.snaks then
return {}
end

-- don't use bot-added references referencing Wikimedia projects or containing "inferred from" (such references are not usable on Wikipedia)
if statement.snaks[aliasesP.importedFrom] or statement.snaks[aliasesP.wikimediaImportURL] or statement.snaks[aliasesP.inferredFrom] then
return {}
end

-- don't include "type of reference"
if statement.snaks[aliasesP.typeOfReference] then
statement.snaks[aliasesP.typeOfReference] = nil
end

-- don't include "image" to prevent littering
if statement.snaks[aliasesP.image] then
statement.snaks[aliasesP.image] = nil
end

-- don't include "language" if it is equal to the local one
if self:getReferenceDetail(statement.snaks, aliasesP.language) == self.conf.langName then
statement.snaks[aliasesP.language] = nil
end

if statement.snaks[aliasesP.statedIn] and not statement.snaks[aliasesP.referenceURL] then
-- "stated in" was given but "reference URL" was not.
-- get "Wikidata property" properties from the item in "stated in"
-- if any of the returned properties of the external-id datatype is in statement.snaks, generate a link from it and use the link in the reference

-- find the "Wikidata property" properties in the item from "stated in"
local wikidataPropertiesOfSource = mw.text.split(p._properties{p.flags.raw, aliasesP.wikidataProperty, [p.args.eid] = self.conf:getValue(statement.snaks[aliasesP.statedIn][1], true, false)}, ", ", true)
for i, wikidataPropertyOfSource in pairs(wikidataPropertiesOfSource) do
if statement.snaks[wikidataPropertyOfSource] and statement.snaks[wikidataPropertyOfSource][1].datatype == "external-id" then
local tempLink = self:getReferenceDetail(statement.snaks, wikidataPropertyOfSource, false, true) -- not raw, linked
if mw.ustring.match(tempLink, "^%[%Z- %Z+%]$") then -- getValue returned a URL in square brackets.
-- the link is in wiki markup, so strip the square brackets and the display text
-- gsub also returns another, discarted value, therefore the result is assigned to tempLink first
tempLink = mw.ustring.gsub(tempLink, "^%[(%Z-) %Z+%]$", "%1")
additionalProcessedProperties[aliasesP.referenceURL] = {tempLink}
statement.snaks[wikidataPropertyOfSource] = nil
break
end
end
end
end

-- don't include "subject named as", but use it as the title when "title" is not present but a URL is
if statement.snaks[aliasesP.subjectNamedAs] then
if not statement.snaks[aliasesP.title] and (statement.snaks[aliasesP.referenceURL] or additionalProcessedProperties[aliasesP.referenceURL]) then
additionalProcessedProperties[aliasesP.title] = {self:getReferenceDetail(statement.snaks, aliasesP.subjectNamedAs, false, false, true)} -- not raw, not linked, anyLang
end
statement.snaks[aliasesP.subjectNamedAs] = nil
end

-- initialize groupedProcessedProperties and citeParams
for _, citeType in ipairs(citeTypes) do
groupedProcessedProperties[citeType] = {}
citeParams[citeType] = {}
end

-- fill groupedProcessedProperties
for refProperty in pairs(statement.snaks) do
-- add the parameter to each matching type of citation
for _, citeType in ipairs(citeTypes) do
repeat -- just a simple wrapper to emulate "continue"
-- skip if there already have been errors
if citeErrors[citeType] then
break
end

-- set mappingKey and prefix
local mappingKey
local prefix = ""
if statement.snaks[refProperty][1].datatype == 'external-id' then
mappingKey = "external-id"
prefix = self.conf:getLabel(refProperty)

if prefix ~= "" then
prefix = prefix .. " "
end
else
mappingKey = refProperty
end

local paramName = citeParamMapping[citeType][mappingKey]
-- skip properties with empty parameter name
if paramName == "" then
break
end

referenceEmpty = false

-- handle unknown properties in the reference
if not paramName then
local error_message = errorText("unknown-property-in-ref", refProperty)
assert(error_message) -- Should not be nil
citeErrors[citeType] = error_message
break
end

-- set processedProperty
local processedProperty
local raw = false -- if the value is wanted raw
if isValueInTable(paramName, citeConfig[citeType]["raw-value-params"] or {}) then
raw = true
end
if isValueInTable(paramName, citeConfig[citeType]["numbered-params"] or {}) then
-- Multiple values may be given.
processedProperty = self:getReferenceDetails(statement.snaks, refProperty, raw, self.linked, true) -- anyLang = true
else
-- If multiple values are given, all but the first suitable one are discarted.
processedProperty = {self:getReferenceDetail(statement.snaks, refProperty, raw, self.linked and (statement.snaks[refProperty][1].datatype ~= 'url'), true)} -- link = true/false, anyLang = true
end

if processedProperty == 0 then
break
end

-- add an entry to groupedProcessedProperties
if not groupedProcessedProperties[citeType][paramName] then
groupedProcessedProperties[citeType][paramName] = {}
end
for _, propertyValue in pairs(processedProperty) do
table.insert(groupedProcessedProperties[citeType][paramName], prefix .. propertyValue)
end
until true
end
end

-- handle additional properties
for refProperty in pairs(additionalProcessedProperties) do
for _, citeType in ipairs(citeTypes) do
repeat
-- skip if there already have been errors
if citeErrors[citeType] then
break
end

local paramName = citeParamMapping[citeType][refProperty]
-- handle unknown properties in the reference
if not paramName then
-- Skip this additional property, but do not cause an error.
break
end
if paramName == "" then
break
end

referenceEmpty = false

if not groupedProcessedProperties[citeType][paramName] then
groupedProcessedProperties[citeType][paramName] = {}
end
for _, propertyValue in pairs(additionalProcessedProperties[refProperty]) do
table.insert(groupedProcessedProperties[citeType][paramName], propertyValue)
end
until true
end
end

-- fill citeParams
for _, citeType in ipairs(citeTypes) do
for paramName, paramValues in pairs(groupedProcessedProperties[citeType]) do
if paramValues == 1 or not isValueInTable(paramName, citeConfig[citeType]["numbered-params"] or {}) then
citeParams[citeType][paramName] = paramValues[1]
else
-- There is more than one value for this parameter - the values will
-- go into separate numbered parameters (e.g. "author1", "author2")
for paramNum, paramValue in pairs(paramValues) do
citeParams[citeType][paramName .. paramNum] = paramValue
end
end
end
end

-- handle missing mandatory parameters for the templates
for _, citeType in ipairs(citeTypes) do
for _, requiredCiteParam in pairs(citeConfig[citeType]["mandatory-params"] or {}) do
if not citeParams[citeType][requiredCiteParam] then -- The required param is not present.
if citeErrors[citeType] then -- Do not override the previous error, if it exists.
break
end
local error_message = errorText("missing-mandatory-param", requiredCiteParam)
assert(error_message) -- Should not be nil
citeErrors[citeType] = error_message
end
end
end

local citeTypeToUse = nil

-- choose the output template
for _, citeType in ipairs(citeTypes) do
if not citeErrors[citeType] then
citeTypeToUse = citeType
break
end
end

-- set refContent
local refContent = ""
if citeTypeToUse then
local templateToUse = citeConfig[citeTypeToUse]["template"]
local paramsToUse = citeParams[citeTypeToUse]

if not templateToUse or templateToUse == "" then
throwError("no-such-reference-template", tostring(templateToUse), i18nPath, citeTypeToUse)
end

-- if this module is being substituted then build a regular template call, otherwise expand the template
if mw.isSubsting() then
for i, v in pairs(paramsToUse) do
refContent = refContent .. "|" .. i .. "=" .. v
end

refContent = ""
else
xpcall(
function () refContent = mw.getCurrentFrame():expandTemplate{title=templateToUse, args=paramsToUse} end,
function () throwError("no-such-reference-template", templateToUse, i18nPath, citeTypeToUse) end
)
end

-- If the citation couldn't be displayed using any template, but is not empty (barring ignored propeties), throw an error.
elseif not referenceEmpty then
refContent = errorText("malformed-reference-header")
for _, citeType in ipairs(citeTypes) do
refContent = refContent .. errorText("template-failure-reason", citeConfig[citeType]["template"], citeErrors[citeType])
end
refContent = refContent .. errorText("malformed-reference-footer")
end

-- wrap refContent
local ref = {}
if refContent ~= "" then
ref = {refContent}

if not self.rawValue then
-- this should become a tag, so save the reference's hash for later
ref.refHash = "wikidata-" .. statement.hash .. "-v" .. (tonumber(i18n['version']) + version)
end
return {ref}
else
return {}
end
end

-- gets a detail of one particular type for a reference
function State:getReferenceDetail(snaks, dType, raw, link, anyLang)
local switchLang = anyLang
local value = nil

if not snaks[dType] then
return nil
end

-- if anyLang, first try the local language and otherwise any language
repeat
for _, v in ipairs(snaks[dType]) do
value = self.conf:getValue(v, raw, link, false, anyLang and not switchLang, false, true) -- noSpecial = true

if value then
break
end
end

if value or not anyLang then
break
end

switchLang = not switchLang
until anyLang and switchLang

return value
end

-- gets the details of one particular type for a reference
function State:getReferenceDetails(snaks, dType, raw, link, anyLang)
local values = {}

if not snaks[dType] then
return {}
end

for _, v in ipairs(snaks[dType]) do
-- if nil is returned then it will not be added to the table
values[values + 1] = self.conf:getValue(v, raw, link, false, anyLang, false, true) -- noSpecial = true
end

return values
end

-- level 1 hook
function State:getAlias(object)
local value = object.value
local title = nil

if value and self.linked then
if self.conf.entityID:sub(1,1) == "Q" then
title = mw.wikibase.getSitelink(self.conf.entityID)
elseif self.conf.entityID:sub(1,1) == "P" then
title = "d:Property:" .. self.conf.entityID
end

if title then
value = buildWikilink(title, value)
end
end

value = {value} -- create one value object

if value > 0 then
return {value} -- wrap the value object in an array and return it
else
return {} -- return empty array if there was no value
end
end

-- level 1 hook
function State:getBadge(value)
value = self.conf:getLabel(value, self.rawValue, self.linked, self.shortName)

if value == "" then
value = nil
end

value = {value} -- create one value object

if value > 0 then
return {value} -- wrap the value object in an array and return it
else
return {} -- return empty array if there was no value
end
end

function State:callHook(param, hooks, statement, result)
local valuesArray, refHash

-- call a parameter's hook if it has been defined and if it has not been called before
if not result[param] and hooks[param] then
valuesArray = self[hooks[param]](self, statement, param, result, hooks) -- array with value objects

-- add to the result
if valuesArray > 0 then
result[param] = valuesArray
result.count = result.count + 1
else
result[param] = {} -- an empty array to indicate that we've tried this hook already
return true -- miss == true
end
end

return false
end

-- iterate through claims, claim's qualifiers or claim's references to collect values
function State:iterate(statements, hooks, matchHook)
matchHook = matchHook or alwaysTrue

local matches = false
local rankPos = nil
local result, gotRequired

for _, v in ipairs(statements) do
-- rankPos will be nil for non-claim statements (e.g. qualifiers, references, etc.)
matches, rankPos = matchHook(self, v)

if matches then
result = {count = 0} -- collection of arrays with value objects

local function walk(formatTable)
local miss

for i2, v2 in pairs(formatTable.req) do
-- call a hook, adding its return value to the result
miss = self:callHook(i2, hooks, v, result)

if miss then
-- we miss a required value for this level, so return false
return false
end

if result.count hooks.count then
-- we're done if all hooks have been called;
-- returning at this point breaks the loop
return true
end
end

for _, v2 in ipairs(formatTable) do
if result.count hooks.count then
-- we're done if all hooks have been called;
-- returning at this point prevents further childs from being processed
return true
end

if v2.child then
walk(v2.child)
end
end

return true
end
gotRequired = walk(self.parsedFormat)

-- only append the result if we got values for all required parameters on the root level
if gotRequired then
-- if we have a rankPos (only with matchHook() for complete claims), then update the foundRank
if rankPos and self.conf.foundRank > rankPos then
self.conf.foundRank = rankPos
end

-- append the result
self.results[self.results + 1] = result

-- break if we only need a single value
if self.singleValue then
break
end
end
end
end

return self:out()
end

local function getEntityId(arg, eid, page, allowOmitPropPrefix, globalSiteId)
local id = nil
local prop = nil

if arg then
if arg:sub(1,1) == ":" then
page = arg
eid = nil
elseif arg:sub(1,1):upper() "Q" or arg:sub(1,9):lower() "property:" or allowOmitPropPrefix then
eid = arg
page = nil
else
prop = arg
end
end

if eid then
if eid:sub(1,9):lower() == "property:" then
id = replaceAlias(mw.text.trim(eid:sub(10)))

if id:sub(1,1):upper() ~= "P" then
id = ""
end
else
id = replaceAlias(eid)
end
elseif page then
if page:sub(1,1) == ":" then
page = mw.text.trim(page:sub(2))
end

id = mw.wikibase.getEntityIdForTitle(page, globalSiteId) or ""
end

if not id then
id = mw.wikibase.getEntityIdForCurrentPage() or ""
end

id = id:upper()

if not mw.wikibase.isValidEntityId(id) then
id = ""
end

return id, prop
end

local function nextArg(args)
local arg = args[args.pointer]

if arg then
args.pointer = args.pointer + 1
return mw.text.trim(arg)
else
return nil
end
end

local function claimCommand(args, funcName)
local cfg = Config:new()
cfg:processFlagOrCommand(funcName) -- process first command (== function name)

local lastArg, parsedFormat, formatParams, claims, value
local hooks = {count = 0}

-- set the date if given;
-- must come BEFORE processing the flags
if args[p.args.date] then
cfg.atDate = {parseDate(args[p.args.date])}
cfg.periods = {false, true, false} -- change default time constraint to 'current'
end

-- process flags and commands
repeat
lastArg = nextArg(args)
until not cfg:processFlagOrCommand(lastArg)

-- get the entity ID from either the positional argument, the eid argument or the page argument
cfg.entityID, cfg.propertyID = getEntityId(lastArg, args[p.args.eid], args[p.args.page], false, args[p.args.globalSiteId])

if cfg.entityID == "" then
return "" -- we cannot continue without a valid entity ID
end

cfg.entity = mw.wikibase.getEntity(cfg.entityID)

if not cfg.propertyID then
cfg.propertyID = nextArg(args)
end

cfg.propertyID = replaceAlias(cfg.propertyID)

if not cfg.entity or not cfg.propertyID then
return "" -- we cannot continue without an entity or a property ID
end

cfg.propertyID = cfg.propertyID:upper()

if not cfg.entity.claims or not cfg.entity.claims[cfg.propertyID] then
return "" -- there is no use to continue without any claims
end

claims = cfg.entity.claims[cfg.propertyID]

if cfg.states.qualifiersCount > 0 then
-- do further processing if "qualifier(s)" command was given

if args - args.pointer + 1 > cfg.states.qualifiersCount then
-- claim ID or literal value has been given

cfg.propertyValue = nextArg(args)
end

for i = 1, cfg.states.qualifiersCount do
-- check if given qualifier ID is an alias and add it
cfg.qualifierIDs[parameters.qualifier..i] = replaceAlias(nextArg(args) or ""):upper()
end
elseif cfg.states[parameters.reference] then
-- do further processing if "reference(s)" command was given

cfg.propertyValue = nextArg(args)
end

-- check for special property value 'somevalue' or 'novalue'
if cfg.propertyValue then
cfg.propertyValue = replaceSpecialChars(cfg.propertyValue)

if cfg.propertyValue ~= "" and mw.text.trim(cfg.propertyValue) == "" then
cfg.propertyValue = " " -- single space represents 'somevalue', whereas empty string represents 'novalue'
else
cfg.propertyValue = mw.text.trim(cfg.propertyValue)
end
end

-- parse the desired format, or choose an appropriate format
if args["format"] then
parsedFormat, formatParams = parseFormat(args["format"])
elseif cfg.states.qualifiersCount > 0 then -- "qualifier(s)" command given
if cfg.states[parameters.property] then -- "propert(y|ies)" command given
parsedFormat, formatParams = parseFormat(formats.propertyWithQualifier)
else
parsedFormat, formatParams = parseFormat(formats.qualifier)
end
elseif cfg.states[parameters.property] then -- "propert(y|ies)" command given
parsedFormat, formatParams = parseFormat(formats.property)
else -- "reference(s)" command given
parsedFormat, formatParams = parseFormat(formats.reference)
end

-- if a "qualifier(s)" command and no "propert(y|ies)" command has been given, make the movable separator a semicolon
if cfg.states.qualifiersCount > 0 and not cfg.states[parameters.property] then
cfg.separators["sep"..parameters.separator][1] = {";"}
end

-- if only "reference(s)" has been given, set the default separator to none (except when raw)
if cfg.states[parameters.reference] and not cfg.states[parameters.property] and cfg.states.qualifiersCount == 0
and not cfg.states[parameters.reference].rawValue then
cfg.separators["sep"][1] = nil
end

-- if exactly one "qualifier(s)" command has been given, make "sep%q" point to "sep%q1" to make them equivalent
if cfg.states.qualifiersCount == 1 then
cfg.separators["sep"..parameters.qualifier] = cfg.separators["sep"..parameters.qualifier.."1"]
end

-- process overridden separator values;
-- must come AFTER tweaking the default separators
cfg:processSeparators(args)

-- define the hooks that should be called (getProperty, getQualifiers, getReferences);
-- only define a hook if both its command ("propert(y|ies)", "reference(s)", "qualifier(s)") and its parameter ("%p", "%r", "%q1", "%q2", "%q3") have been given
for i, v in pairs(cfg.states) do
-- e.g. 'formatParams["%q1"] or formatParams["%q"]' to define hook even if "%q1" was not defined to be able to build a complete value for "%q"
if formatParams[i] or formatParams[i:sub(1, 2)] then
hooks[i] = getHookName(i, 1)
hooks.count = hooks.count + 1
end
end

-- the "%q" parameter is not attached to a state, but is a collection of the results of multiple states (attached to "%q1", "%q2", "%q3", ...);
-- so if this parameter is given then this hook must be defined separately, but only if at least one "qualifier(s)" command has been given
if formatParams[parameters.qualifier] and cfg.states.qualifiersCount > 0 then
hooks[parameters.qualifier] = getHookName(parameters.qualifier, 1)
hooks.count = hooks.count + 1
end

-- create a state for "properties" if it doesn't exist yet, which will be used as a base configuration for each claim iteration;
-- must come AFTER defining the hooks
if not cfg.states[parameters.property] then
cfg.states[parameters.property] = State:new(cfg, parameters.property)

-- if the "single" flag has been given then this state should be equivalent to "property" (singular)
if cfg.singleClaim then
cfg.states[parameters.property].singleValue = true
end
end

-- if the "sourced" flag has been given then create a state for "reference" if it doesn't exist yet, using default values,
-- which must exist in order to be able to determine if a claim has any references;
-- must come AFTER defining the hooks
if cfg.sourcedOnly and not cfg.states[parameters.reference] then
cfg:processFlagOrCommand(p.claimCommands.reference) -- use singular "reference" to minimize overhead
end

-- set the parsed format and the separators (and optional punctuation mark);
-- must come AFTER creating the additonal states
cfg:setFormatAndSeparators(cfg.states[parameters.property], parsedFormat)

-- process qualifier matching values, analogous to cfg.propertyValue
for i, v in pairs(args) do
i = tostring(i)

if i:match('^[Pp]%d+$') or aliasesP[i] then
v = replaceSpecialChars(v)

-- check for special qualifier value 'somevalue'
if v ~= "" and mw.text.trim(v) == "" then
v = " " -- single space represents 'somevalue'
end

cfg.qualifierIDsAndValues[replaceAlias(i):upper()] = v
end
end

-- first sort the claims on rank to pre-define the order of output (preferred first, then normal, then deprecated)
claims = sortOnRank(claims)

-- then iterate through the claims to collect values
value = cfg:concatValues(cfg.states[parameters.property]:iterate(claims, hooks, State.claimMatches)) -- pass property state with level 1 hooks and matchHook

-- if desired, add a clickable icon that may be used to edit the returned values on Wikidata
if cfg.editable and value ~= "" then
value = value .. cfg:getEditIcon()
end

return value
end

local function generalCommand(args, funcName)
local cfg = Config:new()
cfg.curState = State:new(cfg)

local lastArg
local value = nil

repeat
lastArg = nextArg(args)
until not cfg:processFlag(lastArg)

-- get the entity ID from either the positional argument, the eid argument or the page argument
cfg.entityID = getEntityId(lastArg, args[p.args.eid], args[p.args.page], true, args[p.args.globalSiteId])

if cfg.entityID "" or not mw.wikibase.entityExists(cfg.entityID) then
return "" -- we cannot continue without an entity
end

-- serve according to the given command
if funcName p.generalCommands.label then
value = cfg:getLabel(cfg.entityID, cfg.curState.rawValue, cfg.curState.linked, cfg.curState.shortName)
elseif funcName == p.generalCommands.title then
cfg.inSitelinks = true

if cfg.entityID:sub(1,1) == "Q" then
value = mw.wikibase.getSitelink(cfg.entityID)
end

if cfg.curState.linked and value then
value = buildWikilink(value)
end
elseif funcName == p.generalCommands.description then
value = mw.wikibase.getDescription(cfg.entityID)
else
local parsedFormat, formatParams
local hooks = {count = 0}

cfg.entity = mw.wikibase.getEntity(cfg.entityID)

if funcName p.generalCommands.alias or funcName p.generalCommands.badge then
cfg.curState.singleValue = true
end

if funcName p.generalCommands.alias or funcName p.generalCommands.aliases then
if not cfg.entity.aliases or not cfg.entity.aliases[cfg.langCode] then
return "" -- there is no use to continue without any aliasses
end

local aliases = cfg.entity.aliases[cfg.langCode]

-- parse the desired format, or parse the default aliases format
if args["format"] then
parsedFormat, formatParams = parseFormat(args["format"])
else
parsedFormat, formatParams = parseFormat(formats.alias)
end

-- process overridden separator values;
-- must come AFTER tweaking the default separators
cfg:processSeparators(args)

-- define the hook that should be called (getAlias);
-- only define the hook if the parameter ("%a") has been given
if formatParams[parameters.alias] then
hooks[parameters.alias] = getHookName(parameters.alias, 1)
hooks.count = hooks.count + 1
end

-- set the parsed format and the separators (and optional punctuation mark)
cfg:setFormatAndSeparators(cfg.curState, parsedFormat)

-- iterate to collect values
value = cfg:concatValues(cfg.curState:iterate(aliases, hooks))
elseif funcName p.generalCommands.badge or funcName p.generalCommands.badges then
if not cfg.entity.sitelinks or not cfg.entity.sitelinks[cfg.siteID] or not cfg.entity.sitelinks[cfg.siteID].badges then
return "" -- there is no use to continue without any badges
end

local badges = cfg.entity.sitelinks[cfg.siteID].badges

cfg.inSitelinks = true

-- parse the desired format, or parse the default aliases format
if args["format"] then
parsedFormat, formatParams = parseFormat(args["format"])
else
parsedFormat, formatParams = parseFormat(formats.badge)
end

-- process overridden separator values;
-- must come AFTER tweaking the default separators
cfg:processSeparators(args)

-- define the hook that should be called (getBadge);
-- only define the hook if the parameter ("%b") has been given
if formatParams[parameters.badge] then
hooks[parameters.badge] = getHookName(parameters.badge, 1)
hooks.count = hooks.count + 1
end

-- set the parsed format and the separators (and optional punctuation mark)
cfg:setFormatAndSeparators(cfg.curState, parsedFormat)

-- iterate to collect values
value = cfg:concatValues(cfg.curState:iterate(badges, hooks))
end
end

value = value or ""

if cfg.editable and value ~= "" then
-- if desired, add a clickable icon that may be used to edit the returned value on Wikidata
value = value .. cfg:getEditIcon()
end

return value
end

-- modules that include this module should call the functions with an underscore prepended, e.g.: p._property(args)
local function establishCommands(commandList, commandFunc)
for _, commandName in pairs(commandList) do
local function wikitextWrapper(frame)
local args = copyTable(frame.args)
args.pointer = 1
loadI18n(aliasesP, frame)
return commandFunc(args, commandName)
end
p[commandName] = wikitextWrapper

local function luaWrapper(args)
args = copyTable(args)
args.pointer = 1
loadI18n(aliasesP)
return commandFunc(args, commandName)
end
p["_" .. commandName] = luaWrapper
end
end

establishCommands(p.claimCommands, claimCommand)
establishCommands(p.generalCommands, generalCommand)

-- main function that is supposed to be used by wrapper templates
function p.main(frame)
if not mw.wikibase then return nil end

local f, args

loadI18n(aliasesP, frame)

-- get the parent frame to take the arguments that were passed to the wrapper template
frame = frame:getParent() or frame

if not frame.args[1] then
throwError("no-function-specified")
end

f = mw.text.trim(frame.args[1])

if f == "main" then
throwError("main-called-twice")
end

assert(p["_"..f], errorText('no-such-function', f))

-- copy arguments from immutable to mutable table
args = copyTable(frame.args)

-- remove the function name from the list
table.remove(args, 1)

return p["_"..f](args)
end

return p