Module: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 < 0 then

negative = true

num = num * -1

end

num = tostring(num)

numZeros = numDigits - num:len()

for _ = 1, numZeros do

num = "0"..num

end

if negative then

num = "-"..num

end

return num

end

local function replaceSpecialChar(chr)

if chr == '_' then

-- replace underscores with spaces

return ' '

else

return chr

end

end

local function replaceSpecialChars(str)

local chr

local esc = false

local strOut = ""

for i = 1, #str do

chr = str:sub(i,i)

if not esc then

if chr == '\\' then

esc = true

else

strOut = strOut .. replaceSpecialChar(chr)

end

else

strOut = strOut .. chr

esc = false

end

end

return strOut

end

local function buildWikilink(target, label)

if not label or target == label then

return "" .. target .. ""

else

return "" .. label .. ""

end

end

-- used to make frame.args mutable, to replace #frame.args (which is always 0)

-- with the actual amount and to simply copy tables

local function copyTable(tIn)

if not tIn then

return nil

end

local tOut = {}

for i, v in pairs(tIn) do

tOut[i] = v

end

return tOut

end

-- used to merge output arrays together;

-- note that it currently mutates the first input array

local function mergeArrays(a1, a2)

for i = 1, #a2 do

a1[#a1 + 1] = a2[i]

end

return a1

end

local function split(str, del)

local out = {}

local i, j = str:find(del)

if i and j then

out[1] = str:sub(1, i - 1)

out[2] = str:sub(j + 1)

else

out[1] = str

end

return out

end

local function parseWikidataURL(url)

local id

if url:match('^http[s]?://') then

id = split(url, "Q")

if id[2] then

return "Q" .. id[2]

end

end

return nil

end

local function parseDate(dateStr, precision)

precision = precision or "d"

local i, j, index, ptr

local parts = {nil, nil, nil}

if dateStr == nil then

return parts[1], parts[2], parts[3] -- year, month, day

end

-- 'T' for snak values, '/' for outputs with '/Julian' attached

i, j = dateStr:find("[T/]")

if i then

dateStr = dateStr:sub(1, i-1)

end

local from = 1

if dateStr:sub(1,1) == "-" then

-- this is a negative number, look further ahead

from = 2

end

index = 1

ptr = 1

i, j = dateStr:find("-", from)

if i then

-- year

parts[index] = tonumber(dateStr:sub(ptr, i-1), 10) -- explicitly give base 10 to prevent error

if parts[index] == -0 then

parts[index] = tonumber("0") -- for some reason, 'parts[index] = 0' may actually store '-0', so parse from string instead

end

if precision == "y" then

-- we're done

return parts[1], parts[2], parts[3] -- year, month, day

end

index = index + 1

ptr = i + 1

i, j = dateStr:find("-", ptr)

if i then

-- month

parts[index] = tonumber(dateStr:sub(ptr, i-1), 10)

if precision == "m" then

-- we're done

return parts[1], parts[2], parts[3] -- year, month, day

end

index = index + 1

ptr = i + 1

end

end

if dateStr:sub(ptr) ~= "" then

-- day if we have month, month if we have year, or year

parts[index] = tonumber(dateStr:sub(ptr), 10)

end

return parts[1], parts[2], parts[3] -- year, month, day

end

local function datePrecedesDate(aY, aM, aD, bY, bM, bD)

if aY == nil or bY == nil then

return nil

end

aM = aM or 1

aD = aD or 1

bM = bM or 1

bD = bD or 1

if aY < bY then

return true

end

if aY > bY then

return false

end

if aM < bM then

return true

end

if aM > bM then

return false

end

if aD < bD then

return true

end

return false

end

local function getHookName(param, index)

if hookNames[param] then

return hookNames[param][index]

elseif param:len() > 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 = " "

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 "File:" .. datavalue .. ""

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 < 0 then

sign = -1

y = y * sign

end

-- if precision is tens/hundreds/thousands/millions/billions of years

if precision <= 8 then

yDiv = y / yFactor

-- if precision is tens/hundreds/thousands of years

if precision >= 6 then

mayAddCalendar = true

if precision <= 7 then

-- round centuries/millenniums up (e.g. 20th century or 3rd millennium)

yRound = math.ceil(yDiv)

if not raw then

if precision == 6 then

suffix = i18n['datetime']['suffixes']['millennium']

else

suffix = i18n['datetime']['suffixes']['century']

end

suffix = i18n.getOrdinalSuffix(yRound) .. suffix

else

-- if not verbose, take the first year of the century/millennium

-- (e.g. 1901 for 20th century or 2001 for 3rd millennium)

yRound = (yRound - 1) * yFactor + 1

end

else

-- precision == 8

-- round decades down (e.g. 2010s)

yRound = math.floor(yDiv) * yFactor

if not raw then

prefix = i18n['datetime']['prefixes']['decade-period']

suffix = i18n['datetime']['suffixes']['decade-period']

end

end

if raw and sign < 0 then

-- if BCE then compensate for "counting backwards"

-- (e.g. -2019 for 2010s BCE, -2000 for 20th century BCE or -3000 for 3rd millennium BCE)

yRound = yRound + yFactor - 1

end

else

local yReFactor, yReDiv, yReRound

-- round to nearest for tens of thousands of years or more

yRound = math.floor(yDiv + 0.5)

if yRound == 0 then

if precision <= 2 and y ~= 0 then

yReFactor = 1e6

yReDiv = y / yReFactor

yReRound = math.floor(yReDiv + 0.5)

if yReDiv == yReRound then

-- change precision to millions of years only if we have a whole number of them

precision = 3

yFactor = yReFactor

yRound = yReRound

end

end

if yRound == 0 then

-- otherwise, take the unrounded (original) number of years

precision = 5

yFactor = 1

yRound = y

mayAddCalendar = true

end

end

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 < 0 then

ce = i18n['datetime']['BCE']

elseif precision <= 5 then

ce = i18n['datetime']['CE']

end

if ce then

if link then

ce = buildWikilink(i18n['datetime']['common-era'], ce)

end

suffix = suffix .. " " .. ce

end

value = tostring(yRound)

if m then

dateStr = self.langObj:formatDate("F", "1-"..m.."-1")

if d then

if self.mdyDate then

dateStr = dateStr .. " " .. d .. ","

else

dateStr = d .. " " .. dateStr

end

end

value = dateStr .. " " .. value

end

value = prefix .. value .. suffix .. calendar

else

value = padZeros(yRound * sign, 4)

if m then

value = value .. "-" .. padZeros(m, 2)

if d then

value = value .. "-" .. padZeros(d, 2)

end

end

value = value .. calendar

end

return value

elseif datatype == 'globecoordinate' then

-- logic from https://github.com/DataValues/Geo (v4.0.1)

local precision, unitsPerDegree, numDigits, strFormat, value, globe

local latitude, latConv, latValue, latLink

local longitude, lonConv, lonValue, lonLink

local latDirection, latDirectionN, latDirectionS, latDirectionEN

local lonDirection, lonDirectionE, lonDirectionW, lonDirectionEN

local degSymbol, minSymbol, secSymbol, separator

local latDegrees = nil

local latMinutes = nil

local latSeconds = nil

local lonDegrees = nil

local lonMinutes = nil

local lonSeconds = nil

local latDegSym = ""

local latMinSym = ""

local latSecSym = ""

local lonDegSym = ""

local lonMinSym = ""

local lonSecSym = ""

local latDirectionEN_N = "N"

local latDirectionEN_S = "S"

local lonDirectionEN_E = "E"

local lonDirectionEN_W = "W"

if not raw then

latDirectionN = i18n['coord']['latitude-north']

latDirectionS = i18n['coord']['latitude-south']

lonDirectionE = i18n['coord']['longitude-east']

lonDirectionW = i18n['coord']['longitude-west']

degSymbol = i18n['coord']['degrees']

minSymbol = i18n['coord']['minutes']

secSymbol = i18n['coord']['seconds']

separator = i18n['coord']['separator']

else

latDirectionN = latDirectionEN_N

latDirectionS = latDirectionEN_S

lonDirectionE = lonDirectionEN_E

lonDirectionW = lonDirectionEN_W

degSymbol = "/"

minSymbol = "/"

secSymbol = "/"

separator = "/"

end

latitude = datavalue['latitude']

longitude = datavalue['longitude']

if latitude < 0 then

latDirection = latDirectionS

latDirectionEN = latDirectionEN_S

latitude = math.abs(latitude)

else

latDirection = latDirectionN

latDirectionEN = latDirectionEN_N

end

if longitude < 0 then

lonDirection = lonDirectionW

lonDirectionEN = lonDirectionEN_W

longitude = math.abs(longitude)

else

lonDirection = lonDirectionE

lonDirectionEN = lonDirectionEN_E

end

precision = datavalue['precision']

if not precision or precision <= 0 then

precision = 1 / 3600 -- precision not set (correctly), set to arcsecond

end

-- remove insignificant detail

latitude = math.floor(latitude / precision + 0.5) * precision

longitude = math.floor(longitude / precision + 0.5) * precision

if precision >= 1 - (1 / 60) and precision < 1 then

precision = 1

elseif precision >= (1 / 60) - (1 / 3600) and precision < (1 / 60) then

precision = 1 / 60

end

if 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 <= 0 then

numDigits = tonumber("0") -- for some reason, 'numDigits = 0' may actually store '-0', so parse from string instead

end

strFormat = "%." .. numDigits .. "f"

if precision >= 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 = "[https://geohack.toolforge.org/geohack.php?language="..self.langCode.."¶ms="..latLink.."_"..latDirectionEN.."_"..lonLink.."_"..lonDirectionEN.."_globe:"..globe.." "..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 = "{{" .. templateToUse .. 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)

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

local 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