Module:Wd

-- Original module located at [[:en:Module:Wd]] and [[:en:Module:Wd/i18n]].require("strict")local p = {}local arg = ...local i18nlocal function loadI18n(aliasesP, frame)local titleif frame then-- current module invoked by page/template, get its title from frametitle = frame:getTitle()else-- current module included by other module, get its title from ...title = argendif not i18n theni18n = require(title .. "/i18n").init(aliasesP)endendp.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[ <span style=\"font-size:85\\%\">(%q)</span>][%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 copiedlocal 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] thenid = aliasesP[id]endreturn idendlocal function errorText(code, param)local text = i18n["errors"][code]if param then text = mw.ustring.gsub(text, "$1", param) endreturn textendlocal function throwError(errorMessage, param)error(errorText(errorMessage, param))endlocal function replaceDecimalMark(num)return mw.ustring.gsub(num, "[.]", i18n['numeric']['decimal-mark'], 1)endlocal function padZeros(num, numDigits)local numZeroslocal negative = falseif num < 0 thennegative = truenum = num * -1endnum = tostring(num)numZeros = numDigits - num:len()for _ = 1, numZeros donum = "0"..numendif negative thennum = "-"..numendreturn numendlocal function replaceSpecialChar(chr)if chr == '_' then-- replace underscores with spacesreturn ' 'elsereturn chrendendlocal function replaceSpecialChars(str)local chrlocal esc = falselocal strOut = ""for i = 1, #str dochr = str:sub(i,i)if not esc thenif chr == '\\' thenesc = trueelsestrOut = strOut .. replaceSpecialChar(chr)endelsestrOut = strOut .. chresc = falseendendreturn strOutendlocal function buildWikilink(target, label)if not label or target == label thenreturn "[[" .. target .. "]]"elsereturn "[[" .. target .. "|" .. label .. "]]"endend-- used to make frame.args mutable, to replace #frame.args (which is always 0)-- with the actual amount and to simply copy tableslocal function copyTable(tIn)if not tIn thenreturn nilendlocal tOut = {}for i, v in pairs(tIn) dotOut[i] = vendreturn tOutend-- used to merge output arrays together;-- note that it currently mutates the first input arraylocal function mergeArrays(a1, a2)for i = 1, #a2 doa1[#a1 + 1] = a2[i]endreturn a1endlocal function split(str, del)local out = {}local i, j = str:find(del)if i and j thenout[1] = str:sub(1, i - 1)out[2] = str:sub(j + 1)elseout[1] = strendreturn outendlocal function parseWikidataURL(url)local idif url:match('^http[s]?://') thenid = split(url, "Q")if id[2] thenreturn "Q" .. id[2]endendreturn nilendlocal function parseDate(dateStr, precision)precision = precision or "d"local i, j, index, ptrlocal parts = {nil, nil, nil}if dateStr == nil thenreturn parts[1], parts[2], parts[3]  -- year, month, dayend-- 'T' for snak values, '/' for outputs with '/Julian' attachedi, j = dateStr:find("[T/]")if i thendateStr = dateStr:sub(1, i-1)endlocal from = 1if dateStr:sub(1,1) == "-" then-- this is a negative number, look further aheadfrom = 2endindex = 1ptr = 1i, j = dateStr:find("-", from)if i then-- yearparts[index] = tonumber(dateStr:sub(ptr, i-1), 10)  -- explicitly give base 10 to prevent errorif parts[index] == -0 thenparts[index] = tonumber("0")  -- for some reason, 'parts[index] = 0' may actually store '-0', so parse from string insteadendif precision == "y" then-- we're donereturn parts[1], parts[2], parts[3]  -- year, month, dayendindex = index + 1ptr = i + 1i, j = dateStr:find("-", ptr)if i then-- monthparts[index] = tonumber(dateStr:sub(ptr, i-1), 10)if precision == "m" then-- we're donereturn parts[1], parts[2], parts[3]  -- year, month, dayendindex = index + 1ptr = i + 1endendif dateStr:sub(ptr) ~= "" then-- day if we have month, month if we have year, or yearparts[index] = tonumber(dateStr:sub(ptr), 10)endreturn parts[1], parts[2], parts[3]  -- year, month, dayendlocal function datePrecedesDate(aY, aM, aD, bY, bM, bD)if aY == nil or bY == nil thenreturn nilendaM = aM or 1aD = aD or 1bM = bM or 1bD = bD or 1if aY < bY thenreturn trueendif aY > bY thenreturn falseendif aM < bM thenreturn trueendif aM > bM thenreturn falseendif aD < bD thenreturn trueendreturn falseendlocal function getHookName(param, index)if hookNames[param] thenreturn hookNames[param][index]elseif param:len() > 2 thenreturn hookNames[param:sub(1, 2).."\\d"][index]elsereturn nilendendlocal function alwaysTrue()return trueend-- 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, newlocal params = {}local function newObject(array)local obj = {}  -- new objectobj.str = ""array[#array + 1] = obj  -- array{object}obj.parent = arrayreturn objendlocal function endParam()if param > 0 thenif cur.str ~= "" thencur.str = "%"..cur.strcur.param = trueparams[cur.str] = truecur.parent.req[cur.str] = trueprev = curcur = newObject(cur.parent)endparam = 0endendroot = {}  -- arrayroot.req = {}cur = newObject(root)prev = nilesc = falseparam = 0for i = 1, #str dochr = str:sub(i,i)if not esc thenif chr == '\\' thenendParam()esc = trueelseif chr == '%' thenendParam()if cur.str ~= "" thencur = newObject(cur.parent)endparam = 2elseif chr == '[' thenendParam()if prev and cur.str == "" thentable.remove(cur.parent)cur = prevendcur.child = {}  -- new arraycur.child.req = {}cur.child.parent = curcur = newObject(cur.child)elseif chr == ']' thenendParam()if cur.parent.parent thennew = newObject(cur.parent.parent.parent)if cur.str == "" thentable.remove(cur.parent)endcur = newendelseif param > 1 thenparam = param - 1elseif param == 1 thenif not chr:match('%d') thenendParam()endendcur.str = cur.str .. replaceSpecialChar(chr)endelsecur.str = cur.str .. chresc = falseendprev = nilendendParam()-- make sure that at least one required parameter has been definedif not next(root.req) thenthrowError("missing-required-parameter")end-- make sure that the separator parameter "%s" is not amongst the required parametersif root.req[parameters.separator] thenthrowError("extra-required-parameter", parameters.separator)endreturn root, paramsendlocal function sortOnRank(claims)local rankPoslocal ranks = {{}, {}, {}, {}}  -- preferred, normal, deprecated, (default)local sorted = {}for _, v in ipairs(claims) dorankPos = rankTable[v.rank] or 4ranks[rankPos][#ranks[rankPos] + 1] = vendsorted = ranks[1]sorted = mergeArrays(sorted, ranks[2])sorted = mergeArrays(sorted, ranks[3])return sortedendlocal Config = {}-- allows for recursive callsfunction Config:new()local cfg = {}setmetatable(cfg, self)self.__index = selfcfg.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 = nilcfg.entityID = nilcfg.propertyID = nilcfg.propertyValue = nilcfg.qualifierIDs = {}cfg.qualifierIDsAndValues = {}cfg.bestRank = truecfg.ranks = {true, true, false}  -- preferred = true, normal = true, deprecated = falsecfg.foundRank = #cfg.rankscfg.flagBest = falsecfg.flagRank = falsecfg.periods = {true, true, true}  -- future = true, current = true, former = truecfg.flagPeriod = falsecfg.atDate = {parseDate(os.date('!%Y-%m-%d'))}  -- today as {year, month, day}cfg.mdyDate = falsecfg.singleClaim = falsecfg.sourcedOnly = falsecfg.editable = falsecfg.editAtEnd = falsecfg.inSitelinks = falsecfg.langCode = mw.language.getContentLanguage().codecfg.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 = 0cfg.curState = nilcfg.prefetchedRefs = nilreturn cfgendlocal State = {}function State:new(cfg, type)local stt = {}setmetatable(stt, self)self.__index = selfstt.conf = cfgstt.type = typestt.results = {}stt.parsedFormat = {}stt.separator = {}stt.movSeparator = {}stt.puncMark = {}stt.linked = falsestt.rawValue = falsestt.shortName = falsestt.anyLanguage = falsestt.unitOnly = falsestt.singleValue = falsereturn sttend-- if id == nil then item connected to current page is usedfunction Config:getLabel(id, raw, link, short)local label = nillocal prefix, title= "", nilif not id thenid = mw.wikibase.getEntityIdForCurrentPage()if not id thenreturn ""endendid = id:upper()  -- just to be sureif raw then-- check if given id actually existsif mw.wikibase.isValidEntityId(id) and mw.wikibase.entityExists(id) thenlabel = idendprefix, title = "d:Special:EntityPage/", label -- may be nilelse-- try short name first if requestedif short thenlabel = p._property{aliasesP.shortName, [p.args.eid] = id}  -- get short nameif label == "" thenlabel = nilendend-- get labelif not label thenlabel = mw.wikibase.getLabelByLang(id, self.langCode) -- XXX: should use fallback labels?endendif not label thenlabel = ""elseif link then-- build a link if requestedif not title thenif id:sub(1,1) == "Q" thentitle = mw.wikibase.getSitelink(id)elseif id:sub(1,1) == "P" then-- properties have no sitelink, link to Wikidata insteadprefix, title = "d:Special:EntityPage/", idendendlabel = mw.text.nowiki(label) -- escape raw label text so it cannot be wikitext markupif title thenlabel = buildWikilink(prefix .. title, label)endendreturn labelendfunction Config:getEditIcon()local value = ""local prefix = ""local front = "&nbsp;"local back = ""if self.entityID:sub(1,1) == "P" thenprefix = "Property:"endif self.editAtEnd thenfront = '<span style="float:'if self.langObj:isRTL() thenfront = front .. 'left'elsefront = front .. 'right'endfront = front .. '">'back = '</span>'endvalue = "[[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.langCodeif self.propertyID thenvalue = value .. "#" .. self.propertyIDelseif self.inSitelinks thenvalue = value .. "#sitelinks-wikipedia"endvalue = value .. "|" .. i18n['info']['edit-on-wikidata'] .. "]]"return front .. value .. backend-- 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 outputfunction Config:concatValues(valuesArray)local outString = ""local j, skipfor i = 1, #valuesArray do-- check if this is a referenceif valuesArray[i].refHash thenj = i - 1skip = false-- skip this reference if it is part of a continuous row of references that already contains the exact same referencewhile valuesArray[j] and valuesArray[j].refHash doif valuesArray[i].refHash == valuesArray[j].refHash thenskip = truebreakendj = j - 1endif not skip then-- add <ref> 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})endelseoutString = outString .. valuesArray[i][1]endendreturn outStringendfunction Config:convertUnit(unit, raw, link, short, unitOnly)local space = " "local label = ""local itemIDif unit == "" or unit == "1" thenreturn nilendif unitOnly thenspace = ""enditemID = parseWikidataURL(unit)if itemID thenif itemID == aliasesQ.percentage thenreturn "%"elselabel = self:getLabel(itemID, raw, link, short)if label ~= "" thenreturn space .. labelendendendreturn ""endfunction State:getValue(snak)return self.conf:getValue(snak, self.rawValue, self.linked, self.shortName, self.anyLanguage, self.unitOnly, false, self.type:sub(1,2))endfunction Config:getValue(snak, raw, link, short, anyLang, unitOnly, noSpecial, type)if snak.snaktype == 'value' thenlocal datatype = snak.datavalue.typelocal subtype = snak.datatypelocal datavalue = snak.datavalue.valueif datatype == 'string' thenif subtype == 'url' and link then-- create link explicitlyif raw then-- will render as a linked number like [1]return "[" .. datavalue .. "]"elsereturn "[" .. datavalue .. " " .. datavalue .. "]"endelseif subtype == 'commonsMedia' thenif link thenreturn buildWikilink("c:File:" .. datavalue, datavalue)elseif not raw thenreturn "[[File:" .. datavalue .. "]]"elsereturn datavalueendelseif subtype == 'geo-shape' and link thenreturn buildWikilink("c:" .. datavalue, datavalue)elseif subtype == 'math' and not raw thenlocal attribute = nilif (type == parameters.property or (type == parameters.qualifier and self.propertyID == aliasesP.hasPart)) and snak.property == aliasesP.definingFormula thenattribute = {qid = self.entityID}endreturn mw.getCurrentFrame():extensionTag("math", datavalue, attribute)elseif subtype == 'external-id' and link thenlocal url = p._property{aliasesP.formatterURL, [p.args.eid] = snak.property}  -- get formatter URLif url ~= "" thenurl = mw.ustring.gsub(url, "$1", datavalue)return "[" .. url .. " " .. datavalue .. "]"elsereturn datavalueendelsereturn datavalueendelseif datatype == 'monolingualtext' thenif anyLang or datavalue['language'] == self.langCode thenreturn datavalue['text']elsereturn nilendelseif datatype == 'quantity' thenlocal value = ""local unitif not unitOnly then-- get value and strip + signs from frontvalue = mw.ustring.gsub(datavalue['amount'], "^%+(.+)$", "%1")if raw thenreturn valueend-- replace decimal mark based on localevalue = replaceDecimalMark(value)-- add delimiters for readabilityvalue = i18n.addDelimiters(value)endunit = self:convertUnit(datavalue['unit'], raw, link, short, unitOnly)if unit thenvalue = value .. unitendreturn valueelseif datatype == 'time' thenlocal y, m, d, p, yDiv, yRound, yFull, value, calendarID, dateStrlocal yFactor = 1local sign = 1local prefix = ""local suffix = ""local mayAddCalendar = falselocal calendar = ""local precision = datavalue['precision']if precision == 11 thenp = "d"elseif precision == 10 thenp = "m"elsep = "y"yFactor = 10^(9-precision)endy, m, d = parseDate(datavalue['time'], p)if y < 0 thensign = -1y = y * signend-- if precision is tens/hundreds/thousands/millions/billions of yearsif precision <= 8 thenyDiv = y / yFactor-- if precision is tens/hundreds/thousands of yearsif precision >= 6 thenmayAddCalendar = trueif precision <= 7 then-- round centuries/millenniums up (e.g. 20th century or 3rd millennium)yRound = math.ceil(yDiv)if not raw thenif precision == 6 thensuffix = i18n['datetime']['suffixes']['millennium']elsesuffix = i18n['datetime']['suffixes']['century']endsuffix = i18n.getOrdinalSuffix(yRound) .. suffixelse-- 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 + 1endelse-- precision == 8-- round decades down (e.g. 2010s)yRound = math.floor(yDiv) * yFactorif not raw thenprefix = i18n['datetime']['prefixes']['decade-period']suffix = i18n['datetime']['suffixes']['decade-period']endendif 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 - 1endelselocal yReFactor, yReDiv, yReRound-- round to nearest for tens of thousands of years or moreyRound = math.floor(yDiv + 0.5)if yRound == 0 thenif precision <= 2 and y ~= 0 thenyReFactor = 1e6yReDiv = y / yReFactoryReRound = math.floor(yReDiv + 0.5)if yReDiv == yReRound then-- change precision to millions of years only if we have a whole number of themprecision = 3yFactor = yReFactoryRound = yReRoundendendif yRound == 0 then-- otherwise, take the unrounded (original) number of yearsprecision = 5yFactor = 1yRound = ymayAddCalendar = trueendendif precision >= 1 and y ~= 0 thenyFull = yRound * yFactoryReFactor = 1e9yReDiv = yFull / yReFactoryReRound = math.floor(yReDiv + 0.5)if yReDiv == yReRound then-- change precision to billions of years if we're in that rangeprecision = 0yFactor = yReFactoryRound = yReRoundelseyReFactor = 1e6yReDiv = yFull / yReFactoryReRound = math.floor(yReDiv + 0.5)if yReDiv == yReRound then-- change precision to millions of years if we're in that rangeprecision = 3yFactor = yReFactoryRound = yReRoundendendendif not raw thenif precision == 3 thensuffix = i18n['datetime']['suffixes']['million-years']elseif precision == 0 thensuffix = i18n['datetime']['suffixes']['billion-years']elseyRound = yRound * yFactorif yRound == 1 thensuffix = i18n['datetime']['suffixes']['year']elsesuffix = i18n['datetime']['suffixes']['years']endendelseyRound = yRound * yFactorendendelseyRound = ymayAddCalendar = trueendif mayAddCalendar thencalendarID = parseWikidataURL(datavalue['calendarmodel'])if calendarID and calendarID == aliasesQ.prolepticJulianCalendar thenif not raw thenif link thencalendar = " ("..buildWikilink(i18n['datetime']['julian-calendar'], i18n['datetime']['julian'])..")"elsecalendar = " ("..i18n['datetime']['julian']..")"endelsecalendar = "/"..i18n['datetime']['julian']endendendif not raw thenlocal ce = nilif sign < 0 thence = i18n['datetime']['BCE']elseif precision <= 5 thence = i18n['datetime']['CE']endif ce thenif link thence = buildWikilink(i18n['datetime']['common-era'], ce)endsuffix = suffix .. " " .. ceendvalue = tostring(yRound)if m thendateStr = self.langObj:formatDate("F", "1-"..m.."-1")if d thenif self.mdyDate thendateStr = dateStr .. " " .. d .. ","elsedateStr = d .. " " .. dateStrendendvalue = dateStr .. " " .. valueendvalue = prefix .. value .. suffix .. calendarelsevalue = padZeros(yRound * sign, 4)if m thenvalue = value .. "-" .. padZeros(m, 2)if d thenvalue = value .. "-" .. padZeros(d, 2)endendvalue = value .. calendarendreturn valueelseif datatype == 'globecoordinate' then-- logic from https://github.com/DataValues/Geo (v4.0.1)local precision, unitsPerDegree, numDigits, strFormat, value, globelocal latitude, latConv, latValue, latLinklocal longitude, lonConv, lonValue, lonLinklocal latDirection, latDirectionN, latDirectionS, latDirectionENlocal lonDirection, lonDirectionE, lonDirectionW, lonDirectionENlocal degSymbol, minSymbol, secSymbol, separatorlocal latDegrees = nillocal latMinutes = nillocal latSeconds = nillocal lonDegrees = nillocal lonMinutes = nillocal lonSeconds = nillocal 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 thenlatDirectionN = 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']elselatDirectionN = latDirectionEN_NlatDirectionS = latDirectionEN_SlonDirectionE = lonDirectionEN_ElonDirectionW = lonDirectionEN_WdegSymbol = "/"minSymbol = "/"secSymbol = "/"separator = "/"endlatitude = datavalue['latitude']longitude = datavalue['longitude']if latitude < 0 thenlatDirection = latDirectionSlatDirectionEN = latDirectionEN_Slatitude = math.abs(latitude)elselatDirection = latDirectionNlatDirectionEN = latDirectionEN_Nendif longitude < 0 thenlonDirection = lonDirectionWlonDirectionEN = lonDirectionEN_Wlongitude = math.abs(longitude)elselonDirection = lonDirectionElonDirectionEN = lonDirectionEN_Eendprecision = datavalue['precision']if not precision or precision <= 0 thenprecision = 1 / 3600  -- precision not set (correctly), set to arcsecondend-- remove insignificant detaillatitude = math.floor(latitude / precision + 0.5) * precisionlongitude = math.floor(longitude / precision + 0.5) * precisionif precision >= 1 - (1 / 60) and precision < 1 thenprecision = 1elseif precision >= (1 / 60) - (1 / 3600) and precision < (1 / 60) thenprecision = 1 / 60endif precision >= 1 thenunitsPerDegree = 1elseif precision >= (1 / 60)  thenunitsPerDegree = 60elseunitsPerDegree = 3600endnumDigits = math.ceil(-math.log10(unitsPerDegree * precision))if numDigits <= 0 thennumDigits = tonumber("0")  -- for some reason, 'numDigits = 0' may actually store '-0', so parse from string insteadendstrFormat = "%." .. numDigits .. "f"if precision >= 1 thenlatDegrees = strFormat:format(latitude)lonDegrees = strFormat:format(longitude)if not raw thenlatDegSym = replaceDecimalMark(latDegrees) .. degSymbollonDegSym = replaceDecimalMark(lonDegrees) .. degSymbolelselatDegSym = latDegrees .. degSymbollonDegSym = lonDegrees .. degSymbolendelselatConv = math.floor(latitude * unitsPerDegree * 10^numDigits + 0.5) / 10^numDigitslonConv = math.floor(longitude * unitsPerDegree * 10^numDigits + 0.5) / 10^numDigitsif precision >= (1 / 60) thenlatMinutes = latConvlonMinutes = lonConvelselatSeconds = latConvlonSeconds = lonConvlatMinutes = math.floor(latSeconds / 60)lonMinutes = math.floor(lonSeconds / 60)latSeconds = strFormat:format(latSeconds - (latMinutes * 60))lonSeconds = strFormat:format(lonSeconds - (lonMinutes * 60))if not raw thenlatSecSym = replaceDecimalMark(latSeconds) .. secSymbollonSecSym = replaceDecimalMark(lonSeconds) .. secSymbolelselatSecSym = latSeconds .. secSymbollonSecSym = lonSeconds .. secSymbolendendlatDegrees = math.floor(latMinutes / 60)lonDegrees = math.floor(lonMinutes / 60)latDegSym = latDegrees .. degSymbollonDegSym = lonDegrees .. degSymbollatMinutes = latMinutes - (latDegrees * 60)lonMinutes = lonMinutes - (lonDegrees * 60)if precision >= (1 / 60) thenlatMinutes = strFormat:format(latMinutes)lonMinutes = strFormat:format(lonMinutes)if not raw thenlatMinSym = replaceDecimalMark(latMinutes) .. minSymbollonMinSym = replaceDecimalMark(lonMinutes) .. minSymbolelselatMinSym = latMinutes .. minSymbollonMinSym = lonMinutes .. minSymbolendelselatMinSym = latMinutes .. minSymbollonMinSym = lonMinutes .. minSymbolendendlatValue = latDegSym .. latMinSym .. latSecSym .. latDirectionlonValue = lonDegSym .. lonMinSym .. lonSecSym .. lonDirectionvalue = latValue .. separator .. lonValueif link thenglobe = parseWikidataURL(datavalue['globe'])if globe thenglobe = mw.wikibase.getLabelByLang(globe, "en"):lower()elseglobe = "earth"endlatLink = table.concat({latDegrees, latMinutes, latSeconds}, "_")lonLink = table.concat({lonDegrees, lonMinutes, lonSeconds}, "_")value = "[https://geohack.toolforge.org/geohack.php?language="..self.langCode.."&params="..latLink.."_"..latDirectionEN.."_"..lonLink.."_"..lonDirectionEN.."_globe:"..globe.." "..value.."]"endreturn valueelseif datatype == 'wikibase-entityid' thenlocal labellocal itemID = datavalue['numeric-id']if subtype == 'wikibase-item' thenitemID = "Q" .. itemIDelseif subtype == 'wikibase-property' thenitemID = "P" .. itemIDelsereturn '<strong class="error">' .. errorText('unknown-data-type', subtype) .. '</strong>'endlabel = self:getLabel(itemID, raw, link, short)if label == "" thenlabel = nilendreturn labelelsereturn '<strong class="error">' .. errorText('unknown-data-type', datatype) .. '</strong>'endelseif snak.snaktype == 'somevalue' and not noSpecial thenif raw thenreturn " "  -- single space represents 'somevalue'elsereturn i18n['values']['unknown']endelseif snak.snaktype == 'novalue' and not noSpecial thenif raw thenreturn ""  -- empty string represents 'novalue'elsereturn i18n['values']['none']endelsereturn nilendendfunction Config:getSingleRawQualifier(claim, qualifierID)local qualifiersif claim.qualifiers then qualifiers = claim.qualifiers[qualifierID] endif qualifiers and qualifiers[1] thenreturn self:getValue(qualifiers[1], true)  -- raw = trueelsereturn nilendendfunction Config:snakEqualsValue(snak, value)local snakValue = self:getValue(snak, true)  -- raw = trueif snakValue and snak.snaktype == 'value' and snak.datavalue.type == 'wikibase-entityid' then value = value:upper() endreturn snakValue == valueendfunction Config:setRank(rank)local rankPosif rank == p.flags.best thenself.bestRank = trueself.flagBest = true  -- mark that 'best' flag was givenreturnendif rank:sub(1,9) == p.flags.preferred thenrankPos = 1elseif rank:sub(1,6) == p.flags.normal thenrankPos = 2elseif rank:sub(1,10) == p.flags.deprecated thenrankPos = 3elsereturnend-- one of the rank flags was given, check if another one was given beforeif not self.flagRank thenself.ranks = {false, false, false}  -- no other rank flag given before, so unset ranksself.bestRank = self.flagBest       -- unsets bestRank only if 'best' flag was not given beforeself.flagRank = true                -- mark that a rank flag was givenendif rank:sub(-1) == "+" thenfor i = rankPos, 1, -1 doself.ranks[i] = trueendelseif rank:sub(-1) == "-" thenfor i = rankPos, #self.ranks doself.ranks[i] = trueendelseself.ranks[rankPos] = trueendendfunction Config:setPeriod(period)local periodPosif period == p.flags.future thenperiodPos = 1elseif period == p.flags.current thenperiodPos = 2elseif period == p.flags.former thenperiodPos = 3elsereturnend-- one of the period flags was given, check if another one was given beforeif not self.flagPeriod thenself.periods = {false, false, false}  -- no other period flag given before, so unset periodsself.flagPeriod = true                -- mark that a period flag was givenendself.periods[periodPos] = trueendfunction Config:qualifierMatches(claim, id, value)local qualifiersif claim.qualifiers then qualifiers = claim.qualifiers[id] endif qualifiers thenfor _, v in pairs(qualifiers) doif self:snakEqualsValue(v, value) thenreturn trueendendelseif value == "" then-- if the qualifier is not present then treat it the same as the special value 'novalue'return trueendreturn falseendfunction Config:rankMatches(rankPos)if self.bestRank thenreturn (self.ranks[rankPos] and self.foundRank >= rankPos)elsereturn self.ranks[rankPos]endendfunction Config:timeMatches(claim)local startTime = nillocal startTimeY = nillocal startTimeM = nillocal startTimeD = nillocal endTime = nillocal endTimeY = nillocal endTimeM = nillocal endTimeD = nilif self.periods[1] and self.periods[2] and self.periods[3] then-- any timereturn trueendstartTime = self:getSingleRawQualifier(claim, aliasesP.startTime)if startTime and startTime ~= "" and startTime ~= " " thenstartTimeY, startTimeM, startTimeD = parseDate(startTime)endendTime = self:getSingleRawQualifier(claim, aliasesP.endTime)if endTime and endTime ~= "" and endTime ~= " " thenendTimeY, endTimeM, endTimeD = parseDate(endTime)endif startTimeY ~= nil and endTimeY ~= nil and datePrecedesDate(endTimeY, endTimeM, endTimeD, startTimeY, startTimeM, startTimeD) then-- invalidate end time if it precedes start timeendTimeY = nilendTimeM = nilendTimeD = nilendif self.periods[1] then-- futureif startTimeY and datePrecedesDate(self.atDate[1], self.atDate[2], self.atDate[3], startTimeY, startTimeM, startTimeD) thenreturn trueendendif self.periods[2] then-- currentif (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)) thenreturn trueendendif self.periods[3] then-- formerif endTimeY and not datePrecedesDate(self.atDate[1], self.atDate[2], self.atDate[3], endTimeY, endTimeM, endTimeD) thenreturn trueendendreturn falseendfunction Config:processFlag(flag)if not flag thenreturn falseendif flag == p.flags.linked thenself.curState.linked = truereturn trueelseif flag == p.flags.raw thenself.curState.rawValue = trueif 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] = {" "}endreturn trueelseif flag == p.flags.short thenself.curState.shortName = truereturn trueelseif flag == p.flags.multilanguage thenself.curState.anyLanguage = truereturn trueelseif flag == p.flags.unit thenself.curState.unitOnly = truereturn trueelseif flag == p.flags.mdy thenself.mdyDate = truereturn trueelseif flag == p.flags.single thenself.singleClaim = truereturn trueelseif flag == p.flags.sourced thenself.sourcedOnly = truereturn trueelseif flag == p.flags.edit thenself.editable = truereturn trueelseif flag == p.flags.editAtEnd thenself.editable = trueself.editAtEnd = truereturn trueelseif flag == p.flags.best or flag:match('^'..p.flags.preferred..'[+-]?$') or flag:match('^'..p.flags.normal..'[+-]?$') or flag:match('^'..p.flags.deprecated..'[+-]?$') thenself:setRank(flag)return trueelseif flag == p.flags.future or flag == p.flags.current or flag == p.flags.former thenself:setPeriod(flag)return trueelseif flag == "" then-- ignore empty flags and carry onreturn trueelsereturn falseendendfunction Config:processFlagOrCommand(flag)local param = ""if not flag thenreturn falseendif flag == p.claimCommands.property or flag == p.claimCommands.properties thenparam = parameters.propertyelseif flag == p.claimCommands.qualifier or flag == p.claimCommands.qualifiers thenself.states.qualifiersCount = self.states.qualifiersCount + 1param = parameters.qualifier .. self.states.qualifiersCountself.separators["sep"..param] = {copyTable(defaultSeparators["sep%q\\d"])}elseif flag == p.claimCommands.reference or flag == p.claimCommands.references thenparam = parameters.referenceelsereturn self:processFlag(flag)endif self.states[param] thenreturn falseend-- create a new state for each commandself.states[param] = State:new(self, param)-- use "%x" as the general parameter nameself.states[param].parsedFormat = parseFormat(parameters.general)  -- will be overwritten for param=="%p"-- set the separatorself.states[param].separator = self.separators["sep"..param]  -- will be nil for param=="%p", which will be set separatelyif flag == p.claimCommands.property or flag == p.claimCommands.qualifier or flag == p.claimCommands.reference thenself.states[param].singleValue = trueendself.curState = self.states[param]return trueendfunction Config:processSeparators(args)local sepfor i, v in pairs(self.separators) doif args[i] thensep = replaceSpecialChars(args[i])if sep ~= "" thenself.separators[i][1] = {sep}elseself.separators[i][1] = nilendendendendfunction Config:setFormatAndSeparators(state, parsedFormat)state.parsedFormat = parsedFormatstate.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 usefunction State:isSourced(claim)self.conf.prefetchedRefs = self:getReferences(claim)return (#self.conf.prefetchedRefs > 0)endfunction State:resetCaches()-- any prefetched references of the previous claim must not be usedself.conf.prefetchedRefs = nilendfunction State:claimMatches(claim)local matches, rankPos-- first of all, reset any cached values used for the previous claimself:resetCaches()-- if a property value was given, check if it matches the claim's property valueif self.conf.propertyValue thenmatches = self.conf:snakEqualsValue(claim.mainsnak, self.conf.propertyValue)elsematches = trueend-- if any qualifier values were given, check if each matches one of the claim's qualifier valuesfor i, v in pairs(self.conf.qualifierIDsAndValues) domatches = (matches and self.conf:qualifierMatches(claim, i, v))end-- check if the claim's rank and time period matchrankPos = rankTable[claim.rank] or 4matches = (matches and self.conf:rankMatches(rankPos) and self.conf:timeMatches(claim))-- if only claims with references must be returned, check if this one has anyif self.conf.sourcedOnly thenmatches = (matches and self:isSourced(claim))  -- prefetches and caches referencesendreturn matches, rankPosendfunction State:out()local result  -- collection of arrays with value objectslocal valuesArray  -- array with value objectslocal sep = nil  -- value objectlocal out = {}  -- array with value objectslocal function walk(formatTable, result)local valuesArray = {}  -- array with value objectsfor i, v in pairs(formatTable.req) doif 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 resultreturn {}endendfor _, v in ipairs(formatTable) doif v.param thenvaluesArray = mergeArrays(valuesArray, result[v.str])elseif v.str ~= "" thenvaluesArray[#valuesArray + 1] = {v.str}endif v.child thenvaluesArray = mergeArrays(valuesArray, walk(v.child, result))endendreturn valuesArrayend-- iterate through the results from back to front, so that we know when to add separatorsfor i = #self.results, 1, -1 doresult = self.results[i]-- if there is already some output, then add the separatorsif #out > 0 thensep = self.separator[1]  -- fixed separatorresult[parameters.separator] = {self.movSeparator[1]}  -- movable separatorelsesep = nilresult[parameters.separator] = {self.puncMark[1]}  -- optional punctuation markendvaluesArray = walk(self.parsedFormat, result)if #valuesArray > 0 thenif sep thenvaluesArray[#valuesArray + 1] = sependout = mergeArrays(valuesArray, out)endend-- reset state before next iterationself.results = {}return outend-- level 1 hookfunction State:getProperty(claim)local value = {self:getValue(claim.mainsnak)}  -- create one value objectif #value > 0 thenreturn {value}  -- wrap the value object in an array and return itelsereturn {}  -- return empty array if there was no valueendend-- level 1 hookfunction State:getQualifiers(claim, param)local qualifiersif claim.qualifiers then qualifiers = claim.qualifiers[self.conf.qualifierIDs[param]] endif qualifiers then-- iterate through claim's qualifier statements to collect their values;-- return array with multiple value objectsreturn self.conf.states[param]:iterate(qualifiers, {[parameters.general] = hookNames[parameters.qualifier.."\\d"][2], count = 1})  -- pass qualifier state with level 2 hookelsereturn {}  -- return empty arrayendend-- level 2 hookfunction State:getQualifier(snak)local value = {self:getValue(snak)}  -- create one value objectif #value > 0 thenreturn {value}  -- wrap the value object in an array and return itelsereturn {}  -- return empty array if there was no valueendend-- level 1 hookfunction State:getAllQualifiers(claim, param, result, hooks)local out = {}  -- array with value objectslocal sep = self.conf.separators["sep"..parameters.qualifier][1]  -- value object-- iterate through the output of the separate "qualifier(s)" commandsfor i = 1, self.conf.states.qualifiersCount do-- if a hook has not been called yet, call it nowif not result[parameters.qualifier..i] thenself:callHook(parameters.qualifier..i, hooks, claim, result)end-- if there is output for this particular "qualifier(s)" command, then add itif result[parameters.qualifier..i] and result[parameters.qualifier..i][1] then-- if there is already some output, then add the separatorif #out > 0 and sep thenout[#out + 1] = sependout = mergeArrays(out, result[parameters.qualifier..i])endendreturn outend-- level 1 hookfunction State:getReferences(claim)if self.conf.prefetchedRefs then-- return references that have been prefetched by isSourcedreturn self.conf.prefetchedRefsendif claim.references then-- iterate through claim's reference statements to collect their values;-- return array with multiple value objectsreturn self.conf.states[parameters.reference]:iterate(claim.references, {[parameters.general] = hookNames[parameters.reference][2], count = 1})  -- pass reference state with level 2 hookelsereturn {}  -- return empty arrayendend-- level 2 hookfunction State:getReference(statement)local key, citeWeb, citeQ, labellocal params = {}local citeParams = {['web'] = {}, ['q'] = {}}local citeMismatch = {}local useCite = nillocal useParams = nillocal value = ""local ref = {}local referenceEmpty = true  -- will be set to false if at least one parameter is left unremoved    local numAuthorParameters = 0    local numAuthorNameStringParameters = 0    local tempLink    local additionalRefProperties = {}  -- will hold properties of the reference which are not in statement.snaks, namely backup title from "subject named as" and link from an external ID    local wikidataPropertiesOfSource  -- will contain "Wikidata property" properties of the item in stated in, if anylocal version = 6  -- increment this each time the below logic is changed to avoid conflict errorsif statement.snaks then-- don't include "imported from", which is added by a botif statement.snaks[aliasesP.importedFrom] thenstatement.snaks[aliasesP.importedFrom] = nilend-- don't include "Wikimedia import URL"if statement.snaks[aliasesP.wikimediaImportURL] thenstatement.snaks[aliasesP.wikimediaImportURL] = nil-- don't include "retrieved" if no "referenceURL" is present,-- as "retrieved" probably belongs to "Wikimedia import URL"if statement.snaks[aliasesP.retrieved] and not statement.snaks[aliasesP.referenceURL] thenstatement.snaks[aliasesP.retrieved] = nilendend-- don't include "inferred from", which is added by a botif statement.snaks[aliasesP.inferredFrom] thenstatement.snaks[aliasesP.inferredFrom] = nilend-- don't include "type of reference"if statement.snaks[aliasesP.typeOfReference] thenstatement.snaks[aliasesP.typeOfReference] = nilend-- don't include "image" to prevent litteringif statement.snaks[aliasesP.image] thenstatement.snaks[aliasesP.image] = nilend-- don't include "language" if it is equal to the local oneif self:getReferenceDetail(statement.snaks, aliasesP.language) == self.conf.langName thenstatement.snaks[aliasesP.language] = nilend                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"        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        tempLink = self.conf:getValue(statement.snaks[wikidataPropertyOfSource][1], false, true)  -- not raw, linked        if mw.ustring.match(tempLink, "^%[%Z- %Z+%]$") then  -- getValue returned a URL.            additionalRefProperties[aliasesP.referenceURL] = mw.ustring.gsub(tempLink, "^%[(%Z-) %Z+%]$", "%1")  -- the link is in wiki markup, so strip the square brackets and the display text            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 additionalRefProperties[aliasesP.referenceURL]) then        additionalRefProperties[aliasesP.title] = statement.snaks[aliasesP.subjectNamedAs][1].datavalue.value        end        statement.snaks[aliasesP.subjectNamedAs] = nil        end-- retrieve all the parametersfor i in pairs(statement.snaks) dolabel = ""-- multiple authors may be givenif i == aliasesP.author or i == aliasesP.authorNameString thenparams[i] = self:getReferenceDetails(statement.snaks, i, false, self.linked, true)  -- link = true/false, anyLang = trueelseif i == aliasesP.statedIn then-- Get "stated in" raw, as it is wanted (for Cite Q) even if it doesn't have a local language label.params[aliasesP.statedIn] = {self:getReferenceDetail(statement.snaks, aliasesP.statedIn, true)}  -- raw = trueelseparams[i] = {self:getReferenceDetail(statement.snaks, i, false, self.linked and (statement.snaks[i][1].datatype ~= 'url'), true)}  -- link = true/false, anyLang = trueendif #params[i] == 0 thenparams[i] = nilelsereferenceEmpty = falseif statement.snaks[i][1].datatype == 'external-id' thenkey = "external-id"label = self.conf:getLabel(i)if label ~= "" thenlabel = label .. " "endelsekey = iend-- add the parameter to each matching type of citationfor j in pairs(citeParams) do-- do so if there was no mismatch with a previous parameterif not citeMismatch[j] then-- check if this parameter is not mismatching itselfif i18n['cite'][j][key] then-- continue if an option is available in the corresponding cite templateif i18n['cite'][j][key] ~= "" then                                -- handle non-author properties (and author properties ("author" and "author name string"), if they don't use the same template parameter)                                if (i ~= aliasesP.author and i ~= aliasesP.authorNameString) or (i18n['cite'][j][aliasesP.author] ~= i18n['cite'][j][aliasesP.authorNameString]) then    citeParams[j][i18n['cite'][j][key]] = label .. params[i][1]                                    -- to avoid problems with non-author multiple parameters (if existent), the following old code is retained    for k=2, #params[i] do                                        citeParams[j][i18n['cite'][j][key]..k] = label .. params[i][k]    end                                -- handle "author" and "author name string" specially if they use the same template parameter                                elseif i == aliasesP.author or i == aliasesP.authorNameString then                                    if params[aliasesP.author] ~= nil then                                        numAuthorParameters = #params[aliasesP.author]                                    else                                        numAuthorParameters = 0                                    end                                    if params[aliasesP.authorNameString] ~= nil then                                        numAuthorNameStringParameters = #params[aliasesP.authorNameString]                                    else                                        numAuthorNameStringParameters = 0                                    end                                    -- execute only if both "author" and "author name string" satisfy this condition: the property is both in params and in statement.snaks or it is neither in params nor in statement.snaks                                    -- reason: parameters are added to params each iteration of the loop, not before the loop                                    if ((statement.snaks[aliasesP.author] == nil) == (numAuthorParameters == 0)) and ((statement.snaks[aliasesP.authorNameString] == nil) == (numAuthorNameStringParameters == 0)) then                                        for k=1, numAuthorParameters + numAuthorNameStringParameters do                                            if k <= numAuthorParameters then  -- now handling the authors from the "author" property                                                citeParams[j][i18n['cite'][j][aliasesP.author]..k] = label .. params[aliasesP.author][k]                                            else  -- now handling the authors from "author name string"                                                citeParams[j][i18n['cite'][j][aliasesP.authorNameString]..k] = label .. params[aliasesP.authorNameString][k - numAuthorParameters]                                            end        end                                    end                                endendelseciteMismatch[j] = trueendendendendend-- use additional propertiesfor i in pairs(additionalRefProperties) dofor j in pairs(citeParams) doif not citeMismatch[j] and i18n["cite"][j][i] thenciteParams[j][i18n["cite"][j][i]] = additionalRefProperties[i]elseciteMismatch[j] = trueendendend-- get title of general template for citing web referencesciteWeb = split(mw.wikibase.getSitelink(aliasesQ.citeWeb) or "", ":")[2]  -- split off namespace from front-- get title of template that expands stated-in references into citationsciteQ = 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 presentif citeWeb and not citeMismatch['web'] and citeParams['web'][i18n['cite']['web'][aliasesP.referenceURL]] and citeParams['web'][i18n['cite']['web'][aliasesP.title]] then-- we need a processed "stated in" for this templateciteParams['web'][i18n['cite']['web'][aliasesP.statedIn]] = self:getReferenceDetail(statement.snaks, aliasesP.statedIn, false, self.linked, true)useCite = citeWebuseParams = citeParams['web']-- (2) use the template that expands stated-in references into citations if there is a match and if at least "stated in" is presentelseif citeQ and not citeMismatch['q'] and citeParams['q'][i18n['cite']['q'][aliasesP.statedIn]] thenuseCite = citeQuseParams = citeParams['q']endif useCite and useParams then-- if this module is being substituted then build a regular template call, otherwise expand the templateif mw.isSubsting() thenfor i, v in pairs(useParams) dovalue = value .. "|" .. i .. "=" .. vendvalue = "{{" .. useCite .. value .. "}}"elsevalue = mw.getCurrentFrame():expandTemplate{title=useCite, args=useParams}end-- (3) if the citation couldn't be displayed using Cite web or Cite Q, but has properties other than the removed ones, throw an errorelseif not referenceEmpty thenvalue = "<span style=\"color:#dd3333\">" .. errorText("malformed-reference") .. "</span>"endif value ~= "" thenvalue = {value}  -- create one value objectif not self.rawValue then-- this should become a <ref> tag, so save the reference's hash for latervalue.refHash = "wikidata-" .. statement.hash .. "-v" .. (tonumber(i18n['cite']['version']) + version)endref = {value}  -- wrap the value object in an arrayendendreturn refend-- gets a detail of one particular type for a referencefunction State:getReferenceDetail(snaks, dType, raw, link, anyLang)local switchLang = anyLanglocal value = nilif not snaks[dType] thenreturn nilend-- if anyLang, first try the local language and otherwise any languagerepeatfor _, v in ipairs(snaks[dType]) dovalue = self.conf:getValue(v, raw, link, false, anyLang and not switchLang, false, true)  -- noSpecial = trueif value thenbreakendendif value or not anyLang thenbreakendswitchLang = not switchLanguntil anyLang and switchLangreturn valueend-- gets the details of one particular type for a referencefunction State:getReferenceDetails(snaks, dType, raw, link, anyLang)local values = {}if not snaks[dType] thenreturn {}endfor _, v in ipairs(snaks[dType]) do-- if nil is returned then it will not be added to the tablevalues[#values + 1] = self.conf:getValue(v, raw, link, false, anyLang, false, true)  -- noSpecial = trueendreturn valuesend-- level 1 hookfunction State:getAlias(object)local value = object.valuelocal title = nilif value and self.linked thenif self.conf.entityID:sub(1,1) == "Q" thentitle = mw.wikibase.getSitelink(self.conf.entityID)elseif self.conf.entityID:sub(1,1) == "P" thentitle = "d:Property:" .. self.conf.entityIDendif title thenvalue = buildWikilink(title, value)endendvalue = {value}  -- create one value objectif #value > 0 thenreturn {value}  -- wrap the value object in an array and return itelsereturn {}  -- return empty array if there was no valueendend-- level 1 hookfunction State:getBadge(value)value = self.conf:getLabel(value, self.rawValue, self.linked, self.shortName)if value == "" thenvalue = nilendvalue = {value}  -- create one value objectif #value > 0 thenreturn {value}  -- wrap the value object in an array and return itelsereturn {}  -- return empty array if there was no valueendendfunction State:callHook(param, hooks, statement, result)local valuesArray, refHash-- call a parameter's hook if it has been defined and if it has not been called beforeif not result[param] and hooks[param] thenvaluesArray = self[hooks[param]](self, statement, param, result, hooks)  -- array with value objects-- add to the resultif #valuesArray > 0 thenresult[param] = valuesArrayresult.count = result.count + 1elseresult[param] = {}  -- an empty array to indicate that we've tried this hook alreadyreturn true  -- miss == trueendendreturn falseend-- iterate through claims, claim's qualifiers or claim's references to collect valuesfunction State:iterate(statements, hooks, matchHook)matchHook = matchHook or alwaysTruelocal matches = falselocal rankPos = nillocal result, gotRequiredfor _, 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 thenresult = {count = 0}  -- collection of arrays with value objectslocal function walk(formatTable)local missfor i2, v2 in pairs(formatTable.req) do-- call a hook, adding its return value to the resultmiss = self:callHook(i2, hooks, v, result)if miss then-- we miss a required value for this level, so return falsereturn falseendif result.count == hooks.count then-- we're done if all hooks have been called;-- returning at this point breaks the loopreturn trueendendfor _, v2 in ipairs(formatTable) doif result.count == hooks.count then-- we're done if all hooks have been called;-- returning at this point prevents further childs from being processedreturn trueendif v2.child thenwalk(v2.child)endendreturn trueendgotRequired = walk(self.parsedFormat)-- only append the result if we got values for all required parameters on the root levelif gotRequired then-- if we have a rankPos (only with matchHook() for complete claims), then update the foundRankif rankPos and self.conf.foundRank > rankPos thenself.conf.foundRank = rankPosend-- append the resultself.results[#self.results + 1] = result-- break if we only need a single valueif self.singleValue thenbreakendendendendreturn self:out()endlocal function getEntityId(arg, eid, page, allowOmitPropPrefix, globalSiteId)local id = nillocal prop = nilif arg thenif arg:sub(1,1) == ":" thenpage = argeid = nilelseif arg:sub(1,1):upper() == "Q" or arg:sub(1,9):lower() == "property:" or allowOmitPropPrefix theneid = argpage = nilelseprop = argendendif eid thenif eid:sub(1,9):lower() == "property:" thenid = replaceAlias(mw.text.trim(eid:sub(10)))if id:sub(1,1):upper() ~= "P" thenid = ""endelseid = replaceAlias(eid)endelseif page thenif page:sub(1,1) == ":" thenpage = mw.text.trim(page:sub(2))endid = mw.wikibase.getEntityIdForTitle(page, globalSiteId) or ""endif not id thenid = mw.wikibase.getEntityIdForCurrentPage() or ""endid = id:upper()if not mw.wikibase.isValidEntityId(id) thenid = ""endreturn id, propendlocal function nextArg(args)local arg = args[args.pointer]if arg thenargs.pointer = args.pointer + 1return mw.text.trim(arg)elsereturn nilendendlocal function claimCommand(args, funcName)local cfg = Config:new()cfg:processFlagOrCommand(funcName)  -- process first command (== function name)local lastArg, parsedFormat, formatParams, claims, valuelocal hooks = {count = 0}-- set the date if given;-- must come BEFORE processing the flagsif args[p.args.date] thencfg.atDate = {parseDate(args[p.args.date])}cfg.periods = {false, true, false}  -- change default time constraint to 'current'end-- process flags and commandsrepeatlastArg = nextArg(args)until not cfg:processFlagOrCommand(lastArg)-- get the entity ID from either the positional argument, the eid argument or the page argumentcfg.entityID, cfg.propertyID = getEntityId(lastArg, args[p.args.eid], args[p.args.page], false, args[p.args.globalSiteId])if cfg.entityID == "" thenreturn ""  -- we cannot continue without a valid entity IDendcfg.entity = mw.wikibase.getEntity(cfg.entityID)if not cfg.propertyID thencfg.propertyID = nextArg(args)endcfg.propertyID = replaceAlias(cfg.propertyID)if not cfg.entity or not cfg.propertyID thenreturn ""  -- we cannot continue without an entity or a property IDendcfg.propertyID = cfg.propertyID:upper()if not cfg.entity.claims or not cfg.entity.claims[cfg.propertyID] thenreturn ""  -- there is no use to continue without any claimsendclaims = cfg.entity.claims[cfg.propertyID]if cfg.states.qualifiersCount > 0 then-- do further processing if "qualifier(s)" command was givenif #args - args.pointer + 1 > cfg.states.qualifiersCount then-- claim ID or literal value has been givencfg.propertyValue = nextArg(args)endfor i = 1, cfg.states.qualifiersCount do-- check if given qualifier ID is an alias and add itcfg.qualifierIDs[parameters.qualifier..i] = replaceAlias(nextArg(args) or ""):upper()endelseif cfg.states[parameters.reference] then-- do further processing if "reference(s)" command was givencfg.propertyValue = nextArg(args)end-- check for special property value 'somevalue' or 'novalue'if cfg.propertyValue thencfg.propertyValue = replaceSpecialChars(cfg.propertyValue)if cfg.propertyValue ~= "" and mw.text.trim(cfg.propertyValue) == "" thencfg.propertyValue = " "  -- single space represents 'somevalue', whereas empty string represents 'novalue'elsecfg.propertyValue = mw.text.trim(cfg.propertyValue)endend-- parse the desired format, or choose an appropriate formatif args["format"] thenparsedFormat, formatParams = parseFormat(args["format"])elseif cfg.states.qualifiersCount > 0 then  -- "qualifier(s)" command givenif cfg.states[parameters.property] then  -- "propert(y|ies)" command givenparsedFormat, formatParams = parseFormat(formats.propertyWithQualifier)elseparsedFormat, formatParams = parseFormat(formats.qualifier)endelseif cfg.states[parameters.property] then  -- "propert(y|ies)" command givenparsedFormat, formatParams = parseFormat(formats.property)else  -- "reference(s)" command givenparsedFormat, formatParams = parseFormat(formats.reference)end-- if a "qualifier(s)" command and no "propert(y|ies)" command has been given, make the movable separator a semicolonif cfg.states.qualifiersCount > 0 and not cfg.states[parameters.property] thencfg.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 thencfg.separators["sep"][1] = nilend-- if exactly one "qualifier(s)" command has been given, make "sep%q" point to "sep%q1" to make them equivalentif cfg.states.qualifiersCount == 1 thencfg.separators["sep"..parameters.qualifier] = cfg.separators["sep"..parameters.qualifier.."1"]end-- process overridden separator values;-- must come AFTER tweaking the default separatorscfg: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 givenfor 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)] thenhooks[i] = getHookName(i, 1)hooks.count = hooks.count + 1endend-- 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 givenif formatParams[parameters.qualifier] and cfg.states.qualifiersCount > 0 thenhooks[parameters.qualifier] = getHookName(parameters.qualifier, 1)hooks.count = hooks.count + 1end-- 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 hooksif not cfg.states[parameters.property] thencfg.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 thencfg.states[parameters.property].singleValue = trueendend-- 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 hooksif cfg.sourcedOnly and not cfg.states[parameters.reference] thencfg:processFlagOrCommand(p.claimCommands.reference)  -- use singular "reference" to minimize overheadend-- set the parsed format and the separators (and optional punctuation mark);-- must come AFTER creating the additonal statescfg:setFormatAndSeparators(cfg.states[parameters.property], parsedFormat)-- process qualifier matching values, analogous to cfg.propertyValuefor i, v in pairs(args) doi = tostring(i)if i:match('^[Pp]%d+$') or aliasesP[i] thenv = replaceSpecialChars(v)-- check for special qualifier value 'somevalue'if v ~= "" and mw.text.trim(v) == "" thenv = " "  -- single space represents 'somevalue'endcfg.qualifierIDsAndValues[replaceAlias(i):upper()] = vendend-- 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 valuesvalue = 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 Wikidataif cfg.editable and value ~= "" thenvalue = value .. cfg:getEditIcon()endreturn valueendlocal function generalCommand(args, funcName)local cfg = Config:new()cfg.curState = State:new(cfg)local lastArglocal value = nilrepeatlastArg = nextArg(args)until not cfg:processFlag(lastArg)-- get the entity ID from either the positional argument, the eid argument or the page argumentcfg.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) thenreturn ""  -- we cannot continue without an entityend-- serve according to the given commandif funcName == p.generalCommands.label thenvalue = cfg:getLabel(cfg.entityID, cfg.curState.rawValue, cfg.curState.linked, cfg.curState.shortName)elseif funcName == p.generalCommands.title thencfg.inSitelinks = trueif cfg.entityID:sub(1,1) == "Q" thenvalue = mw.wikibase.getSitelink(cfg.entityID)endif cfg.curState.linked and value thenvalue = buildWikilink(value)endelseif funcName == p.generalCommands.description thenvalue = mw.wikibase.getDescription(cfg.entityID)elselocal parsedFormat, formatParamslocal hooks = {count = 0}cfg.entity = mw.wikibase.getEntity(cfg.entityID)if funcName == p.generalCommands.alias or funcName == p.generalCommands.badge thencfg.curState.singleValue = trueendif funcName == p.generalCommands.alias or funcName == p.generalCommands.aliases thenif not cfg.entity.aliases or not cfg.entity.aliases[cfg.langCode] thenreturn ""  -- there is no use to continue without any aliassesendlocal aliases = cfg.entity.aliases[cfg.langCode]-- parse the desired format, or parse the default aliases formatif args["format"] thenparsedFormat, formatParams = parseFormat(args["format"])elseparsedFormat, formatParams = parseFormat(formats.alias)end-- process overridden separator values;-- must come AFTER tweaking the default separatorscfg:processSeparators(args)-- define the hook that should be called (getAlias);-- only define the hook if the parameter ("%a") has been givenif formatParams[parameters.alias] thenhooks[parameters.alias] = getHookName(parameters.alias, 1)hooks.count = hooks.count + 1end-- set the parsed format and the separators (and optional punctuation mark)cfg:setFormatAndSeparators(cfg.curState, parsedFormat)-- iterate to collect valuesvalue = cfg:concatValues(cfg.curState:iterate(aliases, hooks))elseif funcName == p.generalCommands.badge or funcName == p.generalCommands.badges thenif not cfg.entity.sitelinks or not cfg.entity.sitelinks[cfg.siteID] or not cfg.entity.sitelinks[cfg.siteID].badges thenreturn ""  -- there is no use to continue without any badgesendlocal badges = cfg.entity.sitelinks[cfg.siteID].badgescfg.inSitelinks = true-- parse the desired format, or parse the default aliases formatif args["format"] thenparsedFormat, formatParams = parseFormat(args["format"])elseparsedFormat, formatParams = parseFormat(formats.badge)end-- process overridden separator values;-- must come AFTER tweaking the default separatorscfg:processSeparators(args)-- define the hook that should be called (getBadge);-- only define the hook if the parameter ("%b") has been givenif formatParams[parameters.badge] thenhooks[parameters.badge] = getHookName(parameters.badge, 1)hooks.count = hooks.count + 1end-- set the parsed format and the separators (and optional punctuation mark)cfg:setFormatAndSeparators(cfg.curState, parsedFormat)-- iterate to collect valuesvalue = cfg:concatValues(cfg.curState:iterate(badges, hooks))endendvalue = value or ""if cfg.editable and value ~= "" then-- if desired, add a clickable icon that may be used to edit the returned value on Wikidatavalue = value .. cfg:getEditIcon()endreturn valueend-- 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) dolocal function wikitextWrapper(frame)local args = copyTable(frame.args)args.pointer = 1loadI18n(aliasesP, frame)return commandFunc(args, commandName)endp[commandName] = wikitextWrapperlocal function luaWrapper(args)args = copyTable(args)args.pointer = 1loadI18n(aliasesP)return commandFunc(args, commandName)endp["_" .. commandName] = luaWrapperendendestablishCommands(p.claimCommands, claimCommand)establishCommands(p.generalCommands, generalCommand)-- main function that is supposed to be used by wrapper templatesfunction p.main(frame)if not mw.wikibase then return nil endlocal f, argsloadI18n(aliasesP, frame)-- get the parent frame to take the arguments that were passed to the wrapper templateframe = frame:getParent() or frameif not frame.args[1] thenthrowError("no-function-specified")endf = mw.text.trim(frame.args[1])if f == "main" thenthrowError("main-called-twice")endassert(p["_"..f], errorText('no-such-function', f))-- copy arguments from immutable to mutable tableargs = copyTable(frame.args)-- remove the function name from the listtable.remove(args, 1)return p["_"..f](args)endreturn p