Module:Wd
Documentation for this module may be created at Module:Wd/doc
-- Original module located at [[:en:Module:Wd]]. local p = {} local arg = ... local i18n function loadSubmodules(frame) -- internationalization if not i18n then if frame then i18n = require(frame:getTitle().."/i18n") else i18n = require(arg.."/i18n") end end end local aliasesP = { coord = "P625", --------------- author = "P50", publisher = "P123", importedFrom = "P143", statedIn = "P248", publicationDate = "P577", startTime = "P580", endTime = "P582", retrieved = "P813", referenceURL = "P854", archiveURL = "P1065", title = "P1476", quote = "P1683", shortName = "P1813", language = "P2439", archiveDate = "P2960" } local aliasesQ = { percentage = "Q11229", prolepticJulianCalendar = "Q1985786" } local parameters = { property = "%p", qualifier = "%q", reference = "%r", separator = "%s", general = "%x" } local formats = { property = "%p[%s][%r]", qualifier = "%q[%s][%r]", reference = "%r", propertyWithQualifier = "%p[ <span style=\"font-size:smaller\">(%q)</span>][%s][%r]" } local hookNames = { -- {level_1, level_2} [parameters.property] = {"getProperty"}, [parameters.reference] = {"getReferences", "getReference"}, [parameters.qualifier] = {"getAllQualifiers"}, [parameters.qualifier.."\\d"] = {"getQualifiers", "getQualifier"} } -- default value objects, should NOT be mutated but instead copied local defaultSeparators = { ["sep"] = {" "}, ["sep%s"] = {","}, ["sep%q"] = {"; "}, ["sep%q\\d"] = {", "}, ["sep%r"] = nil, -- none ["punc"] = nil -- none } local Config = {} Config.__index = Config -- allows for recursive calls function Config.new() local cfg = {} setmetatable(cfg, Config) cfg.separators = { -- single value objects wrapped in arrays so that we can pass by reference ["sep"] = {copyTable(defaultSeparators["sep"])}, ["sep%s"] = {copyTable(defaultSeparators["sep%s"])}, ["sep%q"] = {copyTable(defaultSeparators["sep%q"])}, ["sep%r"] = {copyTable(defaultSeparators["sep%r"])}, ["punc"] = {copyTable(defaultSeparators["punc"])} } cfg.entity = nil cfg.propertyID = nil cfg.propertyValue = nil cfg.qualifierIDs = {} cfg.bestRank = true cfg.ranks = {true, true, false} -- preferred = true, normal = true, deprecated = false cfg.foundRank = #cfg.ranks cfg.flagBest = false cfg.flagRank = false cfg.periods = {true, true, true} -- future = true, current = true, former = true cfg.flagPeriod = false cfg.mdyDate = false cfg.pageTitle = false cfg.langCode = mw.language.getContentLanguage().code cfg.langName = mw.language.fetchLanguageName(cfg.langCode, cfg.langCode) cfg.langObj = mw.language.new(cfg.langCode) cfg.states = {} cfg.states.qualifiersCount = 0 cfg.curState = nil return cfg end local State = {} State.__index = State function State.new(cfg) local stt = {} setmetatable(stt, State) stt.conf = cfg stt.results = {} stt.parsedFormat = {} stt.separator = {} stt.movSeparator = {} stt.puncMark = {} stt.linked = false stt.rawValue = false stt.shortName = false stt.singleValue = false return stt end function applyStringParams(str, ...) for i, v in ipairs(arg) do str = mw.ustring.gsub(str, "$"..i, v) end return str end function unknownDataTypeError(dataType) return applyStringParams(i18n['errors']['unknown-data-type'], dataType) end function missingRequiredParameterError() return i18n['errors']['missing-required-parameter'] end function extraRequiredParameterError(param) return applyStringParams(i18n['errors']['extra-required-parameter'], param) end -- used to make frame.args mutable, to replace #frame.args (which is always 0) -- with the actual amount and to simply copy tables function copyTable(tIn) if not tIn then return nil end local tOut = {} for i, v in pairs(tIn) do tOut[i] = v end return tOut end -- used to merge output arrays together; -- note that it currently mutates the first input array function mergeArrays(a1, a2) for i = 1, #a2 do a1[#a1 + 1] = a2[i] end return a1 end -- used to create the final output string when it's all done, so that for references the -- function extensionTag("ref", ...) is only called when they really ended up in the final output function concatValues(valuesArray) local outString = "" local j, skip for i = 1, #valuesArray do -- check if this is a reference if valuesArray[i].refHash then j = i - 1 skip = false -- skip this reference if it is part of a continuous row of references that already contains the exact same reference while valuesArray[j] and valuesArray[j].refHash do if valuesArray[i].refHash == valuesArray[j].refHash then skip = true break end j = j - 1 end if not skip then -- add <ref> tag with the reference's hash as its name (to deduplicate references) outString = outString .. mw.getCurrentFrame():extensionTag("ref", valuesArray[i][1], {name = "wikidata-" .. valuesArray[i].refHash}) end else outString = outString .. valuesArray[i][1] end end return outString end function getHookName(param, index) if hookNames[param] then return hookNames[param][index] elseif string.len(param) > 2 then return hookNames[string.sub(param, 1, 2).."\\d"][index] else return nil end end function parseWikidataURL(url) local i, j if url:match('^http[s]?://') then i, j = url:find("Q") if i then return url:sub(i) end end return nil end 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 function convertUnit(unit, link) link = link or false local itemID, label, title if unit == "" or unit == "1" then return nil end itemID = parseWikidataURL(unit) if itemID then if itemID == aliasesQ.percentage then return "%" else label = mw.wikibase.label(itemID) title = nil if link or label == nil then title = mw.wikibase.sitelink(itemID) end if link then if title then return " " .. "[[" .. title .. "|" .. (label or title) .. "]]" end if not label then return " " .. "[[d:" .. itemID .. "|" .. itemID .. "]]" end end return " " .. (label or title or itemID) end end return " " .. unit end function getOrdinalSuffix(num) return i18n.getOrdinalSuffix(num) end function addDecimalMarks(num) return i18n.addDecimalMarks(num) end -- used for cleaner output when subst:ituting this module function replaceHTMLSpaces(str) return mw.ustring.gsub(str, " ", " ") end function convertRank(rank) if rank == "preferred" then return 1 elseif rank == "normal" then return 2 elseif rank == "deprecated" then return 3 else return 4 -- default (in its literal sense) end end function datePrecedesDate(aY, aM, aD, bY, bM, bD) if aY == nil or bY == nil then return nil end aM = aM or 1 aD = aD or 1 bM = bM or 1 bD = bD or 1 if aY < bY then return true end if aY > bY then return false end if aM < bM then return true end if aM > bM then return false end if aD < bD then return true end return false end function alwaysTrue() return true end -- The following function parses a format string. -- -- The example below shows how a parsed string is structured in memory. -- Variables other than 'str' and 'child' are left out for clarity's sake. -- -- Example: -- "A %p B [%s[%q1]] C [%r] D" -- -- Structure: -- [ -- { -- str = "A " -- }, -- { -- str = "%p" -- }, -- { -- str = " B ", -- child = -- [ -- { -- str = "%s", -- child = -- [ -- { -- str = "%q1" -- } -- ] -- } -- ] -- }, -- { -- str = " C ", -- child = -- [ -- { -- str = "%r" -- } -- ] -- }, -- { -- str = " D" -- } -- ] -- function parseFormat(str) local chr, esc, param, root, cur, prev, new local params = {} local function newObject(array) local obj = {} -- new object obj.str = "" array[#array + 1] = obj -- array{object} obj.parent = array return obj end local function endParam() if param > 0 then if cur.str ~= "" then cur.str = "%"..cur.str cur.param = true params[cur.str] = true cur.parent.req[cur.str] = true prev = cur cur = newObject(cur.parent) end param = 0 end end root = {} -- array root.req = {} cur = newObject(root) prev = nil esc = false param = 0 for i = 1, #str do chr = str:sub(i,i) if not esc then if chr == '\\' then endParam() esc = true elseif chr == '%' then endParam() if cur.str ~= "" then cur = newObject(cur.parent) end param = 2 elseif chr == '[' then endParam() if prev and cur.str == "" then table.remove(cur.parent) cur = prev end cur.child = {} -- new array cur.child.req = {} cur.child.parent = cur cur = newObject(cur.child) elseif chr == ']' then endParam() if cur.parent.parent then new = newObject(cur.parent.parent.parent) if cur.str == "" then table.remove(cur.parent) end cur = new end else if param > 1 then param = param - 1 elseif param == 1 then if not string.match(chr, '%d') then endParam() end end cur.str = cur.str .. chr end else cur.str = cur.str .. chr esc = false end prev = nil end endParam() return root, params end function sortOnRank(claims) local rankPos local ranks = {{}, {}, {}, {}} -- preferred, normal, deprecated, (default) local sorted = {} for i, v in ipairs(claims) do rankPos = convertRank(v.rank) ranks[rankPos][#ranks[rankPos] + 1] = v end sorted = ranks[1] sorted = mergeArrays(sorted, ranks[2]) sorted = mergeArrays(sorted, ranks[3]) return sorted end function getShortName(itemID) if itemID then return p._property({itemID, aliasesP.shortName}) -- "property" is single else return p._property({aliasesP.shortName}) -- "property" is single end end function getLabel(ID) if ID then return p._label({ID}) else return p._label({}) end end function Config:getValue(snak, raw, link, short, anyLang) raw = raw or false link = link or false short = short or false anyLang = anyLang or false if snak.snaktype == 'value' then if snak.datavalue.type == 'string' then return snak.datavalue.value elseif snak.datavalue.type == 'monolingualtext' then if anyLang then return snak.datavalue.value['text'], snak.datavalue.value['language'] elseif snak.datavalue.value['language'] == self.langCode then return snak.datavalue.value['text'] else return nil end elseif snak.datavalue.type == 'quantity' then -- strip + signs from front local value = mw.ustring.gsub(snak.datavalue.value['amount'], "^\+(.+)$", "%1") if not raw then value = addDecimalMarks(value) local unit = convertUnit(snak.datavalue.value['unit'], link) if unit then value = value .. unit end end return value elseif snak.datavalue.type == 'time' then local y, m, d, p, yDiv, yRound, yFull, value, calendarID, dateStr local yFactor = 1 local sign = 1 local prefix = "" local suffix = "" local mayAddCalendar = false local calendar = "" local precision = snak.datavalue.value['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(snak.datavalue.value['time'], p) if y < 0 then sign = -1 y = y * sign end -- if precision is tens/hundreds/thousands/millions/billions of years if precision <= 8 then yDiv = y / yFactor -- if precision is tens/hundreds/thousands of years if precision >= 6 then mayAddCalendar = true if precision <= 7 then -- round centuries/millenniums up (e.g. 20th century or 3rd millennium) yRound = math.ceil(yDiv) if not raw then if precision == 6 then suffix = i18n['datetime']['suffixes']['millennium'] else suffix = i18n['datetime']['suffixes']['century'] end suffix = getOrdinalSuffix(yRound) .. suffix else -- if not verbose, take the first year of the century/millennium -- (e.g. 1901 for 20th century or 2001 for 3rd millennium) yRound = (yRound - 1) * yFactor + 1 end else -- precision == 8 -- round decades down (e.g. 2010s) yRound = math.floor(yDiv) * yFactor if not raw then prefix = i18n['datetime']['prefixes']['decade-period'] suffix = i18n['datetime']['suffixes']['decade-period'] end end if raw and sign < 0 then -- if BCE then compensate for "counting backwards" -- (e.g. -2019 for 2010s BCE, -2000 for 20th century BCE or -3000 for 3rd millennium BCE) yRound = yRound + yFactor - 1 end else local yReFactor, yReDiv, yReRound -- round to nearest for tens of thousands of years or more yRound = math.floor(yDiv + 0.5) if yRound == 0 then if precision <= 2 and y ~= 0 then yReFactor = 1e6 yReDiv = y / yReFactor yReRound = math.floor(yReDiv + 0.5) if yReDiv == yReRound then -- change precision to millions of years only if we have a whole number of them precision = 3 yFactor = yReFactor yRound = yReRound end end if yRound == 0 then -- otherwise, take the unrounded (original) number of years precision = 5 yFactor = 1 yRound = y mayAddCalendar = true end end if precision >= 1 and y ~= 0 then yFull = yRound * yFactor yReFactor = 1e9 yReDiv = yFull / yReFactor yReRound = math.floor(yReDiv + 0.5) if yReDiv == yReRound then -- change precision to billions of years if we're in that range precision = 0 yFactor = yReFactor yRound = yReRound else yReFactor = 1e6 yReDiv = yFull / yReFactor yReRound = math.floor(yReDiv + 0.5) if yReDiv == yReRound then -- change precision to millions of years if we're in that range precision = 3 yFactor = yReFactor yRound = yReRound end end end if not raw then if precision == 3 then suffix = i18n['datetime']['suffixes']['million-years'] elseif precision == 0 then suffix = i18n['datetime']['suffixes']['billion-years'] else yRound = yRound * yFactor if yRound == 1 then suffix = i18n['datetime']['suffixes']['year'] else suffix = i18n['datetime']['suffixes']['years'] end end else yRound = yRound * yFactor end end else yRound = y mayAddCalendar = true end if mayAddCalendar then calendarID = parseWikidataURL(snak.datavalue.value['calendarmodel']) if calendarID and calendarID == aliasesQ.prolepticJulianCalendar then if not raw then if link then calendar = " ([["..i18n['datetime']['julian-calendar'].."|"..i18n['datetime']['julian'].."]])" else calendar = " ("..i18n['datetime']['julian']..")" end else calendar = "/"..i18n['datetime']['julian'] end end end if not raw then local ce = nil if sign < 0 then ce = i18n['datetime']['BCE'] elseif precision <= 5 then ce = i18n['datetime']['CE'] end if ce then if link then ce = "[[" .. i18n['datetime']['common-era'] .. "|" .. ce .. "]]" end suffix = suffix .. " " .. ce end value = tostring(yRound) if m then dateStr = self.langObj:formatDate("F", "1-"..m.."-1") if d then if self.mdyDate then dateStr = dateStr .. " " .. d .. "," else dateStr = d .. " " .. dateStr end end value = dateStr .. " " .. value end value = prefix .. value .. suffix .. calendar else value = tostring(yRound * sign) if m then value = value .. "-" .. m if d then value = value .. "-" .. d end end value = value .. calendar end return value elseif snak.datavalue.type == 'globecoordinate' then -- logic from https://github.com/DataValues/Geo local precision, numDigits, strFormat, value, globe local latValue, latitude, latDegrees, latMinutes, latSeconds local latDirection = i18n['coord']['latitude-north'] local lonValue, longitude, lonDegrees, lonMinutes, lonSeconds local lonDirection = i18n['coord']['longitude-east'] local degSymbol = i18n['coord']['degrees'] local minSymbol = i18n['coord']['minutes'] local secSymbol = i18n['coord']['seconds'] local separator = i18n['coord']['separator'] if raw then degSymbol = "/" minSymbol = "/" secSymbol = "/" separator = "/" end latitude = snak.datavalue.value['latitude'] longitude = snak.datavalue.value['longitude'] if latitude < 0 then latDirection = i18n['coord']['latitude-south'] latitude = math.abs(latitude) end if longitude < 0 then lonDirection = i18n['coord']['longitude-west'] longitude = math.abs(longitude) end precision = snak.datavalue.value['precision'] latitude = math.floor(latitude / precision + 0.5) * precision longitude = math.floor(longitude / precision + 0.5) * precision numDigits = math.ceil(-math.log10(3600 * precision)) if numDigits < 0 or numDigits == -0 then numDigits = tonumber("0") -- for some reason, 'numDigits = 0' may actually store '-0', so parse from string instead end strFormat = "%." .. numDigits .. "f" -- use string.format() to strip decimal point followed by a zero (.0) for whole numbers latSeconds = tonumber(string.format(strFormat, math.floor(latitude * 3600 * 10^numDigits + 0.5) / 10^numDigits)) lonSeconds = tonumber(string.format(strFormat, math.floor(longitude * 3600 * 10^numDigits + 0.5) / 10^numDigits)) latMinutes = math.floor(latSeconds / 60) lonMinutes = math.floor(lonSeconds / 60) latSeconds = latSeconds - (latMinutes * 60) lonSeconds = lonSeconds - (lonMinutes * 60) latDegrees = math.floor(latMinutes / 60) lonDegrees = math.floor(lonMinutes / 60) latMinutes = latMinutes - (latDegrees * 60) lonMinutes = lonMinutes - (lonDegrees * 60) latValue = latDegrees .. degSymbol lonValue = lonDegrees .. degSymbol if precision < 1 then latValue = latValue .. latMinutes .. minSymbol lonValue = lonValue .. lonMinutes .. minSymbol end if precision < (1 / 60) then latSeconds = string.format(strFormat, latSeconds) lonSeconds = string.format(strFormat, lonSeconds) latValue = latValue .. latSeconds .. secSymbol lonValue = lonValue .. lonSeconds .. secSymbol end latValue = latValue .. latDirection lonValue = lonValue .. lonDirection value = latValue .. separator .. lonValue if link then globe = parseWikidataURL(snak.datavalue.value['globe']) if globe then globe = mw.wikibase.getEntity(globe):getLabel("en"):lower() else globe = "earth" end value = "[https://tools.wmflabs.org/geohack/geohack.php?language="..self.langCode.."¶ms="..latitude.."_"..latDirection.."_"..longitude.."_"..lonDirection.."_globe:"..globe.." "..value.."]" end return value elseif snak.datavalue.type == 'wikibase-entityid' then local value = "" local title = nil local itemID = "Q" .. snak.datavalue.value['numeric-id'] if raw then if link then return "[[d:" .. itemID .. "|" .. itemID .. "]]" else return itemID end end if short then value = getShortName(itemID) end if value == "" then value = mw.wikibase.label(itemID) end if link or value == nil then title = mw.wikibase.sitelink(itemID) end if link then if title then value = "[[" .. title .. "|" .. (value or title) .. "]]" elseif not value then value = "[[d:" .. itemID .. "|" .. itemID .. "]]" end elseif not value then value = (title or itemID) end return value else return '<strong class="error">' .. unknownDataTypeError(snak.datavalue.type) .. '.</strong>' end elseif snak.snaktype == 'somevalue' then if raw then return " " -- single space represents 'somevalue' else return i18n['values']['unknown'] end elseif snak.snaktype == 'novalue' then if raw then return "" -- empty string represents 'novalue' else return i18n['values']['none'] end else return nil end end function Config:getSingleRawQualifier(claim, qualifierID) local qualifiers if claim.qualifiers then qualifiers = claim.qualifiers[qualifierID] end if qualifiers and qualifiers[1] then return self:getValue(qualifiers[1], true) -- raw = true else return nil end end function Config:snakEqualsValue(snak, value) local snakValue = self:getValue(snak, true) -- raw = true if snakValue and snak.snaktype == 'value' and snak.datavalue.type == 'wikibase-entityid' then value = value:upper() end return snakValue == value end function Config:setRank(rank) local rankPos if rank == "best" then self.bestRank = true self.flagBest = true -- mark that 'best' flag was given return end if rank:sub(1,9) == "preferred" then rankPos = 1 elseif rank:sub(1,6) == "normal" then rankPos = 2 elseif rank:sub(1,10) == "deprecated" then rankPos = 3 else return end -- one of the rank flags was given, check if another one was given before if not self.flagRank then self.ranks = {false, false, false} -- no other rank flag given before, so unset ranks self.bestRank = self.flagBest -- unsets bestRank only if 'best' flag was not given before self.flagRank = true -- mark that a rank flag was given end if rank:sub(-1) == "+" then for i = rankPos, 1, -1 do self.ranks[i] = true end elseif rank:sub(-1) == "-" then for i = rankPos, #self.ranks do self.ranks[i] = true end else self.ranks[rankPos] = true end end function Config:setPeriod(period) local periodPos if period == "future" then periodPos = 1 elseif period == "current" then periodPos = 2 elseif period == "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:processFlag(flag) if not flag then return false else flag = mw.text.trim(flag) end if flag == "linked" then self.curState.linked = true return true elseif flag == "raw" then self.curState.rawValue = true if self.curState == self.states[parameters.reference] then -- raw reference values end with periods and require a separator (other than none) self.separators["sep%r"][1] = {" "} end return true elseif flag == "short" then self.curState.shortName = true return true elseif flag == "mdy" then self.mdyDate = true return true elseif flag == "best" or flag:match('^preferred[+-]?$') or flag:match('^normal[+-]?$') or flag:match('^deprecated[+-]?$') then self:setRank(flag) return true elseif flag == "future" or flag == "current" or flag == "former" then self:setPeriod(flag) return true elseif flag == "" then -- ignore empty flags and carry on return true else return false end end function Config:processFlagOrCommand(flag) local param = "" if not flag then return false else flag = mw.text.trim(flag) end if flag == "property" or flag == "properties" then param = parameters.property elseif flag:match('^qualifier[s]?$') then self.states.qualifiersCount = self.states.qualifiersCount + 1 param = parameters.qualifier .. self.states.qualifiersCount self.separators["sep"..param] = {copyTable(defaultSeparators["sep%q\\d"])} elseif flag:match('^reference[s]?$') then param = parameters.reference else return self:processFlag(flag) end if self.states[param] then return false end -- create a new State for each command self.states[param] = State.new(self) -- use "%x" as the general parameter name self.states[param].parsedFormat = parseFormat(parameters.general) -- will be overwritten for param=="%p" -- set the separator self.states[param].separator = self.separators["sep"..param] -- will be nil for param=="%p", which will be set separately if string.sub(flag, -1) ~= 's' then self.states[param].singleValue = true end self.curState = self.states[param] return true 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 local now = os.date('!*t') startTime = self:getSingleRawQualifier(claim, aliasesP.startTime) if startTime and startTime ~= "" and startTime ~= " " then startTimeY, startTimeM, startTimeD = parseDate(startTime) end endTime = self:getSingleRawQualifier(claim, aliasesP.endTime) if endTime and endTime ~= "" and endTime ~= " " then endTimeY, endTimeM, endTimeD = parseDate(endTime) elseif endTime == " " then -- end time is 'unknown', assume it is somewhere in the past; -- we can do this by taking the current date as a placeholder for the end time endTimeY = now['year'] endTimeM = now['month'] endTimeD = now['day'] end if startTimeY ~= nil and endTimeY ~= nil and datePrecedesDate(endTimeY, endTimeM, endTimeD, startTimeY, startTimeM, startTimeD) then -- invalidate end time if it precedes start time endTimeY = nil endTimeM = nil endTimeD = nil end if self.periods[1] then -- future if startTimeY and datePrecedesDate(now['year'], now['month'], now['day'], startTimeY, startTimeM, startTimeD) then return true end end if self.periods[2] then -- current if (startTimeY == nil or not datePrecedesDate(now['year'], now['month'], now['day'], startTimeY, startTimeM, startTimeD)) and (endTimeY == nil or datePrecedesDate(now['year'], now['month'], now['day'], endTimeY, endTimeM, endTimeD)) then return true end end if self.periods[3] then -- former if endTimeY and not datePrecedesDate(now['year'], now['month'], now['day'], endTimeY, endTimeM, endTimeD) then return true end end return false end function State:claimMatches(claim) local matches, rankPos -- 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 -- check if the claim's rank and time period match rankPos = convertRank(claim.rank) matches = (matches and self.conf:rankMatches(rankPos) and self.conf:timeMatches(claim)) return matches, rankPos end function State:out() local result -- collection of arrays with value objects local valuesArray -- array with value objects local sep = nil -- value object local out = {} -- array with value objects local function walk(formatTable, result) local valuesArray = {} -- array with value objects for i, v in pairs(formatTable.req) do if not result[i] or not result[i][1] then -- we've got no result for a parameter that is required on this level, -- so skip this level (and its children) by returning an empty result return {} end end for i, v in ipairs(formatTable) do if v.param then valuesArray = mergeArrays(valuesArray, result[v.str]) elseif v.str ~= "" then valuesArray[#valuesArray + 1] = {v.str} end if v.child then valuesArray = mergeArrays(valuesArray, walk(v.child, result)) end end return valuesArray end -- iterate through the results from back to front, so that we know when to add separators for i = #self.results, 1, -1 do result = self.results[i] -- if there is already some output, then add the separators if #out > 0 then sep = self.separator[1] -- fixed separator result[parameters.separator] = {self.movSeparator[1]} -- movable separator else sep = nil result[parameters.separator] = {self.puncMark[1]} -- optional punctuation mark end valuesArray = walk(self.parsedFormat, result) if #valuesArray > 0 then if sep then valuesArray[#valuesArray + 1] = sep end out = mergeArrays(valuesArray, out) end end -- reset state before next iteration self.results = {} return out end -- level 1 hook function State:getProperty(claim) local value = {self.conf:getValue(claim.mainsnak, self.rawValue, self.linked, self.shortName)} -- create one value object if #value > 0 then return {value} -- wrap the value object in an array and return it else return {} -- return empty array if there was no value end end -- level 1 hook function State:getQualifiers(claim, param) local qualifiers if claim.qualifiers then qualifiers = claim.qualifiers[self.conf.qualifierIDs[param]] end if qualifiers then -- iterate through claim's qualifier statements to collect their values; -- return array with multiple value objects return self.conf.states[param]:iterate(qualifiers, {[parameters.general] = hookNames[parameters.qualifier.."\\d"][2], count = 1}) -- pass qualifier State with level 2 hook else return {} -- return empty array end end -- level 2 hook function State:getQualifier(snak) local value = {self.conf:getValue(snak, self.rawValue, self.linked, self.shortName)} -- create one value object if #value > 0 then return {value} -- wrap the value object in an array and return it else return {} -- return empty array if there was no value end end -- level 1 hook function State:getAllQualifiers(claim, param, result, hooks) local out = {} -- array with value objects local sep = self.conf.separators["sep"..parameters.qualifier][1] -- value object -- iterate through the output of the separate "qualifier(s)" commands for i = 1, self.conf.states.qualifiersCount do -- if a hook has not been called yet, call it now if not result[parameters.qualifier..i] then self:callHook(parameters.qualifier..i, hooks, claim, result) end -- if there is output for this particular "qualifier(s)" command, then add it if result[parameters.qualifier..i] and result[parameters.qualifier..i][1] then -- if there is already some output, then add the separator if #out > 0 and sep then out[#out + 1] = sep end out = mergeArrays(out, result[parameters.qualifier..i]) end end return out end -- level 1 hook function State:getReferences(claim) if claim.references then -- iterate through claim's reference statements to collect their values; -- return array with multiple value objects return self.conf.states[parameters.reference]:iterate(claim.references, {[parameters.general] = hookNames[parameters.reference][2], count = 1}) -- pass reference State with level 2 hook else return {} -- return empty array end end -- level 2 hook -- logic determined based on https://www.wikidata.org/wiki/Help:Sources function State:getReference(statement) local snakValue, lang, property, url, title local value = "" local ref = {} local snaks = {} local params = {} local leadParams = {} if statement.snaks then for i, v in pairs(statement.snaks) do if v[1] then snaks[i] = v[1] end end -- don't include "imported from" that has been added by a bot if snaks[aliasesP.importedFrom] then snaks[aliasesP.importedFrom] = nil end -- use the general template for citing web references if both URL and title are present if snaks[aliasesP.referenceURL] and snaks[aliasesP.title] and i18n['cite']['cite-web'] and i18n['cite']['cite-web'] ~= "" then params[i18n['cite']['url']] = self.conf:getValue(snaks[aliasesP.referenceURL]) params[i18n['cite']['title']] = self.conf:getValue(snaks[aliasesP.title], false, false, false, true) -- anyLang = true if snaks[aliasesP.publicationDate] then params[i18n['cite']['date']] = self.conf:getValue(snaks[aliasesP.publicationDate]) end if snaks[aliasesP.retrieved] then params[i18n['cite']['access-date']] = self.conf:getValue(snaks[aliasesP.retrieved]) end if snaks[aliasesP.archiveURL] then params[i18n['cite']['archive-url']] = self.conf:getValue(snaks[aliasesP.archiveURL]) end if snaks[aliasesP.archiveDate] then params[i18n['cite']['archive-date']] = self.conf:getValue(snaks[aliasesP.archiveDate]) end if snaks[aliasesP.author] then params[i18n['cite']['author']] = self.conf:getValue(snaks[aliasesP.author]) end if snaks[aliasesP.publisher] then params[i18n['cite']['publisher']] = self.conf:getValue(snaks[aliasesP.publisher]) end if snaks[aliasesP.quote] then params[i18n['cite']['quote']] = self.conf:getValue(snaks[aliasesP.quote], false, false, false, true) end -- anyLang = true if snaks[aliasesP.language] then snakValue = self.conf:getValue(snaks[aliasesP.language]) if self.conf.langName ~= snakValue then params[i18n['cite']['language']] = snakValue end end if mw.isSubsting() then for i, v in pairs(params) do value = value .. "|" .. i .. "=" .. v end value = "{{" .. i18n['cite']['cite-web'] .. value .. "}}" else value = mw.getCurrentFrame():expandTemplate{title=i18n['cite']['cite-web'], args=params} end else -- if no general template for citing web references was defined but URL and title are present, add these together if snaks[aliasesP.referenceURL] and snaks[aliasesP.title] then url = self.conf:getValue(snaks[aliasesP.referenceURL]) title = self.conf:getValue(snaks[aliasesP.title], false, false, false, true) leadParams[#leadParams + 1] = "[" .. url .. " " .. title .. "]" -- set to nil so that they won't be added a second time snaks[aliasesP.referenceURL] = nil snaks[aliasesP.title] = nil end for i, v in pairs(snaks) do property = getLabel(i) if property ~= "" then snakValue, lang = self.conf:getValue(v, false, (i == aliasesP.statedIn), false, true) -- link = true/false, anyLang = true if lang and lang ~= self.conf.langCode then snakValue = "''" .. snakValue .. "'' (" .. mw.language.fetchLanguageName(lang, self.conf.langCode) .. ")" end if i == aliasesP.referenceURL or i == aliasesP.statedIn then leadParams[#leadParams + 1] = snakValue elseif i ~= aliasesP.language or self.conf.langName ~= snakValue then params[#params + 1] = property .. ": " .. snakValue end end end value = table.concat(leadParams, "; ") params = table.concat(params, "; ") if params ~= "" then if value ~= "" then value = value .. "; " end value = value .. params end if value ~= "" then value = value .. "." end end if value ~= "" then ref = {value} -- create one value object if not self.rawValue then -- this should become a <ref> tag, so safe the reference's hash for later ref.refHash = statement.hash end ref = {ref} -- wrap the value object in an array end end return ref end function State:callHook(param, hooks, statement, result) local valuesArray, refHash -- call a parameter's hook if it has been defined and if it has not been called before if not result[param] and hooks[param] then valuesArray = self[hooks[param]](self, statement, param, result, hooks) -- array with value objects -- add to the result if #valuesArray > 0 then result[param] = valuesArray result.count = result.count + 1 else result[param] = {} -- an empty array to indicate that we've tried this hook already return true -- miss == true end end return false end -- iterate through claims, claim's qualifiers or claim's references to collect values function State:iterate(statements, hooks, matchHook) matchHook = matchHook or alwaysTrue local matches = false local rankPos = nil local result, gotRequired for i, v in ipairs(statements) do -- rankPos will be nil for non-claim statements (e.g. qualifiers, references, etc.) matches, rankPos = matchHook(self, v) if matches then result = {count = 0} -- collection of arrays with value objects local function walk(formatTable) local miss for i2, v2 in pairs(formatTable.req) do -- call a hook, adding its return value to the result miss = self:callHook(i2, hooks, v, result) if miss then -- we miss a required value for this level, so return false return false end if result.count == hooks.count then -- we're done if all hooks have been called; -- returning at this point breaks the loop return true end end for i2, v2 in ipairs(formatTable) do if result.count == hooks.count then -- we're done if all hooks have been called; -- returning at this point prevents further childs from being processed return true end if v2.child then walk(v2.child) end end return true end gotRequired = walk(self.parsedFormat) -- only append the result if we got values for all required parameters on the root level if gotRequired then -- if we have a rankPos (only with matchHook() for complete claims), then update the foundRank if rankPos and self.conf.foundRank > rankPos then self.conf.foundRank = rankPos end -- append the result self.results[#self.results + 1] = result -- break if we only need a single value if self.singleValue then break end end end end return self:out() end function p.property(frame) loadSubmodules(frame) return p._property(copyTable(frame.args)) end function p._property(args) loadSubmodules() return execCommand(args, "property") end function p.properties(frame) loadSubmodules(frame) return p._properties(copyTable(frame.args)) end function p._properties(args) loadSubmodules() return execCommand(args, "properties") end function p.qualifier(frame) loadSubmodules(frame) return p._qualifier(copyTable(frame.args)) end function p._qualifier(args) loadSubmodules() return execCommand(args, "qualifier") end function p.qualifiers(frame) loadSubmodules(frame) return p._qualifiers(copyTable(frame.args)) end function p._qualifiers(args) loadSubmodules() return execCommand(args, "qualifiers") end function p.reference(frame) loadSubmodules(frame) return p._reference(copyTable(frame.args)) end function p._reference(args) loadSubmodules() return execCommand(args, "reference") end function p.references(frame) loadSubmodules(frame) return p._references(copyTable(frame.args)) end function p._references(args) loadSubmodules() return execCommand(args, "references") end function execCommand(args, funcName) _ = Config.new() _:processFlagOrCommand(funcName) -- process first command (== function name) local parsedFormat, formatParams, claims, sep local hooks = {count = 0} local nextArg = args[1] local nextIndex = 2 -- process flags and commands while _:processFlagOrCommand(nextArg) do nextArg = args[nextIndex] nextIndex = nextIndex + 1 end if nextArg then nextArg = mw.text.trim(nextArg) else nextArg = "" end -- check for optional item ID if nextArg:sub(1,1):upper() == "Q" then _.entity = mw.wikibase.getEntity(nextArg) -- item ID given _.propertyID = mw.text.trim(args[nextIndex] or "") -- property ID nextIndex = nextIndex + 1 else _.entity = mw.wikibase.getEntity() -- no item ID given, use item connected to current page _.propertyID = nextArg -- property ID end -- check if given property ID is an alias if aliasesP[_.propertyID] then _.propertyID = aliasesP[_.propertyID] end _.propertyID = _.propertyID:upper() if _.states.qualifiersCount > 0 then -- do further processing if "qualifier(s)" command was given if #args - nextIndex + 1 > _.states.qualifiersCount then -- claim ID or literal value has been given nextArg = args[nextIndex] -- don't trim because might be single space representing 'somevalue' nextIndex = nextIndex + 1 _.propertyValue = nextArg end for i = 1, _.states.qualifiersCount do nextArg = mw.text.trim(args[nextIndex] or "") -- is a qualifierID nextIndex = nextIndex + 1 -- check if given qualifier ID is an alias if aliasesP[nextArg] then nextArg = aliasesP[nextArg] end _.qualifierIDs[parameters.qualifier..i] = nextArg:upper() end elseif _.states[parameters.reference] then -- do further processing if "reference(s)" command was given nextArg = args[nextIndex] nextIndex = nextIndex + 1 _.propertyValue = nextArg -- claim ID or literal value (possibly nil) end -- check for special property value 'somevalue' or 'novalue' if _.propertyValue then if _.propertyValue ~= "" and mw.text.trim(_.propertyValue) == "" then _.propertyValue = " " -- single space represents 'somevalue', whereas empty string represents 'novalue' else _.propertyValue = mw.text.trim(_.propertyValue) end end -- parse the desired format, or choose an appropriate format if args["format"] then parsedFormat, formatParams = parseFormat(replaceHTMLSpaces(mw.text.trim(args["format"]))) elseif _.states.qualifiersCount > 0 then -- "qualifier(s)" command given if _.states[parameters.property] then -- "propert(y|ies)" command given parsedFormat, formatParams = parseFormat(formats.propertyWithQualifier) else parsedFormat, formatParams = parseFormat(formats.qualifier) end elseif _.states[parameters.property] then -- "propert(y|ies)" command given parsedFormat, formatParams = parseFormat(formats.property) else -- "reference(s)" command given parsedFormat, formatParams = parseFormat(formats.reference) end -- if a "qualifier(s)" command and no "propert(y|ies)" command has been given, make the movable separator a semicolon if _.states.qualifiersCount > 0 and not _.states[parameters.property] then _.separators["sep"..parameters.separator][1] = {";"} end -- if only "reference(s)" has been given, set the default separator to none (except when raw) if _.states[parameters.reference] and not _.states[parameters.property] and _.states.qualifiersCount == 0 and not _.states[parameters.reference].rawValue then _.separators["sep"][1] = nil end -- if exactly one "qualifier(s)" command has been given, make "sep%q" point to "sep%q1" to make them equivalent; -- must come BEFORE overriding the separator values if _.states.qualifiersCount == 1 then _.separators["sep"..parameters.qualifier] = _.separators["sep"..parameters.qualifier.."1"] end -- process overridden separator values; -- must come AFTER parsing the formats for i, v in pairs(_.separators) do if args[i] then sep = replaceHTMLSpaces(mw.text.trim(args[i])) if sep ~= "" then _.separators[i][1] = {sep} else _.separators[i][1] = nil end end end -- make sure that at least one required parameter has been defined if not next(parsedFormat.req) then error(missingRequiredParameterError()) end -- make sure that the separator parameter "%s" is not amongst the required parameters if parsedFormat.req[parameters.separator] then error(extraRequiredParameterError(parameters.separator)) end -- define the hooks that should be called (getProperty, getQualifiers, getReferences); -- only define a hook if both its command ("propert(y|ies)", "reference(s)", "qualifier(s)") and its parameter ("%p", "%r", "%q1", "%q2", "%q3") have been given for i, v in pairs(_.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[string.sub(i, 1, 2)] then hooks[i] = getHookName(i, 1) hooks.count = hooks.count + 1 end end -- the "%q" parameter is not attached to a state, but is a collection of the results of multiple states (attached to "%q1", "%q2", "%q3", ...); -- so if this parameter is given then this hook must be defined separately, but only if at least one "qualifier(s)" command has been given if formatParams[parameters.qualifier] and _.states.qualifiersCount > 0 then hooks[parameters.qualifier] = getHookName(parameters.qualifier, 1) hooks.count = hooks.count + 1 end -- create a state for "properties" if it doesn't exist yet, which will be used as a base configuration for each claim iteration; -- must come AFTER defining the hooks if not _.states[parameters.property] then _.states[parameters.property] = State.new(_) end -- set the parsed format and the separators (and optional punctuation mark) _.states[parameters.property].parsedFormat = parsedFormat _.states[parameters.property].separator = _.separators["sep"] _.states[parameters.property].movSeparator = _.separators["sep"..parameters.separator] _.states[parameters.property].puncMark = _.separators["punc"] if _.entity and _.entity.claims then claims = _.entity.claims[_.propertyID] end if claims then -- first sort the claims on rank to pre-define the order of output (preferred first, then normal, then deprecated) claims = sortOnRank(claims) -- then iterate through the claims to collect values return concatValues(_.states[parameters.property]:iterate(claims, hooks, State.claimMatches)) -- pass property State with level 1 hooks and matchHook else return "" end end function p.label(frame) loadSubmodules(frame) return p._label(copyTable(frame.args)) end function p._label(args, _) _ = _ or Config.new() _.curState = State.new(_) loadSubmodules() local ID = nil local label = "" local title = nil local nextArg = args[1] local nextIndex = 2 while _:processFlag(nextArg) do nextArg = args[nextIndex] nextIndex = nextIndex + 1 end if nextArg then ID = mw.text.trim(nextArg) if ID == "" then ID = nil end end if ID then if aliasesP[ID] then ID = aliasesP[ID] end ID = ID:upper() -- check if this is a valid ID, and if the number is not larger than max int (to prevent error) if not string.match(ID, '^[QP]%d+$') or tonumber(string.match(ID, '%d+')) > 2147483647 then return "" end if _.curState.rawValue and not _.pageTitle then if mw.wikibase.getEntity(ID) or mw.wikibase.resolvePropertyId(ID) then if _.curState.linked then if ID:sub(1,1) == "P" then label = "[[d:Property:" .. ID .. "|" .. ID .. "]]" else label = "[[d:" .. ID .. "|" .. ID .. "]]" end else label = ID end end return label end else if _.curState.rawValue and not _.pageTitle then label = mw.wikibase.getEntityIdForCurrentPage() or "" if _.curState.linked and label ~= "" then label = "[[d:" .. label .. "|" .. label .. "]]" end return label end end if ID and ID:sub(1,1) == "P" then if not _.pageTitle then label = mw.wikibase.label(ID) or "" if _.curState.linked and label ~= "" then label = "[[d:Property:" .. ID .. "|" .. label .. "]]" end end else if not _.pageTitle then if _.curState.shortName then label = getShortName(ID) end -- at this point, 'label' will be a string and not nil if label == "" then label = mw.wikibase.label(ID) end else -- set 'label' to nil so 'title' will always prevail label = nil end -- at this point, 'label' will be nil or a non-empty string if _.curState.linked or label == nil then if ID then title = mw.wikibase.sitelink(ID) else title = mw.title.getCurrentTitle().prefixedText end end if _.curState.linked and title then label = "[[" .. title .. "|" .. (label or title) .. "]]" else label = label or title or "" end end return label end function p.title(frame) loadSubmodules(frame) return p._title(copyTable(frame.args)) end function p._title(args, _) _ = _ or Config.new() _.pageTitle = true -- loadSubmodules() will already be called by _label() return p._label(args, _) end -- main function that is supposed to be used by wrapper templates function p.main(frame) local f, args, i, v loadSubmodules(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 error(i18n["errors"]["no-function-specified"]) end f = mw.text.trim(frame.args[1]) if f == "main" then error(i18n["errors"]["main-called-twice"]) end assert(p["_"..f], applyStringParams(i18n['errors']['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