Module:Sandbox/Thayts/Wd

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

local p = {} local arg = ... local i18n

--==-- Public declarations and initializations --==--

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", number       = "number", -	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", sort = "sort" }

--==-- Public constants --==--

-- An Ogham space that, just like a normal space, is not accepted by Wikidata as a valid single-character string value, -- but which does not get trimmed as leading/trailing whitespace when passed in an invocation's named argument value. -- This allows it to be used as a special character representing the special value 'somevalue' unambiguously. -- Another advantage of this character is that it is usually visible as a dash instead of whitespace. p.SOMEVALUE = " " p.JULIAN = "Julian"

--==-- Private constants --==--

local NB_SPACE    = "&#160;" local ENC_PIPE    = "&#124;" local SLASH       = "/" local LAT_DIR_N_EN = "N" local LAT_DIR_S_EN = "S" local LON_DIR_E_EN = "E" local LON_DIR_W_EN = "W" local PROP        = "prop" local RANK        = "rank" local CLAIM       = "_claim" local REFERENCE   = "_reference" local UNIT        = "_unit" local UNKNOWN     = "_unknown"

--==-- Private declarations and initializations --==--

local aliasesP = { coord                  = "P625", ---	image                  = "P18", author                 = "P50", publisher              = "P123", importedFrom           = "P143", 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" }

local aliasesQ = { julianCalendar         = "Q11184", percentage             = "Q11229", commonEra              = "Q208141", prolepticJulianCalendar = "Q1985786", citeWeb                = "Q5637226", citeQ                  = "Q22321052" }

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

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

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

local defaultSeparators = { ["sep"]   = " ", ["sep%s"] = ",", ["sep%q"] = "; ", ["sep%q0"] = ", ", ["sep%r"] = "",  -- none ["punc"]  = ""   -- none }

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

--==-- Private functions --==--

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

-- used to make frame.args mutable, to replace #frame.args (which is always 0) -- with the actual amount and to simply copy tables; -- does a shallow copy, so nested tables are not copied but linked 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

-- implementation of pairs that skips numeric keys local function npairs(t) return function(t, k)		local v

repeat k, v = next(t, k)		until k == nil or type(k) ~= 'number'

return k, v	end, t, nil end

local function toString(object, insideRef, refs) local mt, value

insideRef = insideRef or false refs = refs or –

if not object then refs.squashed = false return "" end

mt = getmetatable(object)

if mt.sep then local array = {}

for _, obj in ipairs(object) do			local ref = refs[1]

if not insideRef and array[1] and mt.sep[1] ~= "" then refs[1] = {} end

value = toString(obj, insideRef, refs)

if value ~= "" or (refs.squashed and not array[1]) then array[#array + 1] = value else refs[1] = ref end end

value = table.concat(array, mt.sep[1]) else if mt.hash then if refs[1][mt.hash] then refs.squashed = true return "" end

insideRef = true end

if mt.format then local ref, squashed, array

local function processFormat(format) local array = {} local params = {}

-- see if there are required parameters to expand if format.req then

-- before expanding any parameters, check that none of them is nil for i, _ in pairs(format.req) do						if not object[i] then return array -- empty end end end

-- process the format and childs (+1 is needed to process trailing childs) for i = 1, #format + 1 do					if format.childs and format.childs[i] then for _, child in ipairs(format.childs[i]) do							local ref = copyTable(refs[1]) local squashed = refs.squashed

local childArray = processFormat(child)

if not childArray[1] then refs[1] = ref refs.squashed = squashed else mergeArrays(array, childArray) end end end

if format.params and format.params[i] then array[#array + 1] = toString(object[format[i]], insideRef, refs)

if array[#array] == "" and not refs.squashed then return {} end elseif format[i] then array[#array + 1] = format[i]

if not insideRef then refs[1] = {} end end end

return array end

ref = copyTable(refs[1]) squashed = refs.squashed

array = processFormat(mt.format)

if not array[1] then refs[1] = ref refs.squashed = squashed end

value = table.concat(array) else if mt.expand then local args = {}

for i, j in npairs(object) do					args[i] = toString(j, insideRef) end

value = mw.getCurrentFrame:expandTemplate{title=mt.expand, args=args} elseif object.label then value = object.label else value = table.concat(object) end

if not insideRef and not mt.hash and value ~= "" then refs[1] = {} end end

if mt.sub then for i, j in pairs(mt.sub) do				value = mw.ustring.gsub(value, i, j)			end end

if value ~= "" and mt.tag then value = mw.getCurrentFrame:extensionTag(mt.tag[1], value, mt.tag[2])

if mt.hash then refs[1][mt.hash] = true end end

refs.squashed = false end

if mt.trail then value = value .. mt.trail

if not insideRef then refs[1] = {} refs.squashed = false end end

return value end

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 = arg end

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

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

return id end

local function errorText(code, param) local text = i18n["errors"][code] if param then text = mw.ustring.gsub(text, "$1", param) end return text end

local function throwError(errorMessage, param) error(errorText(errorMessage, param)) 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 isPropertyID(id) return id:match('^P%d+$') end

local function buildLink(target, label) local mt = {__tostring=toString}

if not label then mt.format = {"[", target, "]"} return setmetatable({target, target=target, isWebTarget=true}, mt), mt	else mt.format = {"[", target, " ", label, "]"} return setmetatable({label, target=target, isWebTarget=true}, mt), mt	end end

local function buildWikilink(target, label) local mt = {__tostring=toString}

if not label or target == label then mt.format = {"", target, ""} return setmetatable({target, target=target}, mt), mt	else mt.format = {"", label, ""} return setmetatable({label, target=target}, mt), mt	end end

-- does a shallow copy of both the object and the metatable's format, -- so nested tables are not copied but linked local function copyValue(vIn) local vOut = copyTable(vIn) local mtIn = getmetatable(vIn) local mtOut = {format=copyTable(mtIn.format), __tostring=toString} return setmetatable(vOut, mtOut) end

local function split(str, del, from) local i, j

from = from or 1 i, j = str:find(del, from)

if i and j then return str:sub(1, i - 1), str:sub(j + 1), i, j	end

return str end

local function urlEncode(url) local i, j, urlSplit, urlPath local urlPre = "" local count = 0 local pathEnc = {} local delim = ""

i, j = url:find("//", 1, true)

-- check if a hostname is present if i == 1 or (i and url:sub(i - 1, i - 1) == ':') then urlSplit = {split(url, "[/?#]", j + 1)} urlPre = urlSplit[1]

-- split the path from the hostname if urlSplit[2] then urlPath = url:sub(urlSplit[3], urlSplit[4]) .. urlSplit[2] else urlPath = "" end else urlPath = url -- no hostname is present, so it's a path end

-- encode each part of the path for part in mw.text.gsplit(urlPath, "[;/?:@&=+$,#]") do		pathEnc[#pathEnc + 1] = delim pathEnc[#pathEnc + 1] = mw.uri.encode(mw.uri.decode(part, "PATH"), "PATH") count = count + #part + 1 delim = urlPath:sub(count, count) end

-- return the properly encoded URL return urlPre .. table.concat(pathEnc) end

local function parseWikidataURL(url) local id

if url:match('^http[s]?://') then id = ({split(url, "Q")})[2]

if id then return "Q" .. id		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(mw.ustring.gsub(dateStr:sub(ptr, i-1), "^\+(.+)$", "%1"), 10) -- remove '+' sign (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(dateA, dateB) if not dateA[1] or not dateB[1] then return nil end

dateA[2] = dateA[2] or 1 dateA[3] = dateA[3] or 1 dateB[2] = dateB[2] or 1 dateB[3] = dateB[3] or 1

if dateA[1] < dateB[1] then return true end

if dateA[1] > dateB[1] then return false end

if dateA[2] < dateB[2] then return true end

if dateA[2] > dateB[2] then return false end

if dateA[3] < dateB[3] then return true end

return false end

local function newOptionalHook(hooks) return function(state, claim) state:callHooks(hooks, claim)

return true end end

local function newPersistHook(params) return function(state, claim) local param0

if not state.resultsByStatement[claim][1] then local mt = copyTable(state.metaTable) mt.rank = claim.rank state.resultsByStatement[claim][1] = setmetatable({}, mt)

local rankPos = (rankTable[claim.rank] or {})[1]

if rankPos and rankPos < state.conf.foundRank then state.conf.foundRank = rankPos end end

for param, _ in pairs(params) do			if not state.resultsByStatement[claim][1][param] then state.resultsByStatement[claim][1][param] = state.resultsByStatement[claim][param] -- persist result

-- if we need to persist "q", then also persist "q1", "q2", etc.				if param == parameters.qualifier then for i = 1, state.conf.qualifiersCount do						param0 = param..i

if state.resultsByStatement[claim][param0][1] then state.resultsByStatement[claim][1][param0] = state.resultsByStatement[claim][param0] end end end end end

return true end end

local function parseFormat(state, formatStr, i)	local iNext, childHooks, param0 local esc = false local param = 0 local str = "" local hooks = {} local optionalHooks = {} local parsedFormat = {} local params = {} local childs = {} local req = {}

i = i or 1

local function flush if str ~= "" then parsedFormat[#parsedFormat + 1] = str

if param > 0 then req[str] = true params[#parsedFormat] = true

if not state.hooksByParam[str] then if state.conf.statesByParam[str] or str == parameters.separator then state:newValueHook(str) elseif str == parameters.qualifier and state.conf.statesByParam[str.."1"] then state:newValueHook(str)

for i = 1, state.conf.qualifiersCount do							param0 = str..i

if not state.hooksByParam[param0] then state:newValueHook(param0) end end end end

hooks[#hooks + 1] = state.hooksByParam[str] end

str = "" end

param = 0 end

while i <= #formatStr do		chr = formatStr:sub(i,i)

if not esc then if chr == '\\' then if param > 0 then flush end

esc = true elseif chr == '%' then flush param = 2 elseif chr == '[' then flush iNext = #parsedFormat + 1

if not childs[iNext] then childs[iNext] = {} end

childs[iNext][#childs[iNext] + 1], childHooks, i = parseFormat(state, formatStr, i + 1)

if childHooks[1] then optionalHooks[#optionalHooks + 1] = newOptionalHook(childHooks) end elseif chr == ']' then break else if param > 1 then param = param - 1 elseif param == 1 and not chr:match('%d') then flush end

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

i = i + 1 end

flush

if hooks[1] then hooks[#hooks + 1] = newPersistHook(req) end

mergeArrays(hooks, optionalHooks)

parsedFormat.params = params parsedFormat.childs = childs parsedFormat.req = req

return parsedFormat, hooks, i end

-- this function must stay in sync with the getValue function local function parseValue(value, datatype) if datatype == 'quantity' then return {tonumber(value)} elseif datatype == 'time' then local tail local dateValue = {}

dateValue.len = 4 -- length used for comparing

value, tail = split(value, SLASH)

if tail and tail:lower == p.JULIAN:lower then dateValue[4] = p.JULIAN end

if value:sub(1,1) == "-" then dateValue[1], value = split(value, "-", 2) else dateValue[1], value = split(value, "-") end

dateValue[1] = tonumber(dateValue[1])

if value then dateValue[2], value = split(value, "-") dateValue[2] = tonumber(dateValue[2])

if value then dateValue[3] = tonumber(value) end end

return dateValue elseif datatype == 'globecoordinate' then local part, partsIndex local coordValue = {}

coordValue.len = 6 -- length used for comparing

for i = 1, 4 do			part, value = split(value, SLASH) coordValue[i] = tonumber(part)

if not coordValue[i] or not value or i == 4 then coordValue[i] = nil partsIndex = i - 1 break end end

if part:upper == LAT_DIR_S_EN then for i = 1, partsIndex do				coordValue[i] = -coordValue[i] end end

if value then partsIndex = partsIndex + 3

for i = 4, partsIndex do				part, value = split(value, SLASH) coordValue[i] = tonumber(part)

if not coordValue[i] or not value then partsIndex = i - 1 break end end

if value and value:upper == LON_DIR_W_EN then for i = 4, partsIndex do					coordValue[i] = -coordValue[i] end end end

return coordValue elseif datatype == 'wikibase-entityid' then return {value:sub(1,1):upper, tonumber(value:sub(2))} end

return {value} end

local function getEntityId(arg, eid, page, allowOmitPropPrefix) 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) 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

--==-- Classes --==--

local Config = {}

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

cfg.separators = { ["sep"]  = {defaultSeparators["sep"]}, ["sep%q"] = {defaultSeparators["sep%q"]}, ["sep%r"] = {defaultSeparators["sep%r"]}, ["sep%s"] = setmetatable({defaultSeparators["sep%s"]}, {__tostring=toString}), ["punc"] = setmetatable({defaultSeparators["punc"]}, {__tostring=toString}) }

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

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.filterBeforeRank = 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.curTime = os.time

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

cfg.inSitelinks = false

cfg.emptyAllowed = 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.movSeparator = cfg.separators["sep%s"] cfg.puncMark = cfg.separators["punc"]

cfg.statesByParam = {} cfg.statesByID = {} cfg.curState = nil

cfg.sortKeys = {}

return cfg end

local State = {}

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

stt.conf = cfg stt.level = level stt.param = param

stt.linked = false stt.rawValue = false stt.shortName = false stt.anyLanguage = false stt.freeUnit = false stt.freeNumber = false stt.maxResults = 0 -- 0 means unlimited

stt.metaTable = nil stt.results = {} stt.resultsByStatement = {} stt.references = {} stt.hooksByParam = {} stt.hooksByID = {} stt.valHooksByIdOrParam = {} stt.valHooks = {} stt.sortable = {} stt.sortPaths = {} stt.propState = nil

if level and level > 1 then stt.hooks = {stt:newValueHook(param), stt.addToResults} stt.separator = cfg.separators["sep%"..param] or cfg.separators["sep"] -- fall back to "sep" for getAlias and getBadge stt.resultsDatatype = nil else stt.hooks = {} stt.separator = cfg.separators["sep"] stt.resultsDatatype = {CLAIM} end

if id then cfg:addToStatesByID(stt, id) elseif param then cfg.statesByParam[param] = stt end

return stt end

function Config:addToStatesByID(state, id) if not self.statesByID[id] then self.statesByID[id] = {} end

self.statesByID[id][#self.statesByID[id] + 1] = state end

-- if id == nil then item connected to current page is used function Config:getLabel(id, raw, link, short, emptyAllowed) local label local mt = {__tostring=toString} local value = setmetatable({}, mt)

if not id then id = mw.wikibase.getEntityIdForCurrentPage

if not id then return value, mt -- empty value end end

id = id:upper -- just to be sure

-- check if given id actually exists if not mw.wikibase.isValidEntityId(id) or not mw.wikibase.entityExists(id) then return value, mt -- empty value end

if raw then label = id	else -- try short name first if requested if short then label = p.property{aliasesP.shortName, [p.args.eid] = id, format = "%"..parameters.property} -- get short name

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

-- get label if not label then label = mw.wikibase.getLabelByLang(id, self.langCode) end

if not label and not emptyAllowed then return value, mt -- empty value end

value.label = label or "" end

-- split id for potential numeric sorting value[1] = id:sub(1,1) value[2] = tonumber(id:sub(2))

-- build a link if requested if link then if raw or value[1] == "P" then -- link to Wikidata if raw or if property (which has no sitelink) value.target = id

if value[1] == "P" then value.target = "Property:" .. value.target end

value.target = "d:" .. value.target else -- else, value[1] == "Q" value.target = mw.wikibase.getSitelink(id) end

if value.target and label then mt.format = ({buildWikilink(value.target, label)})[2].format end end

return value, mt end

function Config:getEditIcon local value = "" local prefix = "" local front = NB_SPACE 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

function Config:convertUnit(unit, raw, link, short) local itemID local mt = {__tostring=toString} local value = setmetatable({}, mt)

if unit == "" or unit == "1" then return value, mt	end

itemID = parseWikidataURL(unit)

if itemID then if itemID == aliasesQ.percentage then value[1] = itemID:sub(1,1) value[2] = itemID:sub(2)

if not raw then value.label = "%" elseif link then value.target = "d:" .. itemID mt.format = ({buildWikilink(value.target, itemID)})[2].format end else value, mt = self:getLabel(itemID, raw, link, short)

if value.label then value.unitSep = NB_SPACE end end end

return value, mt end

function State:getValue(snak) return self.conf:getValue(snak, self.rawValue, self.linked, self.shortName, self.anyLanguage, self.freeUnit, self.freeNumber, false, self.conf.emptyAllowed, self.param:sub(1, 1)) end

-- returns a value object in the general form {raw_component_1, raw_component_2, ...} with metatable {format={str_component_1, str_component_2, ...}}; -- 'format' is the string representation of the value in unconcatenated form to exploit Lua's string internalization to reduce memory usage; -- this function must stay in sync with the parseValue function function Config:getValue(snak, raw, link, short, anyLang, freeUnit, freeNumber, noSpecial, emptyAllowed, param) local mt = {__tostring=toString} local value = setmetatable({}, mt)

if snak.snaktype == 'value' then local datatype = snak.datavalue.type local subtype = snak.datatype local datavalue = snak.datavalue.value

mt.datatype = {datatype}

if datatype == 'string' then local datatypes = {datatype, subtype}

value[1] = datavalue mt.datatype = datatypes

if subtype == 'url' and link then -- create link explicitly if raw then -- will render as a linked number like [1] value, mt = buildLink(datavalue) else value, mt = buildLink(datavalue, datavalue) end

mt.datatype = datatypes return value elseif subtype == 'commonsMedia' then if link then value, mt = buildWikilink("c:File:" .. datavalue, datavalue) mt.datatype = datatypes elseif not raw then mt.format = {""} end

return value elseif subtype == 'geo-shape' and link then value, mt = buildWikilink("c:" .. datavalue, datavalue) mt.datatype = datatypes return value elseif subtype == 'math' and not raw then local attribute = nil

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

mt.tag = {"math", attribute} return value elseif subtype == 'musical-notation' and not raw then mt.tag = {"score"} return value elseif subtype == 'external-id' and link then local url = p.property{aliasesP.formatterURL, [p.args.eid] = snak.property, format = "%"..parameters.property} -- get formatter URL

if url ~= "" then url = urlEncode(mw.ustring.gsub(url, "$1", datavalue)) value, mt = buildLink(url, datavalue) mt.datatype = datatypes end

return value else return value end elseif datatype == 'monolingualtext' then if anyLang or datavalue['language'] == self.langCode then value[1] = datavalue['text'] value.language = datavalue['language'] end

return value elseif datatype == 'quantity' then local valueStr, unit

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

-- assertion; we should always have a value if not value[1] then return value end

if not raw then -- replace decimal mark based on locale valueStr = replaceDecimalMark(valueStr)

-- add delimiters for readability valueStr = i18n.addDelimiters(valueStr)

mt.format = {valueStr} end end

if freeUnit or (not freeNumber and not raw) then local mtUnit

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

if freeUnit and not freeNumber then value = unit mt = mtUnit mt.datatype = {UNIT} elseif unit[1] then value[#value + 1] = unit[1] value[#value + 1] = unit[2]

value.len = 1 -- (max) length used for sorting value.target = unit.target value.unitLabel = unit.label value.unitSep = unit.unitSep

if raw then mt.format = {valueStr, SLASH} mergeArrays(mt.format, mtUnit.format or unit) else mt.format[#mt.format + 1] = unit.unitSep -- may be nil mergeArrays(mt.format, mtUnit.format or {unit.label}) end end end

return value elseif datatype == 'time' then local y, m, d, p, yDiv, yRound, yFull, yRaw, mStr, ce, calendarID, target 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 = math.abs(y) 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)

-- take the first year of the century/millennium as the raw year -- (e.g. 1901 for 20th century or 2001 for 3rd millennium) yRaw = (yRound - 1) * yFactor + 1

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 yRound = yRaw end else -- precision == 8 -- round decades down (e.g. 2010s) yRound = math.floor(yDiv) * yFactor yRaw = yRound

if not raw then prefix = i18n['datetime']['prefixes']['decade-period'] suffix = i18n['datetime']['suffixes']['decade-period'] end end

if 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) yRaw = yRaw + yFactor - 1

if raw then yRound = yRaw end 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

yRaw = yRound * yFactor

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 = yRaw if yRound == 1 then suffix = i18n['datetime']['suffixes']['year'] else suffix = i18n['datetime']['suffixes']['years'] end end else yRound = yRaw end end else yRound = y				yRaw = yRound mayAddCalendar = true end

value[1] = yRaw * sign value[2] = m			value[3] = d			value.len = 3 -- (max) length used for sorting value.precision = precision mt.format = {}

if not raw then if prefix ~= "" then mt.format[1] = prefix end

if m then mStr = self.langObj:formatDate("F", "1-"..m.."-1")

if d then if self.mdyDate then mt.format[#mt.format + 1] = mStr mt.format[#mt.format + 1] = " " mt.format[#mt.format + 1] = tostring(d) mt.format[#mt.format + 1] = "," else mt.format[#mt.format + 1] = tostring(d) mt.format[#mt.format + 1] = " " mt.format[#mt.format + 1] = mStr end else mt.format[#mt.format + 1] = mStr end

mt.format[#mt.format + 1] = " " end

mt.format[#mt.format + 1] = tostring(yRound)

if suffix ~= "" then mt.format[#mt.format + 1] = suffix end

if sign < 0 then ce = i18n['datetime']['BCE'] elseif precision <= 5 then ce = i18n['datetime']['CE'] end

if ce then mt.format[#mt.format + 1] = " "

if link then target = mw.wikibase.getSitelink(aliasesQ.commonEra)

if target then mergeArrays(mt.format, ({buildWikilink(target, ce)})[2].format) else mt.format[#mt.format + 1] = ce						end else mt.format[#mt.format + 1] = ce					end end else mt.format[1] = padZeros(yRound * sign, 4)

if m then mt.format[#mt.format + 1] = "-" mt.format[#mt.format + 1] = padZeros(m, 2)

if d then mt.format[#mt.format + 1] = "-" mt.format[#mt.format + 1] = padZeros(d, 2) end end end

calendarID = parseWikidataURL(datavalue['calendarmodel'])

if calendarID and calendarID == aliasesQ.prolepticJulianCalendar then value[4] = p.JULIAN -- as value.len == 3, this will not be taken into account while sorting

if mayAddCalendar then if not raw then mt.format[#mt.format + 1] = " ("

if link then target = mw.wikibase.getSitelink(aliasesQ.julianCalendar)

if target then mergeArrays(mt.format, ({buildWikilink(target, i18n['datetime']['julian'])})[2].format) else mt.format[#mt.format + 1] = i18n['datetime']['julian'] end else mt.format[#mt.format + 1] = i18n['datetime']['julian'] end

mt.format[#mt.format + 1] = ")"					else						mt.format[#mt.format + 1] = SLASH						mt.format[#mt.format + 1] = p.JULIAN					end				end			end

return value elseif datatype == 'globecoordinate' then -- logic from https://github.com/DataValues/Geo (v4.0.1)

local precision, unitsPerDegree, numDigits, strFormat, globe local latitude, latConv, latLink local longitude, lonConv, lonLink local latDirection, latDirectionN, latDirectionS, latDirectionEN local lonDirection, lonDirectionE, lonDirectionW, lonDirectionEN local latDegrees, latMinutes, latSeconds local lonDegrees, lonMinutes, lonSeconds local degSymbol, minSymbol, secSymbol, separator

local latSign = 1 local lonSign = 1

local latFormat = {} local lonFormat = {}

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 = LAT_DIR_N_EN latDirectionS = LAT_DIR_S_EN lonDirectionE = LON_DIR_E_EN lonDirectionW = LON_DIR_W_EN

degSymbol = SLASH minSymbol = SLASH secSymbol = SLASH separator = SLASH end

latitude = datavalue['latitude'] longitude = datavalue['longitude']

if latitude < 0 then latDirection = latDirectionS latDirectionEN = LAT_DIR_S_EN latSign = -1 latitude = math.abs(latitude) else latDirection = latDirectionN latDirectionEN = LAT_DIR_N_EN end

if longitude < 0 then lonDirection = lonDirectionW lonDirectionEN = LON_DIR_W_EN lonSign = -1 longitude = math.abs(longitude) else lonDirection = lonDirectionE lonDirectionEN = LON_DIR_E_EN 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) 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 latFormat[5] = replaceDecimalMark(latSeconds) lonFormat[5] = replaceDecimalMark(lonSeconds) else latFormat[5] = latSeconds lonFormat[5] = lonSeconds end

latFormat[6] = secSymbol lonFormat[6] = secSymbol

value[3] = tonumber(latSeconds) * latSign value[6] = tonumber(lonSeconds) * lonSign end

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

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

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

if not raw then latFormat[3] = replaceDecimalMark(latMinutes) lonFormat[3] = replaceDecimalMark(lonMinutes) else latFormat[3] = latMinutes lonFormat[3] = lonMinutes end

latFormat[4] = minSymbol lonFormat[4] = minSymbol

value[2] = tonumber(latMinutes) * latSign value[5] = tonumber(lonMinutes) * lonSign

latDegrees = tostring(latDegrees) lonDegrees = tostring(lonDegrees) end

if not raw then latFormat[1] = replaceDecimalMark(latDegrees) lonFormat[1] = replaceDecimalMark(lonDegrees) else latFormat[1] = latDegrees lonFormat[1] = lonDegrees end

latFormat[2] = degSymbol lonFormat[2] = degSymbol

value[1] = tonumber(latDegrees) * latSign value[4] = tonumber(lonDegrees) * lonSign value.len = 6 -- (max) length used for sorting

latFormat[#latFormat + 1] = latDirection lonFormat[#lonFormat + 1] = lonDirection

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.target = "https://tools.wmflabs.org/geohack/geohack.php?language="..self.langCode.."&params="..latLink.."_"..latDirectionEN.."_"..lonLink.."_"..lonDirectionEN.."_globe:"..globe value.isWebTarget = true

mt.format = {"[", value.target, " "} mergeArrays(mt.format, latFormat) mt.format[#mt.format + 1] = separator mergeArrays(mt.format, lonFormat) mt.format[#mt.format + 1] = "]" else mt.format = latFormat mt.format[#mt.format + 1] = separator mergeArrays(mt.format, lonFormat) end

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

if subtype == 'wikibase-item' then itemID = "Q" .. itemID elseif subtype == 'wikibase-property' then itemID = "P" .. itemID else value[1] = errorText('unknown-data-type', subtype) mt.datatype = {UNKNOWN} mt.format = {' ', value[1], ' '} return value end

value, mt = self:getLabel(itemID, raw, link, short, emptyAllowed) mt.datatype = {datatype, subtype}

return value else value[1] = errorText('unknown-data-type', datatype) mt.datatype = {UNKNOWN} mt.format = {' ', value[1], ' '} return value end elseif snak.snaktype == 'somevalue' then if not noSpecial then value[1] = p.SOMEVALUE -- one Ogham space represents 'somevalue'

if not raw then mt.format = {i18n['values']['unknown']} end end

mt.datatype = {snak.snaktype} return value elseif snak.snaktype == 'novalue' then if not noSpecial then value[1] = "" -- empty string represents 'novalue'

if not raw then mt.format = {i18n['values']['none']} end end

mt.datatype = {snak.snaktype} return value else value[1] = errorText('unknown-data-type', snak.snaktype) mt.datatype = {UNKNOWN} mt.format = {' ', value[1], ' '} return value 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 local mt = getmetatable(snakValue)

if mt.datatype[1] == UNKNOWN then return false end

value = parseValue(value, mt.datatype[1])

for i = 1, (value.len or #value) do		if snakValue[i] ~= value[i] then return false end end

return true end

function Config:setRank(rank) local rankPos, step, to

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

if rank:match('[+-]$') then if rank:sub(-1) == "-" then step = 1 to = #self.ranks else step = -1 to = 1 end

rank = rank:sub(1, -2) end

if rank == p.flags.preferred then rankPos = 1 elseif rank == p.flags.normal then rankPos = 2 elseif rank == 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 to then for i = rankPos, to, step 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 _, qualifier in pairs(qualifiers) do			if self:snakEqualsValue(qualifier, 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) endTime = self:getSingleRawQualifier(claim, aliasesP.endTime)

if startTime and endTime and datePrecedesDate(endTime, startTime) then -- invalidate end time if it precedes start time endTime = nil end

if self.periods[1] then -- future if startTime and datePrecedesDate(self.atDate, startTime) then return true end end

if self.periods[2] then -- current if (not startTime or not datePrecedesDate(self.atDate, startTime)) and (not endTime or datePrecedesDate(self.atDate, endTime)) then return true end end

if self.periods[3] then -- former if endTime and not datePrecedesDate(self.atDate, endTime) 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.statesByParam[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.freeUnit = true return true elseif flag == p.flags.number then self.curState.freeNumber = 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 self.filterBeforeRank = 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:processCommand(command, general) local param, level

if not command then return false end

-- prevent general commands from being processed as valid commands if we only expect claim commands if general then if command == p.generalCommands.alias or command == p.generalCommands.aliases then param = parameters.alias level = 2 -- level 1 hook will be treated as a level 2 hook elseif command == p.generalCommands.badge or command == p.generalCommands.badges then param = parameters.badge level = 2 -- level 1 hook will be treated as a level 2 hook else return false end elseif command == p.claimCommands.property or command == p.claimCommands.properties then param = parameters.property level = 1 elseif command == p.claimCommands.qualifier or command == p.claimCommands.qualifiers then self.qualifiersCount = self.qualifiersCount + 1 param = parameters.qualifier .. self.qualifiersCount self.separators["sep%"..param] = {defaultSeparators["sep%q0"]} level = 2 elseif command == p.claimCommands.reference or command == p.claimCommands.references then param = parameters.reference level = 2 else return nil end

if self.statesByParam[param] then return false end

-- create a new state for each command self.curState = State:new(self, level, param)

if command == p.claimCommands.property or	  command == p.claimCommands.qualifier or	   command == p.claimCommands.reference or	   command == p.generalCommands.alias or	   command == p.generalCommands.badge then self.curState.maxResults = 1 end

return true end

function Config:processCommandOrFlag(commandOrFlag) local success = self:processCommand(commandOrFlag)

if success == nil then success = self:processFlag(commandOrFlag) end

return success end

function Config:processSeparators(args) for i, v in pairs(self.separators) do		if args[i] then self.separators[i][1] = replaceSpecialChars(args[i]) end end end

function State:isSourced(claim) return self.hooksByParam[parameters.reference](self, claim) end

function State:claimMatches(claim) local matches

-- 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 matches = (matches and self.conf:rankMatches((rankTable[claim.rank] or {})[1]) 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)) end

return matches end

function State:newSortFunction local sortPaths = self.sortPaths local sortable = self.sortable local none = {""}

local function resolveValues(sortPath, a, b)		local aVal = nil local bVal = nil local sortKey = nil

for _, subPath in ipairs(sortPath) do			local aSub, bSub, key

if #subPath == 0 then aSub = a				bSub = b			else if #subPath == 1 then aSub = subPath[1] bSub = subPath[1]

if subPath.key then key = subPath[1] end else aSub, bSub, key = resolveValues(subPath, a, b)				end

sortKey = sortKey or key end

if not aVal then aVal = aSub bVal = bSub else aVal = aVal[aSub] bVal = bVal[bSub] end end

return aVal, bVal, sortKey end

return function(a, b)		for _, sortPath in ipairs(sortPaths) do			local valLen, aPart, bPart local aValue, bValue, sortKey = resolveValues(sortPath, a, b)

if not sortKey or sortable[sortKey] then aValue = aValue or none bValue = bValue or none

if aValue.label or bValue.label then aValue = {aValue.label or ""} bValue = {bValue.label or ""} valLen = 1 else valLen = aValue.len or #aValue end

for i = 1, valLen do					aPart = aValue[i] bPart = bValue[i]

if aPart ~= bPart then if aPart == nil then return not sortPath.desc elseif bPart == nil then return sortPath.desc elseif aPart == p.SOMEVALUE or aPart == "" then if aPart == p.SOMEVALUE and bPart == "" then return true end return false elseif bPart == p.SOMEVALUE or bPart == "" then if bPart == p.SOMEVALUE and aPart == "" then return false end return true end

if sortPath.desc then return aPart > bPart else return aPart < bPart end end end end end

return false end end

function State:getHookFunction(param) if param:len > 1 then param = param:sub(1, 1).."0" end

-- fall back to 1 for getAlias and getBadge return (State[hookNames[param][self.level]] or State[hookNames[param][1]]) end

function State:newValueHook(param, id) local hook, idOrParam local func = self:getHookFunction(param)

if self.level > 1 then idOrParam = 1 else idOrParam = id or param end

hook = function(state, statement) local datatype

if not state.resultsByStatement[statement] then state.resultsByStatement[statement] = {} end

if not state.resultsByStatement[statement][idOrParam] then state.resultsByStatement[statement][idOrParam] = func(state, statement, idOrParam)

if not state.resultsDatatype then state.resultsDatatype = copyTable(getmetatable(state.resultsByStatement[statement][1]).datatype) end end

return (#state.resultsByStatement[statement][idOrParam] > 0) end

self.hooksByParam[idOrParam] = hook

return hook end

function State:prepareSortKey(sortKey) local desc = false local sortPath = nil local param = nil local id = nil local newID = nil

if sortKey:match('[+-]$') then if sortKey:sub(-1) == "-" then desc = true end

sortKey = sortKey:sub(1, -2) end

if sortKey == RANK then return {{rankTable}, {{}, {"rank"}}, desc=desc} elseif sortKey:sub(1,1) == '%' then -- param <= param

sortKey = sortKey:sub(2) param = sortKey

if param == parameters.property then sortPath = {{self.resultsByStatement}, {}, {param, key=true}, desc=desc} else if param == parameters.qualifier then param = parameters.qualifier.."1" elseif not param:match('^'..parameters.qualifier..'%d+$') then return nil end

sortPath = {{self.resultsByStatement}, {}, {param, key=true}, {1}, desc=desc} end

if not self.conf.statesByParam[param] then return nil end else local baseParam, level, state local index = 0

if sortKey == PROP then id = sortKey baseParam = parameters.property level = 1 sortPath = {{self.resultsByStatement}, {}, {id, key=true}, desc=desc} else sortKey = replaceAlias(sortKey):upper id = sortKey

if not isPropertyID(id) then return nil end

baseParam = parameters.qualifier.."0" level = 2

sortPath = {{self.resultsByStatement}, {}, {id, key=true}, {1}, desc=desc} end

if not self.conf.statesByID[id] then self.conf.statesByID[id] = {} end

repeat index = index + 1

if self.conf.statesByID[id][index] then -- id <= param

state = self.conf.statesByID[id][index] param = state.param else -- id <= id

param = baseParam newID = id				state = State:new(self.conf, level, param, newID) state.freeNumber = true state.maxResults = 1 self.conf.statesByParam[newID] = state end until not state.rawValue and not (state.freeUnit and not state.freeNumber)

if id == PROP and index > 1 then self.propState = state self.propState.resultsByStatement = self.resultsByStatement end end

return sortPath, param, id, newID end

function State:newValidationHook(param, id, newID) local invalid = false local validated = false

local idOrParam = id or param local newIdOrParam = newID or param

if not self.hooksByParam[newIdOrParam] then self:newValueHook(param, newID) end

local hook = self.hooksByParam[newIdOrParam]

local function validationHook(state, claim) if invalid then return false end

if hook(state.propState or state, claim) and not validated then local datatype

validated = true datatype = getmetatable(state.resultsByStatement[claim][newIdOrParam]).datatype[1]

if datatype == UNKNOWN then invalid = true return false end

state.sortable[idOrParam] = true end

state.resultsByStatement[claim][idOrParam] = state.resultsByStatement[claim][newIdOrParam]

return true end

self.valHooksByIdOrParam[idOrParam] = validationHook self.valHooks[#self.valHooks + 1] = validationHook

return validationHook end

function State:parseFormat(formatStr) local parsedFormat, hooks = parseFormat(self, formatStr)

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

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

self.metaTable = { format = parsedFormat, datatype = {CLAIM}, __tostring = toString }

return hooks end

-- level 1 hook function State:getProperty(claim) return self:getValue(claim.mainsnak) end

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

if claim.qualifiers then qualifiers = claim.qualifiers[self.conf.qualifierIDs[param] or param] end if qualifiers then -- iterate through claim's qualifier statements to collect their values self.conf.statesByParam[param]:iterate(qualifiers) -- pass qualifier state end

-- return array with multiple value objects (or empty array if there were no results) return self.conf.statesByParam[param]:getAndResetResults end

-- level 2 hook function State:getQualifier(snak) return self:getValue(snak) end

-- level 1 hook function State:getAllQualifiers(claim, param) local param0 local array = setmetatable({}, {sep=self.conf.separators["sep%"..param], __tostring=toString})

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

-- add the result if there is any, calling the hook in the process if it's not been called yet if self.hooksByParam[param0](self, claim) then array[#array + 1] = self.resultsByStatement[claim][param0] end end

return array end

-- level 1 hook function State:getReferences(claim, param) if claim.references then -- iterate through claim's reference statements to collect their values self.conf.statesByParam[param]:iterate(claim.references) -- pass reference state end

-- return array with multiple value objects (or empty array if there were no results) return self.conf.statesByParam[param]:getAndResetResults end

-- level 2 hook function State:getReference(statement) local key, keyNum, citeWeb, citeQ, label, mt2 local mt = {datatype={REFERENCE}, __tostring=toString, __pairs=npairs} local value = setmetatable({}, mt) local params = {} local paramKeys = {} local skipKeys = {} local citeValues = {['web'] = {}, ['q'] = {}} local citeValueKeys = {['web'] = {}, ['q'] = {}} local citeMismatch = {} local useCite = nil local useValues = {} local useValueKeys = nil local str = nil

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

if not statement.snaks then return value end

-- if we've parsed the exact same reference before, then return the cached one -- (note that this means that multiple occurences of the same value object could end up in the results) if self.references[statement.hash] then return self.references[statement.hash] end

self.references[statement.hash] = value

-- don't include "imported from", which is added by a bot if statement.snaks[aliasesP.importedFrom] then statement.snaks[aliasesP.importedFrom] = nil end

-- don't include "inferred from", which is added by a bot if statement.snaks[aliasesP.inferredFrom] then statement.snaks[aliasesP.inferredFrom] = nil 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 tostring(self:getReferenceDetail(statement.snaks[aliasesP.language])[1]) == self.conf.langName then statement.snaks[aliasesP.language] = nil end

-- retrieve all the other parameters for i in pairs(statement.snaks) do

-- multiple authors may be given if i == aliasesP.author then params[i] = self:getReferenceDetails(statement.snaks[i], false, self.linked, true, " & ") -- link = true/false, anyLang = true else params[i] = self:getReferenceDetail(statement.snaks[i], false, (self.linked or (i == aliasesP.statedIn)) and (statement.snaks[i][1].datatype ~= 'url'), true) -- link = true/false, anyLang = true end

if not params[i][1] then params[i] = nil else paramKeys[#paramKeys + 1] = i

-- add the parameter to each matching type of citation for j in pairs(citeValues) do				label = ""

-- do so if there was no mismatch with a previous parameter if not citeMismatch[j] then if j == 'q' and statement.snaks[i][1].datatype == 'external-id' then key = 'external-id' label = tostring(self.conf:getLabel(i)) else key = i					end

-- check if this parameter is not mismatching itself if i18n['cite'][j][key] then key = i18n['cite'][j][key]

-- continue if an option is available in the corresponding cite template if key ~= "" then local num = "" local k = 1

while k <= #params[i] do								keyNum = key..num

citeValues[j][keyNum] = setmetatable({}, {sep={""}, __tostring=toString}) -- "sep" is needed to make this a recognizable array, even though it will not be used citeValueKeys[j][#citeValueKeys[j] + 1] = keyNum

-- add the external ID's label to the format if we have one if label ~= "" then citeValues[j][keyNum][1] = copyValue(params[i][k]) mt2 = getmetatable(citeValues[j][keyNum][1]) mt2.format = mergeArrays({label, " "}, mt2.format or {tostring(citeValues[j][keyNum][1])}) else citeValues[j][keyNum][1] = params[i][k] end

k = k + 1 num = k							end end else citeMismatch[j] = true end end end end end

-- get title of general template for citing web references citeWeb = ({split(mw.wikibase.getSitelink(aliasesQ.citeWeb) or "", ":")})[2] -- split off namespace from front

-- get title of template that expands stated-in references into citations citeQ = ({split(mw.wikibase.getSitelink(aliasesQ.citeQ) or "", ":")})[2] -- split off namespace from front

-- (1) use the general template for citing web references if there is a match and if at least both "reference URL" and "title" are present if citeWeb and not citeMismatch['web'] and citeValues['web'][i18n['cite']['web'][aliasesP.referenceURL]] and citeValues['web'][i18n['cite']['web'][aliasesP.title]] then useCite = citeWeb useValues = citeValues['web'] useValueKeys = citeValueKeys['web']

-- (2) use the template that expands stated-in references into citations if there is a match and if at least "stated in" is present elseif citeQ and not citeMismatch['q'] and citeValues['q'][i18n['cite']['q'][aliasesP.statedIn]] then

-- we need the raw "stated in" Q-identifier for the this template citeValues['q'][i18n['cite']['q'][aliasesP.statedIn]][1] = self:getReferenceDetail(statement.snaks[aliasesP.statedIn], true)[1] -- raw = true

useCite = citeQ useValues = citeValues['q'] useValueKeys = citeValueKeys['q'] end

if useCite then

-- make sure that the parameters are added in the exact same order all the time to avoid conflict errors table.sort(useValueKeys)

-- if this module is being substituted then build a regular template call, otherwise expand the template if mw.isSubsting then mt.format = {"{{", useCite, params={}, req={}}

-- iterate through the sorted keys for _, key in ipairs(useValueKeys) do				mt2 = getmetatable(useValues[key][1]) mt2.sub = {["|"] = ENC_PIPE}

value[key] = useValues[key]

mt.format[#mt.format + 1] = "|" mt.format[#mt.format + 1] = key mt.format[#mt.format + 1] = "=" mt.format[#mt.format + 1] = key mt.format.params[#mt.format] = true mt.format.req[key] = true end

mt.format[#mt.format + 1] = "}}" else for _, key in ipairs(useValueKeys) do				value[key] = useValues[key] end

mt.expand = useCite end

-- (3) else, do some default rendering of name-value pairs, but only if at least "stated in", "reference URL" or "title" is present elseif params[aliasesP.statedIn] or params[aliasesP.referenceURL] or params[aliasesP.title] then mt.format = {params={}, req={}}

-- start by adding authors up front if params[aliasesP.author] then label = tostring(self.conf:getLabel(aliasesP.author))

if label == "" then label = aliasesP.author end

value[label] = params[aliasesP.author]

mt.format[1] = label mt.format.params[1] = true mt.format.req[label] = true mt.format[2] = "; " end

-- then add "reference URL" and "title", combining them into one link if both are present if params[aliasesP.referenceURL] then label = tostring(self.conf:getLabel(aliasesP.referenceURL))

if label == "" then label = aliasesP.referenceURL end

value[label] = params[aliasesP.referenceURL]

mt.format[#mt.format + 1] = '[' mt.format[#mt.format + 1] = label mt.format.params[#mt.format] = true mt.format.req[label] = true mt.format[#mt.format + 1] = ' '

if not params[aliasesP.title] then mt.format[#mt.format + 1] = label mt.format.params[#mt.format] = true mt.format.req[label] = true mt.format[#mt.format + 1] = ']' else str = ']' end end

if params[aliasesP.title] then label = tostring(self.conf:getLabel(aliasesP.title))

if label == "" then label = aliasesP.title end

value[label] = params[aliasesP.title]

mt.format[#mt.format + 1] = '"'			mt.format[#mt.format + 1] = label			mt.format.params[#mt.format] = true			mt.format.req[label] = true			mt.format[#mt.format + 1] = '"' mt.format[#mt.format + 1] = str end

-- then add "stated in" if params[aliasesP.statedIn] then label = tostring(self.conf:getLabel(aliasesP.statedIn))

if label == "" then label = aliasesP.statedIn end

value[label] = params[aliasesP.statedIn]

mt.format[#mt.format + 1] = "; " mt.format[#mt.format + 1] = "''" mt.format[#mt.format + 1] = label mt.format.params[#mt.format] = true mt.format.req[label] = true mt.format[#mt.format + 1] = "''" end

-- mark previously added parameters so that they won't be added a second time skipKeys[aliasesP.author] = true skipKeys[aliasesP.referenceURL] = true skipKeys[aliasesP.title] = true skipKeys[aliasesP.statedIn] = true

-- make sure that the parameters are added in the exact same order all the time to avoid conflict errors table.sort(paramKeys)

-- add the rest of the parameters for _, key in ipairs(paramKeys) do			if not skipKeys[key] then label = tostring(self.conf:getLabel(key))

if label ~= "" then value[label] = params[key]

mt.format[#mt.format + 1] = "; " mt.format[#mt.format + 1] = label mt.format[#mt.format + 1] = ": " mt.format[#mt.format + 1] = label mt.format.params[#mt.format] = true mt.format.req[label] = true end end end

mt.format[#mt.format + 1] = "." end

if not next(params) or not next(value) then return value -- empty value end

value[1] = params mt.hash = statement.hash

if not self.rawValue then local curTime = ""

-- if this module is being substituted then add a timestamp to the hash to avoid future conflict errors, -- which could occur when labels on Wikidata have been changed in the meantime while the substitution remains static if mw.isSubsting then curTime = "-" .. self.conf.curTime end

-- this should become a tag, so save the reference's hash for later mt.tag = {"ref", {name = "wikidata-" .. statement.hash .. curTime .. "-v" .. (tonumber(i18n['cite']['version']) + version)}} end

return value end

-- gets a detail of one particular type for a reference function State:getReferenceDetail(snaks, raw, link, anyLang) local value local switchLang = anyLang or false local array = setmetatable({}, {sep={""}, __tostring=toString}) -- "sep" is needed to make this a recognizable array, even though it will not be used

if not snaks then return array end

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

if value[1] then array[1] = value return array end end

if not anyLang then break end

switchLang = not switchLang until anyLang and switchLang

return array end

-- gets the details of one particular type for a reference function State:getReferenceDetails(snaks, raw, link, anyLang, sep) local value local array = setmetatable({}, {sep={sep or ""}, __tostring=toString})

if not snaks then return array end

for _, snak in ipairs(snaks) do		value = self.conf:getValue(snak, raw, link, false, anyLang, false, false, true) -- noSpecial = true

if value[1] then array[#array + 1] = value end end

return array end

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

if alias 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 return ({buildWikilink(title, alias)})[1] end end

return setmetatable({alias}, {__tostring=toString}) end

-- level 1 hook function State:getBadge(value) return ({self.conf:getLabel(value, self.rawValue, self.linked, self.shortName)})[1] end

-- level 1 hook function State:getSeparator return self.conf.movSeparator end

function State:addToResults(statement) self.results[#self.results + 1] = self.resultsByStatement[statement][1]

if #self.results == self.maxResults then return nil end

return true end

function State:getAndResetResults local results = setmetatable(self.results, {sep=self.separator, datatype=self.resultsDatatype, __tostring=toString})

-- reset results before iterating over next dataset self.results = {} self.resultsByStatement = {} self.resultsDatatype = nil

if self.level == 1 and results[1] and results[#results][parameters.separator] then results[#results][parameters.separator] = self.conf.puncMark end

return results end

-- this function may return nil, in which case the iterate function will break its loop function State:callHooks(hooks, statement) local lastResult = nil local i = 1

-- loop through the hooks in order and stop if one gives a negative result while hooks[i] do		lastResult = hooks[i](self, statement)

-- check if false or nil if not lastResult then return lastResult end

i = i + 1 end

return lastResult end

--cycle: --	iterate(statements, hooks): --		for statement in statements: --			valueHook:					state.resultsByStatement[statement][param or 1] = func(state, statement, param) --										func: {if lvl 2 hook}{cycle} --			{if param}{persistHook:		state.resultsByStatement[statement][1][param] = state.resultsByStatement[statement][param]} --			addToResults(statement):	state.results[#state.results + 1] = state.resultsByStatement[statement][1] --		:rof --	getAndResetResults:			return state.results {finally}{ --									state.results = {} --									state.resultsByStatement = {} --								} --:elcyc function State:iterate(statements, hooks) hooks = hooks or self.hooks

for _, statement in ipairs(statements) do

-- call hooks and break if the returned result is nil, which typically happens -- when addToResults found that we collected the maximum number of results if (self:callHooks(hooks, statement) == nil) then break end end end

function State:iterateHooks(claims, hooks) local i = 1 hooks = hooks or self.hooks

while hooks[i] do		local retry = false

for _, claim in ipairs(claims) do			local result = hooks[i](self, claim)

if not result then if result == nil then retry = true end

break end end

if not retry then i = i + 1 end end end

--==-- Public functions --==--

local function claimCommand(args, funcName) local lastArg, hooks, claims, sortKey, sortKeys local sortHooks = {} local value = setmetatable({}, {__tostring=toString}) local cfg = Config:new

cfg:processCommand(funcName) -- process first command (== function name)

-- 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:processCommandOrFlag(lastArg)

cfg.filterBeforeRank = cfg.filterBeforeRank or not (cfg.periods[1] and cfg.periods[2] and cfg.periods[3])

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

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

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

cfg.propertyID = replaceAlias(cfg.propertyID)

if not cfg.propertyID then return value -- empty; we cannot continue without a property ID	end

cfg.propertyID = cfg.propertyID:upper

if cfg.statesByParam[parameters.qualifier.."1"] then -- do further processing if a "qualifier(s)" command was given

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

cfg.propertyValue = nextArg(args) cfg.filterBeforeRank = true end

-- for each given qualifier ID, check if it is an alias and add it		for i = 1, cfg.qualifiersCount do			local param local qualifierID = nextArg(args)

if not qualifierID then break end

param = parameters.qualifier..i			qualifierID = replaceAlias(qualifierID):upper

cfg.qualifierIDs[param] = qualifierID cfg:addToStatesByID(cfg.statesByParam[param], qualifierID) end elseif cfg.statesByParam[parameters.reference] then -- do further processing if "reference(s)" command was given

cfg.propertyValue = nextArg(args) cfg.filterBeforeRank = true end

-- process qualifier matching values, analogous to cfg.propertyValue for i, v in npairs(args) do		local id = replaceAlias(i):upper

if isPropertyID(id) then cfg.qualifierIDsAndValues[id] = v			cfg.filterBeforeRank = true end end

-- potential optimization if only 'preferred' ranked claims are desired, -- or if the 'best' flag was given while no other filter flags were given if not (cfg.ranks[2] or cfg.ranks[3]) or (cfg.bestRank and not cfg.filterBeforeRank) then

-- returns either only 'preferred' ranked claims or only 'normal' ranked claims claims = mw.wikibase.getBestStatements(cfg.entityID, cfg.propertyID)

if #claims == 0 then -- no claims with rank 'preferred' or 'normal' found, -- property might only contain claims with rank 'deprecated'

if not cfg.ranks[3] then return value -- empty; we don't want 'deprecated' claims, so we're done end

claims = nil -- get all statements instead elseif not cfg.ranks[rankTable[claims[1].rank][1]] then -- the best ranked claims don't have the desired rank

-- if the best ranked claims have rank 'normal' which isn't desired, -- then the property might only contain claims with rank 'deprecated' if claims[1].rank == "normal" and not cfg.ranks[3] then return value -- empty; we don't want 'deprecated' claims, so we're done end

claims = nil -- get all statements instead end end

if not claims then claims = mw.wikibase.getAllStatements(cfg.entityID, cfg.propertyID) end

if #claims == 0 then return value -- empty; there is no use to continue without any claims end

-- create a state for "properties" if it doesn't exist yet, which will be used as a base configuration for each claim iteration if not cfg.statesByParam[parameters.property] then cfg.curState = State:new(cfg, 1, parameters.property, PROP)

-- decrease potential overhead (in case this state will be used for sorting/matching) cfg.curState.freeNumber = true

-- if the "single" flag has been given then this state should be equivalent to "property" (singular) if cfg.singleClaim then cfg.curState.maxResults = 1 end else cfg.curState = cfg.statesByParam[parameters.property] cfg:addToStatesByID(cfg.curState, PROP) end

-- parse the desired format, or choose an appropriate format if args["format"] then hooks = cfg.curState:parseFormat(args["format"]) elseif cfg.statesByParam[parameters.qualifier.."1"] then -- "qualifier(s)" command given if cfg.statesByParam[parameters.property] then -- "propert(y|ies)" command given hooks = cfg.curState:parseFormat(formats.propertyWithQualifier) else hooks = cfg.curState:parseFormat(formats.qualifier) end elseif cfg.statesByParam[parameters.property] then -- "propert(y|ies)" command given hooks = cfg.curState:parseFormat(formats.property) else -- "reference(s)" command given hooks = cfg.curState:parseFormat(formats.reference) end

hooks[#hooks + 1] = State.addToResults

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

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

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

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

-- 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 processing the commands and parsing the format if cfg.sourcedOnly and not cfg.curState.hooksByParam[parameters.reference] then if not cfg.statesByParam[parameters.reference] then local refState = State:new(cfg, 2, parameters.reference) refState.maxResults = 1 -- decrease overhead end

cfg.curState:newValueHook(parameters.reference) end

table.insert(hooks, 1, State.claimMatches)

-- if the best ranked claims are desired, we'll sort by rank first if cfg.bestRank then cfg.curState.sortPaths[1] = cfg.curState:prepareSortKey(RANK) end

if args[p.args.sort] then sortKeys = args[p.args.sort] else sortKeys = RANK -- by default, sort by rank end

repeat local sortPath, param, id, newID

sortKey, sortKeys = split(sortKeys, ",") sortKey = mw.text.trim(sortKey)

-- additional sorting by rank is pointless if only the best rank is desired if not (cfg.bestRank and sortKey:match('^'..RANK..'[+-]?$')) then sortPath, param, id, newID = cfg.curState:prepareSortKey(sortKey)

if sortPath then cfg.curState.sortPaths[#cfg.curState.sortPaths + 1] = sortPath

if param and not cfg.curState.valHooksByIdOrParam[id or param] then sortHooks[#sortHooks + 1] = newOptionalHook{cfg.curState:newValidationHook(param, id, newID)} end end end until not sortKeys

cfg.curState:iterate(claims, sortHooks)

table.sort(claims, cfg.curState:newSortFunction)

-- then iterate through the claims to collect values cfg.curState:iterate(claims, hooks) -- pass property state with level 1 hooks

value = cfg.curState:getAndResetResults

-- if desired, add a clickable icon that may be used to edit the returned values on Wikidata if cfg.editable and value[1] then local mt = getmetatable(value) mt.trail = cfg:getEditIcon end

return value end

local function generalCommand(args, funcName) local lastArg local value = setmetatable({}, {__tostring=toString}) local cfg = Config:new

-- process command (== function name); if false, then it's not "alias(es)" or "badge(s)" if not cfg:processCommand(funcName, true) then cfg.curState = State:new(cfg) end

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)

if cfg.entityID == "" or not mw.wikibase.entityExists(cfg.entityID) then return value -- empty; 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[1] = mw.wikibase.getSitelink(cfg.entityID) end

if cfg.curState.linked and value[1] then value = buildWikilink(value[1]) end elseif funcName == p.generalCommands.description then value[1] = mw.wikibase.getDescription(cfg.entityID) else local values

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

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 value -- empty; there is no use to continue without any aliasses end

values = cfg.entity.aliases[cfg.langCode] 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 value -- empty; there is no use to continue without any badges end

cfg.inSitelinks = true values = cfg.entity.sitelinks[cfg.siteID].badges end

cfg.separators["sep"][1] = ", "

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

-- iterate to collect values cfg.curState:iterate(values)

value = cfg.curState:getAndResetResults end

-- if desired, add a clickable icon that may be used to edit the returned values on Wikidata if cfg.editable and value[1] then local mt = getmetatable(value) mt.trail = cfg:getEditIcon end

return value end

-- modules that include this module may 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 stringWrapper(frameOrArgs) local frame, args

-- check if Wikidata is available to prevent errors if not mw.wikibase then return "" end

-- assumption: a frame always has an args table if frameOrArgs.args then -- called by wikitext frame = frameOrArgs args = copyTable(frame.args) else -- called by module args = frameOrArgs end

args.pointer = 1

loadI18n(aliasesP, frame)

return tostring(commandFunc(args, commandName)) end

p[commandName] = stringWrapper

local function tableWrapper(args)

-- check if Wikidata is available to prevent errors if not mw.wikibase then return nil end

args = copyTable(args) args.pointer = 1

loadI18n(aliasesP)

return commandFunc(args, commandName) end

p["_" .. commandName] = tableWrapper 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) 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