Module:Sandbox/Thayts/Wd
-- Original module located at :en:Module:Wd and :en:Module:Wd/i18n.
local p = {}
local arg = ...
local i18n
--==-- Public declarations and initializations --==--
p.claimCommands = {
property = "property",
properties = "properties",
qualifier = "qualifier",
qualifiers = "qualifiers",
reference = "reference",
references = "references"
}
p.generalCommands = {
label = "label",
title = "title",
description = "description",
alias = "alias",
aliases = "aliases",
badge = "badge",
badges = "badges"
}
p.flags = {
linked = "linked",
short = "short",
raw = "raw",
multilanguage = "multilanguage",
unit = "unit",
number = "number",
-------------
preferred = "preferred",
normal = "normal",
deprecated = "deprecated",
best = "best",
future = "future",
current = "current",
former = "former",
edit = "edit",
editAtEnd = "edit@end",
mdy = "mdy",
single = "single",
sourced = "sourced"
}
p.args = {
eid = "eid",
page = "page",
date = "date",
sort = "sort"
}
--==-- Public constants --==--
-- An Ogham space that, just like a normal space, is not accepted by Wikidata as a valid single-character string value,
-- but which does not get trimmed as leading/trailing whitespace when passed in an invocation's named argument value.
-- This allows it to be used as a special character representing the special value 'somevalue' unambiguously.
-- Another advantage of this character is that it is usually visible as a dash instead of whitespace.
p.SOMEVALUE = " "
p.JULIAN = "Julian"
--==-- Private constants --==--
local NB_SPACE = " "
local ENC_PIPE = "|"
local SLASH = "/"
local LAT_DIR_N_EN = "N"
local LAT_DIR_S_EN = "S"
local LON_DIR_E_EN = "E"
local LON_DIR_W_EN = "W"
local PROP = "prop"
local RANK = "rank"
local CLAIM = "_claim"
local REFERENCE = "_reference"
local UNIT = "_unit"
local UNKNOWN = "_unknown"
--==-- Private declarations and initializations --==--
local aliasesP = {
coord = "P625",
-----------------------
image = "P18",
author = "P50",
publisher = "P123",
importedFrom = "P143",
statedIn = "P248",
pages = "P304",
language = "P407",
hasPart = "P527",
publicationDate = "P577",
startTime = "P580",
endTime = "P582",
chapter = "P792",
retrieved = "P813",
referenceURL = "P854",
sectionVerseOrParagraph = "P958",
archiveURL = "P1065",
title = "P1476",
formatterURL = "P1630",
quote = "P1683",
shortName = "P1813",
definingFormula = "P2534",
archiveDate = "P2960",
inferredFrom = "P3452",
typeOfReference = "P3865",
column = "P3903"
}
local aliasesQ = {
julianCalendar = "Q11184",
percentage = "Q11229",
commonEra = "Q208141",
prolepticJulianCalendar = "Q1985786",
citeWeb = "Q5637226",
citeQ = "Q22321052"
}
local parameters = {
property = "p",
qualifier = "q",
reference = "r",
alias = "a",
badge = "b",
separator = "s"
}
local formats = {
property = "%p[%s][%r]",
qualifier = "%q[%s][%r]",
reference = "%r",
propertyWithQualifier = "%p[ (%q)][%s][%r]"
}
local hookNames = { -- {level_1, level_2}
[parameters.property] = {"getProperty"},
[parameters.reference] = {"getReferences", "getReference"},
[parameters.qualifier] = {"getAllQualifiers"},
[parameters.qualifier.."0"] = {"getQualifiers", "getQualifier"},
[parameters.alias] = {"getAlias"},
[parameters.badge] = {"getBadge"},
[parameters.separator] = {"getSeparator"}
}
local defaultSeparators = {
["sep"] = " ",
["sep%s"] = ",",
["sep%q"] = "; ",
["sep%q0"] = ", ",
["sep%r"] = "", -- none
["punc"] = "" -- none
}
local rankTable = {
["preferred"] = {1},
["normal"] = {2},
["deprecated"] = {3}
}
--==-- Private functions --==--
-- used to merge output arrays together;
-- note that it currently mutates the first input array
local function mergeArrays(a1, a2)
for i = 1, #a2 do
a1[#a1 + 1] = a2[i]
end
return a1
end
-- used to make frame.args mutable, to replace #frame.args (which is always 0)
-- with the actual amount and to simply copy tables;
-- does a shallow copy, so nested tables are not copied but linked
local function copyTable(tIn)
if not tIn then
return nil
end
local tOut = {}
for i, v in pairs(tIn) do
tOut[i] = v
end
return tOut
end
-- implementation of pairs that skips numeric keys
local function npairs(t)
return function(t, k)
local v
repeat
k, v = next(t, k)
until k == nil or type(k) ~= 'number'
return k, v
end, t , nil
end
local function toString(object, insideRef, refs)
local mt, value
insideRef = insideRef or false
refs = refs or {{}}
if not object then
refs.squashed = false
return ""
end
mt = getmetatable(object)
if mt.sep then
local array = {}
for _, obj in ipairs(object) do
local ref = refs[1]
if not insideRef and array[1] and mt.sep[1] ~= "" then
refs[1] = {}
end
value = toString(obj, insideRef, refs)
if value ~= "" or (refs.squashed and not array[1]) then
array[#array + 1] = value
else
refs[1] = ref
end
end
value = table.concat(array, mt.sep[1])
else
if mt.hash then
if refs[1][mt.hash] then
refs.squashed = true
return ""
end
insideRef = true
end
if mt.format then
local ref, squashed, array
local function processFormat(format)
local array = {}
local params = {}
-- see if there are required parameters to expand
if format.req then
-- before expanding any parameters, check that none of them is nil
for i, _ in pairs(format.req) do
if not object[i] then
return array -- empty
end
end
end
-- process the format and childs (+1 is needed to process trailing childs)
for i = 1, #format + 1 do
if format.childs and format.childs[i] then
for _, child in ipairs(format.childs[i]) do
local ref = copyTable(refs[1])
local squashed = refs.squashed
local childArray = processFormat(child)
if not childArray[1] then
refs[1] = ref
refs.squashed = squashed
else
mergeArrays(array, childArray)
end
end
end
if format.params and format.params[i] then
array[#array + 1] = toString(object[format[i]], insideRef, refs)
if array[#array] == "" and not refs.squashed then
return {}
end
elseif format[i] then
array[#array + 1] = format[i]
if not insideRef then
refs[1] = {}
end
end
end
return array
end
ref = copyTable(refs[1])
squashed = refs.squashed
array = processFormat(mt.format)
if not array[1] then
refs[1] = ref
refs.squashed = squashed
end
value = table.concat(array)
else
if mt.expand then
local args = {}
for i, j in npairs(object) do
args[i] = toString(j, insideRef)
end
value = mw.getCurrentFrame():expandTemplate{title=mt.expand, args=args}
elseif object.label then
value = object.label
else
value = table.concat(object)
end
if not insideRef and not mt.hash and value ~= "" then
refs[1] = {}
end
end
if mt.sub then
for i, j in pairs(mt.sub) do
value = mw.ustring.gsub(value, i, j)
end
end
if value ~= "" and mt.tag then
value = mw.getCurrentFrame():extensionTag(mt.tag[1], value, mt.tag[2])
if mt.hash then
refs[1][mt.hash] = true
end
end
refs.squashed = false
end
if mt.trail then
value = value .. mt.trail
if not insideRef then
refs[1] = {}
refs.squashed = false
end
end
return value
end
local function loadI18n(aliasesP, frame)
local title
if frame then
-- current module invoked by page/template, get its title from frame
title = frame:getTitle()
else
-- current module included by other module, get its title from ...
title = arg
end
if not i18n then
i18n = require(title .. "/i18n").init(aliasesP)
end
end
local function replaceAlias(id)
if aliasesP[id] then
id = aliasesP[id]
end
return id
end
local function errorText(code, param)
local text = i18n["errors"][code]
if param then text = mw.ustring.gsub(text, "$1", param) end
return text
end
local function throwError(errorMessage, param)
error(errorText(errorMessage, param))
end
local function replaceDecimalMark(num)
return mw.ustring.gsub(num, "[.]", i18n['numeric']['decimal-mark'], 1)
end
local function padZeros(num, numDigits)
local numZeros
local negative = false
if num < 0 then
negative = true
num = num * -1
end
num = tostring(num)
numZeros = numDigits - num:len()
for _ = 1, numZeros do
num = "0"..num
end
if negative then
num = "-"..num
end
return num
end
local function replaceSpecialChar(chr)
if chr == '_' then
-- replace underscores with spaces
return ' '
else
return chr
end
end
local function replaceSpecialChars(str)
local chr
local esc = false
local strOut = ""
for i = 1, #str do
chr = str:sub(i,i)
if not esc then
if chr == '\\' then
esc = true
else
strOut = strOut .. replaceSpecialChar(chr)
end
else
strOut = strOut .. chr
esc = false
end
end
return strOut
end
local function isPropertyID(id)
return id:match('^P%d+$')
end
local function buildLink(target, label)
local mt = {__tostring=toString}
if not label then
mt.format = {"[", target, "]"}
return setmetatable({target, target=target, isWebTarget=true}, mt), mt
else
mt.format = {"[", target, " ", label, "]"}
return setmetatable({label, target=target, isWebTarget=true}, mt), mt
end
end
local function buildWikilink(target, label)
local mt = {__tostring=toString}
if not label or target == label then
mt.format = {"", target, ""}
return setmetatable({target, target=target}, mt), mt
else
mt.format = {"", label, ""}
return setmetatable({label, target=target}, mt), mt
end
end
-- does a shallow copy of both the object and the metatable's format,
-- so nested tables are not copied but linked
local function copyValue(vIn)
local vOut = copyTable(vIn)
local mtIn = getmetatable(vIn)
local mtOut = {format=copyTable(mtIn.format), __tostring=toString}
return setmetatable(vOut, mtOut)
end
local function split(str, del, from)
local i, j
from = from or 1
i, j = str:find(del, from)
if i and j then
return str:sub(1, i - 1), str:sub(j + 1), i, j
end
return str
end
local function urlEncode(url)
local i, j, urlSplit, urlPath
local urlPre = ""
local count = 0
local pathEnc = {}
local delim = ""
i, j = url:find("//", 1, true)
-- check if a hostname is present
if i == 1 or (i and url:sub(i - 1, i - 1) == ':') then
urlSplit = {split(url, "[/?#]", j + 1)}
urlPre = urlSplit[1]
-- split the path from the hostname
if urlSplit[2] then
urlPath = url:sub(urlSplit[3], urlSplit[4]) .. urlSplit[2]
else
urlPath = ""
end
else
urlPath = url -- no hostname is present, so it's a path
end
-- encode each part of the path
for part in mw.text.gsplit(urlPath, "[;/?:@&=+$,#]") do
pathEnc[#pathEnc + 1] = delim
pathEnc[#pathEnc + 1] = mw.uri.encode(mw.uri.decode(part, "PATH"), "PATH")
count = count + #part + 1
delim = urlPath:sub(count, count)
end
-- return the properly encoded URL
return urlPre .. table.concat(pathEnc)
end
local function parseWikidataURL(url)
local id
if url:match('^http[s]?://') then
id = ({split(url, "Q")})[2]
if id then
return "Q" .. id
end
end
return nil
end
local function parseDate(dateStr, precision)
precision = precision or "d"
local i, j, index, ptr
local parts = {nil, nil, nil}
if dateStr == nil then
return parts[1], parts[2], parts[3] -- year, month, day
end
-- 'T' for snak values, '/' for outputs with '/Julian' attached
i, j = dateStr:find("[T/]")
if i then
dateStr = dateStr:sub(1, i-1)
end
local from = 1
if dateStr:sub(1,1) == "-" then
-- this is a negative number, look further ahead
from = 2
end
index = 1
ptr = 1
i, j = dateStr:find("-", from)
if i then
-- year
parts[index] = tonumber(mw.ustring.gsub(dateStr:sub(ptr, i-1), "^\+(.+)$", "%1"), 10) -- remove '+' sign (explicitly give base 10 to prevent error)
if parts[index] == -0 then
parts[index] = tonumber("0") -- for some reason, 'parts[index] = 0' may actually store '-0', so parse from string instead
end
if precision == "y" then
-- we're done
return parts[1], parts[2], parts[3] -- year, month, day
end
index = index + 1
ptr = i + 1
i, j = dateStr:find("-", ptr)
if i then
-- month
parts[index] = tonumber(dateStr:sub(ptr, i-1), 10)
if precision == "m" then
-- we're done
return parts[1], parts[2], parts[3] -- year, month, day
end
index = index + 1
ptr = i + 1
end
end
if dateStr:sub(ptr) ~= "" then
-- day if we have month, month if we have year, or year
parts[index] = tonumber(dateStr:sub(ptr), 10)
end
return parts[1], parts[2], parts[3] -- year, month, day
end
local function datePrecedesDate(dateA, dateB)
if not dateA[1] or not dateB[1] then
return nil
end
dateA[2] = dateA[2] or 1
dateA[3] = dateA[3] or 1
dateB[2] = dateB[2] or 1
dateB[3] = dateB[3] or 1
if dateA[1] < dateB[1] then
return true
end
if dateA[1] > dateB[1] then
return false
end
if dateA[2] < dateB[2] then
return true
end
if dateA[2] > dateB[2] then
return false
end
if dateA[3] < dateB[3] then
return true
end
return false
end
local function newOptionalHook(hooks)
return function(state, claim)
state:callHooks(hooks, claim)
return true
end
end
local function newPersistHook(params)
return function(state, claim)
local param0
if not state.resultsByStatement[claim][1] then
local mt = copyTable(state.metaTable)
mt.rank = claim.rank
state.resultsByStatement[claim][1] = setmetatable({}, mt)
local rankPos = (rankTable[claim.rank] or {})[1]
if rankPos and rankPos < state.conf.foundRank then
state.conf.foundRank = rankPos
end
end
for param, _ in pairs(params) do
if not state.resultsByStatement[claim][1][param] then
state.resultsByStatement[claim][1][param] = state.resultsByStatement[claim][param] -- persist result
-- if we need to persist "q", then also persist "q1", "q2", etc.
if param == parameters.qualifier then
for i = 1, state.conf.qualifiersCount do
param0 = param..i
if state.resultsByStatement[claim][param0][1] then
state.resultsByStatement[claim][1][param0] = state.resultsByStatement[claim][param0]
end
end
end
end
end
return true
end
end
local function parseFormat(state, formatStr, i)
local iNext, childHooks, param0
local esc = false
local param = 0
local str = ""
local hooks = {}
local optionalHooks = {}
local parsedFormat = {}
local params = {}
local childs = {}
local req = {}
i = i or 1
local function flush()
if str ~= "" then
parsedFormat[#parsedFormat + 1] = str
if param > 0 then
req[str] = true
params[#parsedFormat] = true
if not state.hooksByParam[str] then
if state.conf.statesByParam[str] or str == parameters.separator then
state:newValueHook(str)
elseif str == parameters.qualifier and state.conf.statesByParam[str.."1"] then
state:newValueHook(str)
for i = 1, state.conf.qualifiersCount do
param0 = str..i
if not state.hooksByParam[param0] then
state:newValueHook(param0)
end
end
end
end
hooks[#hooks + 1] = state.hooksByParam[str]
end
str = ""
end
param = 0
end
while i <= #formatStr do
chr = formatStr:sub(i,i)
if not esc then
if chr == '\\' then
if param > 0 then
flush()
end
esc = true
elseif chr == '%' then
flush()
param = 2
elseif chr == '[' then
flush()
iNext = #parsedFormat + 1
if not childs[iNext] then
childs[iNext] = {}
end
childs[iNext][#childs[iNext] + 1], childHooks, i = parseFormat(state, formatStr, i + 1)
if childHooks[1] then
optionalHooks[#optionalHooks + 1] = newOptionalHook(childHooks)
end
elseif chr == ']' then
break
else
if param > 1 then
param = param - 1
elseif param == 1 and not chr:match('%d') then
flush()
end
str = str .. replaceSpecialChar(chr)
end
else
str = str .. chr
esc = false
end
i = i + 1
end
flush()
if hooks[1] then
hooks[#hooks + 1] = newPersistHook(req)
end
mergeArrays(hooks, optionalHooks)
parsedFormat.params = params
parsedFormat.childs = childs
parsedFormat.req = req
return parsedFormat, hooks, i
end
-- this function must stay in sync with the getValue function
local function parseValue(value, datatype)
if datatype == 'quantity' then
return {tonumber(value)}
elseif datatype == 'time' then
local tail
local dateValue = {}
dateValue.len = 4 -- length used for comparing
value, tail = split(value, SLASH)
if tail and tail:lower() == p.JULIAN:lower() then
dateValue[4] = p.JULIAN
end
if value:sub(1,1) == "-" then
dateValue[1], value = split(value, "-", 2)
else
dateValue[1], value = split(value, "-")
end
dateValue[1] = tonumber(dateValue[1])
if value then
dateValue[2], value = split(value, "-")
dateValue[2] = tonumber(dateValue[2])
if value then
dateValue[3] = tonumber(value)
end
end
return dateValue
elseif datatype == 'globecoordinate' then
local part, partsIndex
local coordValue = {}
coordValue.len = 6 -- length used for comparing
for i = 1, 4 do
part, value = split(value, SLASH)
coordValue[i] = tonumber(part)
if not coordValue[i] or not value or i == 4 then
coordValue[i] = nil
partsIndex = i - 1
break
end
end
if part:upper() == LAT_DIR_S_EN then
for i = 1, partsIndex do
coordValue[i] = -coordValue[i]
end
end
if value then
partsIndex = partsIndex + 3
for i = 4, partsIndex do
part, value = split(value, SLASH)
coordValue[i] = tonumber(part)
if not coordValue[i] or not value then
partsIndex = i - 1
break
end
end
if value and value:upper() == LON_DIR_W_EN then
for i = 4, partsIndex do
coordValue[i] = -coordValue[i]
end
end
end
return coordValue
elseif datatype == 'wikibase-entityid' then
return {value:sub(1,1):upper(), tonumber(value:sub(2))}
end
return {value}
end
local function getEntityId(arg, eid, page, allowOmitPropPrefix)
local id = nil
local prop = nil
if arg then
if arg:sub(1,1) == ":" then
page = arg
eid = nil
elseif arg:sub(1,1):upper() == "Q" or arg:sub(1,9):lower() == "property:" or allowOmitPropPrefix then
eid = arg
page = nil
else
prop = arg
end
end
if eid then
if eid:sub(1,9):lower() == "property:" then
id = replaceAlias(mw.text.trim(eid:sub(10)))
if id:sub(1,1):upper() ~= "P" then
id = ""
end
else
id = replaceAlias(eid)
end
elseif page then
if page:sub(1,1) == ":" then
page = mw.text.trim(page:sub(2))
end
id = mw.wikibase.getEntityIdForTitle(page) or ""
end
if not id then
id = mw.wikibase.getEntityIdForCurrentPage() or ""
end
id = id:upper()
if not mw.wikibase.isValidEntityId(id) then
id = ""
end
return id, prop
end
local function nextArg(args)
local arg = args[args.pointer]
if arg then
args.pointer = args.pointer + 1
return mw.text.trim(arg)
else
return nil
end
end
--==-- Classes --==--
local Config = {}
-- allows for recursive calls
function Config:new()
local cfg = setmetatable({}, self)
self.__index = self
cfg.separators = {
["sep"] = {defaultSeparators["sep"]},
["sep%q"] = {defaultSeparators["sep%q"]},
["sep%r"] = {defaultSeparators["sep%r"]},
["sep%s"] = setmetatable({defaultSeparators["sep%s"]}, {__tostring=toString}),
["punc"] = setmetatable({defaultSeparators["punc"]}, {__tostring=toString})
}
cfg.entity = nil
cfg.entityID = nil
cfg.propertyID = nil
cfg.propertyValue = nil
cfg.qualifierIDs = {}
cfg.qualifierIDsAndValues = {}
cfg.qualifiersCount = 0
cfg.bestRank = true
cfg.ranks = {true, true, false} -- preferred = true, normal = true, deprecated = false
cfg.foundRank = #cfg.ranks
cfg.flagBest = false
cfg.flagRank = false
cfg.filterBeforeRank = false
cfg.periods = {true, true, true} -- future = true, current = true, former = true
cfg.flagPeriod = false
cfg.atDate = {parseDate(os.date('!%Y-%m-%d'))} -- today as {year, month, day}
cfg.curTime = os.time()
cfg.mdyDate = false
cfg.singleClaim = false
cfg.sourcedOnly = false
cfg.editable = false
cfg.editAtEnd = false
cfg.inSitelinks = false
cfg.emptyAllowed = false
cfg.langCode = mw.language.getContentLanguage().code
cfg.langName = mw.language.fetchLanguageName(cfg.langCode, cfg.langCode)
cfg.langObj = mw.language.new(cfg.langCode)
cfg.siteID = mw.wikibase.getGlobalSiteId()
cfg.movSeparator = cfg.separators["sep%s"]
cfg.puncMark = cfg.separators["punc"]
cfg.statesByParam = {}
cfg.statesByID = {}
cfg.curState = nil
cfg.sortKeys = {}
return cfg
end
local State = {}
function State:new(cfg, level, param, id)
local stt = setmetatable({}, self)
self.__index = self
stt.conf = cfg
stt.level = level
stt.param = param
stt.linked = false
stt.rawValue = false
stt.shortName = false
stt.anyLanguage = false
stt.freeUnit = false
stt.freeNumber = false
stt.maxResults = 0 -- 0 means unlimited
stt.metaTable = nil
stt.results = {}
stt.resultsByStatement = {}
stt.references = {}
stt.hooksByParam = {}
stt.hooksByID = {}
stt.valHooksByIdOrParam = {}
stt.valHooks = {}
stt.sortable = {}
stt.sortPaths = {}
stt.propState = nil
if level and level > 1 then
stt.hooks = {stt:newValueHook(param), stt.addToResults}
stt.separator = cfg.separators["sep%"..param] or cfg.separators["sep"] -- fall back to "sep" for getAlias and getBadge
stt.resultsDatatype = nil
else
stt.hooks = {}
stt.separator = cfg.separators["sep"]
stt.resultsDatatype = {CLAIM}
end
if id then
cfg:addToStatesByID(stt, id)
elseif param then
cfg.statesByParam[param] = stt
end
return stt
end
function Config:addToStatesByID(state, id)
if not self.statesByID[id] then
self.statesByID[id] = {}
end
self.statesByID[id][#self.statesByID[id] + 1] = state
end
-- if id == nil then item connected to current page is used
function Config:getLabel(id, raw, link, short, emptyAllowed)
local label
local mt = {__tostring=toString}
local value = setmetatable({}, mt)
if not id then
id = mw.wikibase.getEntityIdForCurrentPage()
if not id then
return value, mt -- empty value
end
end
id = id:upper() -- just to be sure
-- check if given id actually exists
if not mw.wikibase.isValidEntityId(id) or not mw.wikibase.entityExists(id) then
return value, mt -- empty value
end
if raw then
label = id
else
-- try short name first if requested
if short then
label = p.property{aliasesP.shortName, [p.args.eid] = id, format = "%"..parameters.property} -- get short name
if label == "" then
label = nil
end
end
-- get label
if not label then
label = mw.wikibase.getLabelByLang(id, self.langCode)
end
if not label and not emptyAllowed then
return value, mt -- empty value
end
value.label = label or ""
end
-- split id for potential numeric sorting
value[1] = id:sub(1,1)
value[2] = tonumber(id:sub(2))
-- build a link if requested
if link then
if raw or value[1] == "P" then
-- link to Wikidata if raw or if property (which has no sitelink)
value.target = id
if value[1] == "P" then
value.target = "Property:" .. value.target
end
value.target = "d:" .. value.target
else
-- else, value[1] == "Q"
value.target = mw.wikibase.getSitelink(id)
end
if value.target and label then
mt.format = ({buildWikilink(value.target, label)})[2].format
end
end
return value, mt
end
function Config:getEditIcon()
local value = ""
local prefix = ""
local front = NB_SPACE
local back = ""
if self.entityID:sub(1,1) == "P" then
prefix = "Property:"
end
if self.editAtEnd then
front = ''
back = ''
end
value = "[[File:OOjs UI icon edit-ltr-progressive.svg|frameless|text-top|10px|alt=" .. i18n['info']['edit-on-wikidata'] .. "|link=https://www.wikidata.org/wiki/" .. prefix .. self.entityID .. "?uselang=" .. self.langCode
if self.propertyID then
value = value .. "#" .. self.propertyID
elseif self.inSitelinks then
value = value .. "#sitelinks-wikipedia"
end
value = value .. "|" .. i18n['info']['edit-on-wikidata'] .. "]]"
return front .. value .. back
end
function Config:convertUnit(unit, raw, link, short)
local itemID
local mt = {__tostring=toString}
local value = setmetatable({}, mt)
if unit == "" or unit == "1" then
return value, mt
end
itemID = parseWikidataURL(unit)
if itemID then
if itemID == aliasesQ.percentage then
value[1] = itemID:sub(1,1)
value[2] = itemID:sub(2)
if not raw then
value.label = "%"
elseif link then
value.target = "d:" .. itemID
mt.format = ({buildWikilink(value.target, itemID)})[2].format
end
else
value, mt = self:getLabel(itemID, raw, link, short)
if value.label then
value.unitSep = NB_SPACE
end
end
end
return value, mt
end
function State:getValue(snak)
return self.conf:getValue(snak, self.rawValue, self.linked, self.shortName, self.anyLanguage, self.freeUnit, self.freeNumber, false, self.conf.emptyAllowed, self.param:sub(1, 1))
end
-- returns a value object in the general form {raw_component_1, raw_component_2, ...} with metatable {format={str_component_1, str_component_2, ...}};
-- 'format' is the string representation of the value in unconcatenated form to exploit Lua's string internalization to reduce memory usage;
-- this function must stay in sync with the parseValue function
function Config:getValue(snak, raw, link, short, anyLang, freeUnit, freeNumber, noSpecial, emptyAllowed, param)
local mt = {__tostring=toString}
local value = setmetatable({}, mt)
if snak.snaktype == 'value' then
local datatype = snak.datavalue.type
local subtype = snak.datatype
local datavalue = snak.datavalue.value
mt.datatype = {datatype}
if datatype == 'string' then
local datatypes = {datatype, subtype}
value[1] = datavalue
mt.datatype = datatypes
if subtype == 'url' and link then
-- create link explicitly
if raw then
-- will render as a linked number like [1]
value, mt = buildLink(datavalue)
else
value, mt = buildLink(datavalue, datavalue)
end
mt.datatype = datatypes
return value
elseif subtype == 'commonsMedia' then
if link then
value, mt = buildWikilink("c:File:" .. datavalue, datavalue)
mt.datatype = datatypes
elseif not raw then
mt.format = {"", "File:", datavalue, ""}
end
return value
elseif subtype == 'geo-shape' and link then
value, mt = buildWikilink("c:" .. datavalue, datavalue)
mt.datatype = datatypes
return value
elseif subtype == 'math' and not raw then
local attribute = nil
if (param == parameters.property or (param == parameters.qualifier and self.propertyID == aliasesP.hasPart)) and snak.property == aliasesP.definingFormula then
attribute = {qid = self.entityID}
end
mt.tag = {"math", attribute}
return value
elseif subtype == 'musical-notation' and not raw then
mt.tag = {"score"}
return value
elseif subtype == 'external-id' and link then
local url = p.property{aliasesP.formatterURL, [p.args.eid] = snak.property, format = "%"..parameters.property} -- get formatter URL
if url ~= "" then
url = urlEncode(mw.ustring.gsub(url, "$1", datavalue))
value, mt = buildLink(url, datavalue)
mt.datatype = datatypes
end
return value
else
return value
end
elseif datatype == 'monolingualtext' then
if anyLang or datavalue['language'] == self.langCode then
value[1] = datavalue['text']
value.language = datavalue['language']
end
return value
elseif datatype == 'quantity' then
local valueStr, unit
if freeNumber or not freeUnit then
-- get value and strip + signs from front
valueStr = mw.ustring.gsub(datavalue['amount'], "^\+(.+)$", "%1")
value[1] = tonumber(valueStr)
-- assertion; we should always have a value
if not value[1] then
return value
end
if not raw then
-- replace decimal mark based on locale
valueStr = replaceDecimalMark(valueStr)
-- add delimiters for readability
valueStr = i18n.addDelimiters(valueStr)
mt.format = {valueStr}
end
end
if freeUnit or (not freeNumber and not raw) then
local mtUnit
unit, mtUnit = self:convertUnit(datavalue['unit'], raw, link, short)
if freeUnit and not freeNumber then
value = unit
mt = mtUnit
mt.datatype = {UNIT}
elseif unit[1] then
value[#value + 1] = unit[1]
value[#value + 1] = unit[2]
value.len = 1 -- (max) length used for sorting
value.target = unit.target
value.unitLabel = unit.label
value.unitSep = unit.unitSep
if raw then
mt.format = {valueStr, SLASH}
mergeArrays(mt.format, mtUnit.format or unit)
else
mt.format[#mt.format + 1] = unit.unitSep -- may be nil
mergeArrays(mt.format, mtUnit.format or {unit.label})
end
end
end
return value
elseif datatype == 'time' then
local y, m, d, p, yDiv, yRound, yFull, yRaw, mStr, ce, calendarID, target
local yFactor = 1
local sign = 1
local prefix = ""
local suffix = ""
local mayAddCalendar = false
local calendar = ""
local precision = datavalue['precision']
if precision == 11 then
p = "d"
elseif precision == 10 then
p = "m"
else
p = "y"
yFactor = 10^(9-precision)
end
y, m, d = parseDate(datavalue['time'], p)
if y < 0 then
sign = -1
y = math.abs(y)
end
-- if precision is tens/hundreds/thousands/millions/billions of years
if precision <= 8 then
yDiv = y / yFactor
-- if precision is tens/hundreds/thousands of years
if precision >= 6 then
mayAddCalendar = true
if precision <= 7 then
-- round centuries/millenniums up (e.g. 20th century or 3rd millennium)
yRound = math.ceil(yDiv)
-- take the first year of the century/millennium as the raw year
-- (e.g. 1901 for 20th century or 2001 for 3rd millennium)
yRaw = (yRound - 1) * yFactor + 1
if not raw then
if precision == 6 then
suffix = i18n['datetime']['suffixes']['millennium']
else
suffix = i18n['datetime']['suffixes']['century']
end
suffix = i18n.getOrdinalSuffix(yRound) .. suffix
else
-- if not verbose, take the first year of the century/millennium
yRound = yRaw
end
else
-- precision == 8
-- round decades down (e.g. 2010s)
yRound = math.floor(yDiv) * yFactor
yRaw = yRound
if not raw then
prefix = i18n['datetime']['prefixes']['decade-period']
suffix = i18n['datetime']['suffixes']['decade-period']
end
end
if sign < 0 then
-- if BCE then compensate for "counting backwards"
-- (e.g. -2019 for 2010s BCE, -2000 for 20th century BCE or -3000 for 3rd millennium BCE)
yRaw = yRaw + yFactor - 1
if raw then
yRound = yRaw
end
end
else
local yReFactor, yReDiv, yReRound
-- round to nearest for tens of thousands of years or more
yRound = math.floor(yDiv + 0.5)
if yRound == 0 then
if precision <= 2 and y ~= 0 then
yReFactor = 1e6
yReDiv = y / yReFactor
yReRound = math.floor(yReDiv + 0.5)
if yReDiv == yReRound then
-- change precision to millions of years only if we have a whole number of them
precision = 3
yFactor = yReFactor
yRound = yReRound
end
end
if yRound == 0 then
-- otherwise, take the unrounded (original) number of years
precision = 5
yFactor = 1
yRound = y
mayAddCalendar = true
end
end
if precision >= 1 and y ~= 0 then
yFull = yRound * yFactor
yReFactor = 1e9
yReDiv = yFull / yReFactor
yReRound = math.floor(yReDiv + 0.5)
if yReDiv == yReRound then
-- change precision to billions of years if we're in that range
precision = 0
yFactor = yReFactor
yRound = yReRound
else
yReFactor = 1e6
yReDiv = yFull / yReFactor
yReRound = math.floor(yReDiv + 0.5)
if yReDiv == yReRound then
-- change precision to millions of years if we're in that range
precision = 3
yFactor = yReFactor
yRound = yReRound
end
end
end
yRaw = yRound * yFactor
if not raw then
if precision == 3 then
suffix = i18n['datetime']['suffixes']['million-years']
elseif precision == 0 then
suffix = i18n['datetime']['suffixes']['billion-years']
else
yRound = yRaw
if yRound == 1 then
suffix = i18n['datetime']['suffixes']['year']
else
suffix = i18n['datetime']['suffixes']['years']
end
end
else
yRound = yRaw
end
end
else
yRound = y
yRaw = yRound
mayAddCalendar = true
end
value[1] = yRaw * sign
value[2] = m
value[3] = d
value.len = 3 -- (max) length used for sorting
value.precision = precision
mt.format = {}
if not raw then
if prefix ~= "" then
mt.format[1] = prefix
end
if m then
mStr = self.langObj:formatDate("F", "1-"..m.."-1")
if d then
if self.mdyDate then
mt.format[#mt.format + 1] = mStr
mt.format[#mt.format + 1] = " "
mt.format[#mt.format + 1] = tostring(d)
mt.format[#mt.format + 1] = ","
else
mt.format[#mt.format + 1] = tostring(d)
mt.format[#mt.format + 1] = " "
mt.format[#mt.format + 1] = mStr
end
else
mt.format[#mt.format + 1] = mStr
end
mt.format[#mt.format + 1] = " "
end
mt.format[#mt.format + 1] = tostring(yRound)
if suffix ~= "" then
mt.format[#mt.format + 1] = suffix
end
if sign < 0 then
ce = i18n['datetime']['BCE']
elseif precision <= 5 then
ce = i18n['datetime']['CE']
end
if ce then
mt.format[#mt.format + 1] = " "
if link then
target = mw.wikibase.getSitelink(aliasesQ.commonEra)
if target then
mergeArrays(mt.format, ({buildWikilink(target, ce)})[2].format)
else
mt.format[#mt.format + 1] = ce
end
else
mt.format[#mt.format + 1] = ce
end
end
else
mt.format[1] = padZeros(yRound * sign, 4)
if m then
mt.format[#mt.format + 1] = "-"
mt.format[#mt.format + 1] = padZeros(m, 2)
if d then
mt.format[#mt.format + 1] = "-"
mt.format[#mt.format + 1] = padZeros(d, 2)
end
end
end
calendarID = parseWikidataURL(datavalue['calendarmodel'])
if calendarID and calendarID == aliasesQ.prolepticJulianCalendar then
value[4] = p.JULIAN -- as value.len == 3, this will not be taken into account while sorting
if mayAddCalendar then
if not raw then
mt.format[#mt.format + 1] = " ("
if link then
target = mw.wikibase.getSitelink(aliasesQ.julianCalendar)
if target then
mergeArrays(mt.format, ({buildWikilink(target, i18n['datetime']['julian'])})[2].format)
else
mt.format[#mt.format + 1] = i18n['datetime']['julian']
end
else
mt.format[#mt.format + 1] = i18n['datetime']['julian']
end
mt.format[#mt.format + 1] = ")"
else
mt.format[#mt.format + 1] = SLASH
mt.format[#mt.format + 1] = p.JULIAN
end
end
end
return value
elseif datatype == 'globecoordinate' then
-- logic from https://github.com/DataValues/Geo (v4.0.1)
local precision, unitsPerDegree, numDigits, strFormat, globe
local latitude, latConv, latLink
local longitude, lonConv, lonLink
local latDirection, latDirectionN, latDirectionS, latDirectionEN
local lonDirection, lonDirectionE, lonDirectionW, lonDirectionEN
local latDegrees, latMinutes, latSeconds
local lonDegrees, lonMinutes, lonSeconds
local degSymbol, minSymbol, secSymbol, separator
local latSign = 1
local lonSign = 1
local latFormat = {}
local lonFormat = {}
if not raw then
latDirectionN = i18n['coord']['latitude-north']
latDirectionS = i18n['coord']['latitude-south']
lonDirectionE = i18n['coord']['longitude-east']
lonDirectionW = i18n['coord']['longitude-west']
degSymbol = i18n['coord']['degrees']
minSymbol = i18n['coord']['minutes']
secSymbol = i18n['coord']['seconds']
separator = i18n['coord']['separator']
else
latDirectionN = LAT_DIR_N_EN
latDirectionS = LAT_DIR_S_EN
lonDirectionE = LON_DIR_E_EN
lonDirectionW = LON_DIR_W_EN
degSymbol = SLASH
minSymbol = SLASH
secSymbol = SLASH
separator = SLASH
end
latitude = datavalue['latitude']
longitude = datavalue['longitude']
if latitude < 0 then
latDirection = latDirectionS
latDirectionEN = LAT_DIR_S_EN
latSign = -1
latitude = math.abs(latitude)
else
latDirection = latDirectionN
latDirectionEN = LAT_DIR_N_EN
end
if longitude < 0 then
lonDirection = lonDirectionW
lonDirectionEN = LON_DIR_W_EN
lonSign = -1
longitude = math.abs(longitude)
else
lonDirection = lonDirectionE
lonDirectionEN = LON_DIR_E_EN
end
precision = datavalue['precision']
if not precision or precision <= 0 then
precision = 1 / 3600 -- precision not set (correctly), set to arcsecond
end
-- remove insignificant detail
latitude = math.floor(latitude / precision + 0.5) * precision
longitude = math.floor(longitude / precision + 0.5) * precision
if precision >= 1 - (1 / 60) and precision < 1 then
precision = 1
elseif precision >= (1 / 60) - (1 / 3600) and precision < (1 / 60) then
precision = 1 / 60
end
if precision >= 1 then
unitsPerDegree = 1
elseif precision >= (1 / 60) then
unitsPerDegree = 60
else
unitsPerDegree = 3600
end
numDigits = math.ceil(-math.log10(unitsPerDegree * precision))
if numDigits <= 0 then
numDigits = tonumber("0") -- for some reason, 'numDigits = 0' may actually store '-0', so parse from string instead
end
strFormat = "%." .. numDigits .. "f"
if precision >= 1 then
latDegrees = strFormat:format(latitude)
lonDegrees = strFormat:format(longitude)
else
latConv = math.floor(latitude * unitsPerDegree * 10^numDigits + 0.5) / 10^numDigits
lonConv = math.floor(longitude * unitsPerDegree * 10^numDigits + 0.5) / 10^numDigits
if precision >= (1 / 60) then
latMinutes = latConv
lonMinutes = lonConv
else
latSeconds = latConv
lonSeconds = lonConv
latMinutes = math.floor(latSeconds / 60)
lonMinutes = math.floor(lonSeconds / 60)
latSeconds = strFormat:format(latSeconds - (latMinutes * 60))
lonSeconds = strFormat:format(lonSeconds - (lonMinutes * 60))
if not raw then
latFormat[5] = replaceDecimalMark(latSeconds)
lonFormat[5] = replaceDecimalMark(lonSeconds)
else
latFormat[5] = latSeconds
lonFormat[5] = lonSeconds
end
latFormat[6] = secSymbol
lonFormat[6] = secSymbol
value[3] = tonumber(latSeconds) * latSign
value[6] = tonumber(lonSeconds) * lonSign
end
latDegrees = math.floor(latMinutes / 60)
lonDegrees = math.floor(lonMinutes / 60)
latMinutes = latMinutes - (latDegrees * 60)
lonMinutes = lonMinutes - (lonDegrees * 60)
if precision >= (1 / 60) then
latMinutes = strFormat:format(latMinutes)
lonMinutes = strFormat:format(lonMinutes)
else
latMinutes = tostring(latMinutes)
lonMinutes = tostring(lonMinutes)
end
if not raw then
latFormat[3] = replaceDecimalMark(latMinutes)
lonFormat[3] = replaceDecimalMark(lonMinutes)
else
latFormat[3] = latMinutes
lonFormat[3] = lonMinutes
end
latFormat[4] = minSymbol
lonFormat[4] = minSymbol
value[2] = tonumber(latMinutes) * latSign
value[5] = tonumber(lonMinutes) * lonSign
latDegrees = tostring(latDegrees)
lonDegrees = tostring(lonDegrees)
end
if not raw then
latFormat[1] = replaceDecimalMark(latDegrees)
lonFormat[1] = replaceDecimalMark(lonDegrees)
else
latFormat[1] = latDegrees
lonFormat[1] = lonDegrees
end
latFormat[2] = degSymbol
lonFormat[2] = degSymbol
value[1] = tonumber(latDegrees) * latSign
value[4] = tonumber(lonDegrees) * lonSign
value.len = 6 -- (max) length used for sorting
latFormat[#latFormat + 1] = latDirection
lonFormat[#lonFormat + 1] = lonDirection
if link then
globe = parseWikidataURL(datavalue['globe'])
if globe then
globe = mw.wikibase.getLabelByLang(globe, "en"):lower()
else
globe = "earth"
end
latLink = table.concat({latDegrees, latMinutes, latSeconds}, "_")
lonLink = table.concat({lonDegrees, lonMinutes, lonSeconds}, "_")
value.target = "https://tools.wmflabs.org/geohack/geohack.php?language="..self.langCode.."¶ms="..latLink.."_"..latDirectionEN.."_"..lonLink.."_"..lonDirectionEN.."_globe:"..globe
value.isWebTarget = true
mt.format = {"[", value.target, " "}
mergeArrays(mt.format, latFormat)
mt.format[#mt.format + 1] = separator
mergeArrays(mt.format, lonFormat)
mt.format[#mt.format + 1] = "]"
else
mt.format = latFormat
mt.format[#mt.format + 1] = separator
mergeArrays(mt.format, lonFormat)
end
return value
elseif datatype == 'wikibase-entityid' then
local itemID = datavalue['numeric-id']
if subtype == 'wikibase-item' then
itemID = "Q" .. itemID
elseif subtype == 'wikibase-property' then
itemID = "P" .. itemID
else
value[1] = errorText('unknown-data-type', subtype)
mt.datatype = {UNKNOWN}
mt.format = {'', value[1], ''}
return value
end
value, mt = self:getLabel(itemID, raw, link, short, emptyAllowed)
mt.datatype = {datatype, subtype}
return value
else
value[1] = errorText('unknown-data-type', datatype)
mt.datatype = {UNKNOWN}
mt.format = {'', value[1], ''}
return value
end
elseif snak.snaktype == 'somevalue' then
if not noSpecial then
value[1] = p.SOMEVALUE -- one Ogham space represents 'somevalue'
if not raw then
mt.format = {i18n['values']['unknown']}
end
end
mt.datatype = {snak.snaktype}
return value
elseif snak.snaktype == 'novalue' then
if not noSpecial then
value[1] = "" -- empty string represents 'novalue'
if not raw then
mt.format = {i18n['values']['none']}
end
end
mt.datatype = {snak.snaktype}
return value
else
value[1] = errorText('unknown-data-type', snak.snaktype)
mt.datatype = {UNKNOWN}
mt.format = {'', value[1], ''}
return value
end
end
function Config:getSingleRawQualifier(claim, qualifierID)
local qualifiers
if claim.qualifiers then qualifiers = claim.qualifiers[qualifierID] end
if qualifiers and qualifiers[1] then
return self:getValue(qualifiers[1], true) -- raw = true
else
return nil
end
end
function Config:snakEqualsValue(snak, value)
local snakValue = self:getValue(snak, true) -- raw = true
local mt = getmetatable(snakValue)
if mt.datatype[1] == UNKNOWN then
return false
end
value = parseValue(value, mt.datatype[1])
for i = 1, (value.len or #value) do
if snakValue[i] ~= value[i] then
return false
end
end
return true
end
function Config:setRank(rank)
local rankPos, step, to
if rank == p.flags.best then
self.bestRank = true
self.flagBest = true -- mark that 'best' flag was given
return
end
if rank:match('[+-]$') then
if rank:sub(-1) == "-" then
step = 1
to = #self.ranks
else
step = -1
to = 1
end
rank = rank:sub(1, -2)
end
if rank == p.flags.preferred then
rankPos = 1
elseif rank == p.flags.normal then
rankPos = 2
elseif rank == p.flags.deprecated then
rankPos = 3
else
return
end
-- one of the rank flags was given, check if another one was given before
if not self.flagRank then
self.ranks = {false, false, false} -- no other rank flag given before, so unset ranks
self.bestRank = self.flagBest -- unsets bestRank only if 'best' flag was not given before
self.flagRank = true -- mark that a rank flag was given
end
if to then
for i = rankPos, to, step do
self.ranks[i] = true
end
else
self.ranks[rankPos] = true
end
end
function Config:setPeriod(period)
local periodPos
if period == p.flags.future then
periodPos = 1
elseif period == p.flags.current then
periodPos = 2
elseif period == p.flags.former then
periodPos = 3
else
return
end
-- one of the period flags was given, check if another one was given before
if not self.flagPeriod then
self.periods = {false, false, false} -- no other period flag given before, so unset periods
self.flagPeriod = true -- mark that a period flag was given
end
self.periods[periodPos] = true
end
function Config:qualifierMatches(claim, id, value)
local qualifiers
if claim.qualifiers then qualifiers = claim.qualifiers[id] end
if qualifiers then
for _, qualifier in pairs(qualifiers) do
if self:snakEqualsValue(qualifier, value) then
return true
end
end
elseif value == "" then
-- if the qualifier is not present then treat it the same as the special value 'novalue'
return true
end
return false
end
function Config:rankMatches(rankPos)
if self.bestRank then
return (self.ranks[rankPos] and self.foundRank >= rankPos)
else
return self.ranks[rankPos]
end
end
function Config:timeMatches(claim)
local startTime = nil
local startTimeY = nil
local startTimeM = nil
local startTimeD = nil
local endTime = nil
local endTimeY = nil
local endTimeM = nil
local endTimeD = nil
if self.periods[1] and self.periods[2] and self.periods[3] then
-- any time
return true
end
startTime = self:getSingleRawQualifier(claim, aliasesP.startTime)
endTime = self:getSingleRawQualifier(claim, aliasesP.endTime)
if startTime and endTime and datePrecedesDate(endTime, startTime) then
-- invalidate end time if it precedes start time
endTime = nil
end
if self.periods[1] then
-- future
if startTime and datePrecedesDate(self.atDate, startTime) then
return true
end
end
if self.periods[2] then
-- current
if (not startTime or not datePrecedesDate(self.atDate, startTime)) and
(not endTime or datePrecedesDate(self.atDate, endTime)) then
return true
end
end
if self.periods[3] then
-- former
if endTime and not datePrecedesDate(self.atDate, endTime) then
return true
end
end
return false
end
function Config:processFlag(flag)
if not flag then
return false
end
if flag == p.flags.linked then
self.curState.linked = true
return true
elseif flag == p.flags.raw then
self.curState.rawValue = true
if self.curState == self.statesByParam[parameters.reference] then
-- raw reference values end with periods and require a separator (other than none)
self.separators["sep%r"][1] = " "
end
return true
elseif flag == p.flags.short then
self.curState.shortName = true
return true
elseif flag == p.flags.multilanguage then
self.curState.anyLanguage = true
return true
elseif flag == p.flags.unit then
self.curState.freeUnit = true
return true
elseif flag == p.flags.number then
self.curState.freeNumber = true
return true
elseif flag == p.flags.mdy then
self.mdyDate = true
return true
elseif flag == p.flags.single then
self.singleClaim = true
return true
elseif flag == p.flags.sourced then
self.sourcedOnly = true
self.filterBeforeRank = true
return true
elseif flag == p.flags.edit then
self.editable = true
return true
elseif flag == p.flags.editAtEnd then
self.editable = true
self.editAtEnd = true
return true
elseif flag == p.flags.best or flag:match('^'..p.flags.preferred..'[+-]?$') or flag:match('^'..p.flags.normal..'[+-]?$') or flag:match('^'..p.flags.deprecated..'[+-]?$') then
self:setRank(flag)
return true
elseif flag == p.flags.future or flag == p.flags.current or flag == p.flags.former then
self:setPeriod(flag)
return true
elseif flag == "" then
-- ignore empty flags and carry on
return true
else
return false
end
end
function Config:processCommand(command, general)
local param, level
if not command then
return false
end
-- prevent general commands from being processed as valid commands if we only expect claim commands
if general then
if command == p.generalCommands.alias or command == p.generalCommands.aliases then
param = parameters.alias
level = 2 -- level 1 hook will be treated as a level 2 hook
elseif command == p.generalCommands.badge or command == p.generalCommands.badges then
param = parameters.badge
level = 2 -- level 1 hook will be treated as a level 2 hook
else
return false
end
elseif command == p.claimCommands.property or command == p.claimCommands.properties then
param = parameters.property
level = 1
elseif command == p.claimCommands.qualifier or command == p.claimCommands.qualifiers then
self.qualifiersCount = self.qualifiersCount + 1
param = parameters.qualifier .. self.qualifiersCount
self.separators["sep%"..param] = {defaultSeparators["sep%q0"]}
level = 2
elseif command == p.claimCommands.reference or command == p.claimCommands.references then
param = parameters.reference
level = 2
else
return nil
end
if self.statesByParam[param] then
return false
end
-- create a new state for each command
self.curState = State:new(self, level, param)
if command == p.claimCommands.property or
command == p.claimCommands.qualifier or
command == p.claimCommands.reference or
command == p.generalCommands.alias or
command == p.generalCommands.badge then
self.curState.maxResults = 1
end
return true
end
function Config:processCommandOrFlag(commandOrFlag)
local success = self:processCommand(commandOrFlag)
if success == nil then
success = self:processFlag(commandOrFlag)
end
return success
end
function Config:processSeparators(args)
for i, v in pairs(self.separators) do
if args[i] then
self.separators[i][1] = replaceSpecialChars(args[i])
end
end
end
function State:isSourced(claim)
return self.hooksByParam[parameters.reference](self, claim)
end
function State:claimMatches(claim)
local matches
-- if a property value was given, check if it matches the claim's property value
if self.conf.propertyValue then
matches = self.conf:snakEqualsValue(claim.mainsnak, self.conf.propertyValue)
else
matches = true
end
-- if any qualifier values were given, check if each matches one of the claim's qualifier values
for i, v in pairs(self.conf.qualifierIDsAndValues) do
matches = (matches and self.conf:qualifierMatches(claim, i, v))
end
-- check if the claim's rank and time period match
matches = (matches and self.conf:rankMatches((rankTable[claim.rank] or {})[1]) and self.conf:timeMatches(claim))
-- if only claims with references must be returned, check if this one has any
if self.conf.sourcedOnly then
matches = (matches and self:isSourced(claim))
end
return matches
end
function State:newSortFunction()
local sortPaths = self.sortPaths
local sortable = self.sortable
local none = {""}
local function resolveValues(sortPath, a, b)
local aVal = nil
local bVal = nil
local sortKey = nil
for _, subPath in ipairs(sortPath) do
local aSub, bSub, key
if #subPath == 0 then
aSub = a
bSub = b
else
if #subPath == 1 then
aSub = subPath[1]
bSub = subPath[1]
if subPath.key then
key = subPath[1]
end
else
aSub, bSub, key = resolveValues(subPath, a, b)
end
sortKey = sortKey or key
end
if not aVal then
aVal = aSub
bVal = bSub
else
aVal = aVal[aSub]
bVal = bVal[bSub]
end
end
return aVal, bVal, sortKey
end
return function(a, b)
for _, sortPath in ipairs(sortPaths) do
local valLen, aPart, bPart
local aValue, bValue, sortKey = resolveValues(sortPath, a, b)
if not sortKey or sortable[sortKey] then
aValue = aValue or none
bValue = bValue or none
if aValue.label or bValue.label then
aValue = {aValue.label or ""}
bValue = {bValue.label or ""}
valLen = 1
else
valLen = aValue.len or #aValue
end
for i = 1, valLen do
aPart = aValue[i]
bPart = bValue[i]
if aPart ~= bPart then
if aPart == nil then
return not sortPath.desc
elseif bPart == nil then
return sortPath.desc
elseif aPart == p.SOMEVALUE or aPart == "" then
if aPart == p.SOMEVALUE and bPart == "" then
return true
end
return false
elseif bPart == p.SOMEVALUE or bPart == "" then
if bPart == p.SOMEVALUE and aPart == "" then
return false
end
return true
end
if sortPath.desc then
return aPart > bPart
else
return aPart < bPart
end
end
end
end
end
return false
end
end
function State:getHookFunction(param)
if param:len() > 1 then
param = param:sub(1, 1).."0"
end
-- fall back to 1 for getAlias and getBadge
return (State[hookNames[param][self.level]] or State[hookNames[param][1]])
end
function State:newValueHook(param, id)
local hook, idOrParam
local func = self:getHookFunction(param)
if self.level > 1 then
idOrParam = 1
else
idOrParam = id or param
end
hook = function(state, statement)
local datatype
if not state.resultsByStatement[statement] then
state.resultsByStatement[statement] = {}
end
if not state.resultsByStatement[statement][idOrParam] then
state.resultsByStatement[statement][idOrParam] = func(state, statement, idOrParam)
if not state.resultsDatatype then
state.resultsDatatype = copyTable(getmetatable(state.resultsByStatement[statement][1]).datatype)
end
end
return (#state.resultsByStatement[statement][idOrParam] > 0)
end
self.hooksByParam[idOrParam] = hook
return hook
end
function State:prepareSortKey(sortKey)
local desc = false
local sortPath = nil
local param = nil
local id = nil
local newID = nil
if sortKey:match('[+-]$') then
if sortKey:sub(-1) == "-" then
desc = true
end
sortKey = sortKey:sub(1, -2)
end
if sortKey == RANK then
return {{rankTable}, {{}, {"rank"}}, desc=desc}
elseif sortKey:sub(1,1) == '%' then
-- param <= param
sortKey = sortKey:sub(2)
param = sortKey
if param == parameters.property then
sortPath = {{self.resultsByStatement}, {}, {param, key=true}, desc=desc}
else
if param == parameters.qualifier then
param = parameters.qualifier.."1"
elseif not param:match('^'..parameters.qualifier..'%d+$') then
return nil
end
sortPath = {{self.resultsByStatement}, {}, {param, key=true}, {1}, desc=desc}
end
if not self.conf.statesByParam[param] then
return nil
end
else
local baseParam, level, state
local index = 0
if sortKey == PROP then
id = sortKey
baseParam = parameters.property
level = 1
sortPath = {{self.resultsByStatement}, {}, {id, key=true}, desc=desc}
else
sortKey = replaceAlias(sortKey):upper()
id = sortKey
if not isPropertyID(id) then
return nil
end
baseParam = parameters.qualifier.."0"
level = 2
sortPath = {{self.resultsByStatement}, {}, {id, key=true}, {1}, desc=desc}
end
if not self.conf.statesByID[id] then
self.conf.statesByID[id] = {}
end
repeat
index = index + 1
if self.conf.statesByID[id][index] then
-- id <= param
state = self.conf.statesByID[id][index]
param = state.param
else
-- id <= id
param = baseParam
newID = id
state = State:new(self.conf, level, param, newID)
state.freeNumber = true
state.maxResults = 1
self.conf.statesByParam[newID] = state
end
until not state.rawValue and not (state.freeUnit and not state.freeNumber)
if id == PROP and index > 1 then
self.propState = state
self.propState.resultsByStatement = self.resultsByStatement
end
end
return sortPath, param, id, newID
end
function State:newValidationHook(param, id, newID)
local invalid = false
local validated = false
local idOrParam = id or param
local newIdOrParam = newID or param
if not self.hooksByParam[newIdOrParam] then
self:newValueHook(param, newID)
end
local hook = self.hooksByParam[newIdOrParam]
local function validationHook(state, claim)
if invalid then
return false
end
if hook(state.propState or state, claim) and not validated then
local datatype
validated = true
datatype = getmetatable(state.resultsByStatement[claim][newIdOrParam]).datatype[1]
if datatype == UNKNOWN then
invalid = true
return false
end
state.sortable[idOrParam] = true
end
state.resultsByStatement[claim][idOrParam] = state.resultsByStatement[claim][newIdOrParam]
return true
end
self.valHooksByIdOrParam[idOrParam] = validationHook
self.valHooks[#self.valHooks + 1] = validationHook
return validationHook
end
function State:parseFormat(formatStr)
local parsedFormat, hooks = parseFormat(self, formatStr)
-- make sure that at least one required parameter has been defined
if not next(parsedFormat.req) then
throwError("missing-required-parameter")
end
-- make sure that the separator parameter "%s" is not amongst the required parameters
if parsedFormat.req[parameters.separator] then
throwError("extra-required-parameter", "%"..parameters.separator)
end
self.metaTable = {
format = parsedFormat,
datatype = {CLAIM},
__tostring = toString
}
return hooks
end
-- level 1 hook
function State:getProperty(claim)
return self:getValue(claim.mainsnak)
end
-- level 1 hook
function State:getQualifiers(claim, param)
local qualifiers
if claim.qualifiers then qualifiers = claim.qualifiers[self.conf.qualifierIDs[param] or param] end
if qualifiers then
-- iterate through claim's qualifier statements to collect their values
self.conf.statesByParam[param]:iterate(qualifiers) -- pass qualifier state
end
-- return array with multiple value objects (or empty array if there were no results)
return self.conf.statesByParam[param]:getAndResetResults()
end
-- level 2 hook
function State:getQualifier(snak)
return self:getValue(snak)
end
-- level 1 hook
function State:getAllQualifiers(claim, param)
local param0
local array = setmetatable({}, {sep=self.conf.separators["sep%"..param], __tostring=toString})
-- iterate through the results of the separate "qualifier(s)" commands
for i = 1, self.conf.qualifiersCount do
param0 = param..i
-- add the result if there is any, calling the hook in the process if it's not been called yet
if self.hooksByParam[param0](self, claim) then
array[#array + 1] = self.resultsByStatement[claim][param0]
end
end
return array
end
-- level 1 hook
function State:getReferences(claim, param)
if claim.references then
-- iterate through claim's reference statements to collect their values
self.conf.statesByParam[param]:iterate(claim.references) -- pass reference state
end
-- return array with multiple value objects (or empty array if there were no results)
return self.conf.statesByParam[param]:getAndResetResults()
end
-- level 2 hook
function State:getReference(statement)
local key, keyNum, citeWeb, citeQ, label, mt2
local mt = {datatype={REFERENCE}, __tostring=toString, __pairs=npairs}
local value = setmetatable({}, mt)
local params = {}
local paramKeys = {}
local skipKeys = {}
local citeValues = {['web'] = {}, ['q'] = {}}
local citeValueKeys = {['web'] = {}, ['q'] = {}}
local citeMismatch = {}
local useCite = nil
local useValues = {}
local useValueKeys = nil
local str = nil
local version = 2 -- increment this each time the below logic is changed to avoid conflict errors
if not statement.snaks then
return value
end
-- if we've parsed the exact same reference before, then return the cached one
-- (note that this means that multiple occurences of the same value object could end up in the results)
if self.references[statement.hash] then
return self.references[statement.hash]
end
self.references[statement.hash] = value
-- don't include "imported from", which is added by a bot
if statement.snaks[aliasesP.importedFrom] then
statement.snaks[aliasesP.importedFrom] = nil
end
-- don't include "inferred from", which is added by a bot
if statement.snaks[aliasesP.inferredFrom] then
statement.snaks[aliasesP.inferredFrom] = nil
end
-- don't include "type of reference"
if statement.snaks[aliasesP.typeOfReference] then
statement.snaks[aliasesP.typeOfReference] = nil
end
-- don't include "image" to prevent littering
if statement.snaks[aliasesP.image] then
statement.snaks[aliasesP.image] = nil
end
-- don't include "language" if it is equal to the local one
if tostring(self:getReferenceDetail(statement.snaks[aliasesP.language])[1]) == self.conf.langName then
statement.snaks[aliasesP.language] = nil
end
-- retrieve all the other parameters
for i in pairs(statement.snaks) do
-- multiple authors may be given
if i == aliasesP.author then
params[i] = self:getReferenceDetails(statement.snaks[i], false, self.linked, true, " & ") -- link = true/false, anyLang = true
else
params[i] = self:getReferenceDetail(statement.snaks[i], false, (self.linked or (i == aliasesP.statedIn)) and (statement.snaks[i][1].datatype ~= 'url'), true) -- link = true/false, anyLang = true
end
if not params[i][1] then
params[i] = nil
else
paramKeys[#paramKeys + 1] = i
-- add the parameter to each matching type of citation
for j in pairs(citeValues) do
label = ""
-- do so if there was no mismatch with a previous parameter
if not citeMismatch[j] then
if j == 'q' and statement.snaks[i][1].datatype == 'external-id' then
key = 'external-id'
label = tostring(self.conf:getLabel(i))
else
key = i
end
-- check if this parameter is not mismatching itself
if i18n['cite'][j][key] then
key = i18n['cite'][j][key]
-- continue if an option is available in the corresponding cite template
if key ~= "" then
local num = ""
local k = 1
while k <= #params[i] do
keyNum = key..num
citeValues[j][keyNum] = setmetatable({}, {sep={""}, __tostring=toString}) -- "sep" is needed to make this a recognizable array, even though it will not be used
citeValueKeys[j][#citeValueKeys[j] + 1] = keyNum
-- add the external ID's label to the format if we have one
if label ~= "" then
citeValues[j][keyNum][1] = copyValue(params[i][k])
mt2 = getmetatable(citeValues[j][keyNum][1])
mt2.format = mergeArrays({label, " "}, mt2.format or {tostring(citeValues[j][keyNum][1])})
else
citeValues[j][keyNum][1] = params[i][k]
end
k = k + 1
num = k
end
end
else
citeMismatch[j] = true
end
end
end
end
end
-- get title of general template for citing web references
citeWeb = ({split(mw.wikibase.getSitelink(aliasesQ.citeWeb) or "", ":")})[2] -- split off namespace from front
-- get title of template that expands stated-in references into citations
citeQ = ({split(mw.wikibase.getSitelink(aliasesQ.citeQ) or "", ":")})[2] -- split off namespace from front
-- (1) use the general template for citing web references if there is a match and if at least both "reference URL" and "title" are present
if citeWeb and not citeMismatch['web'] and citeValues['web'][i18n['cite']['web'][aliasesP.referenceURL]] and citeValues['web'][i18n['cite']['web'][aliasesP.title]] then
useCite = citeWeb
useValues = citeValues['web']
useValueKeys = citeValueKeys['web']
-- (2) use the template that expands stated-in references into citations if there is a match and if at least "stated in" is present
elseif citeQ and not citeMismatch['q'] and citeValues['q'][i18n['cite']['q'][aliasesP.statedIn]] then
-- we need the raw "stated in" Q-identifier for the this template
citeValues['q'][i18n['cite']['q'][aliasesP.statedIn]][1] = self:getReferenceDetail(statement.snaks[aliasesP.statedIn], true)[1] -- raw = true
useCite = citeQ
useValues = citeValues['q']
useValueKeys = citeValueKeys['q']
end
if useCite then
-- make sure that the parameters are added in the exact same order all the time to avoid conflict errors
table.sort(useValueKeys)
-- if this module is being substituted then build a regular template call, otherwise expand the template
if mw.isSubsting() then
mt.format = {"{{", useCite, params={}, req={}}
-- iterate through the sorted keys
for _, key in ipairs(useValueKeys) do
mt2 = getmetatable(useValues[key][1])
mt2.sub = {["|"] = ENC_PIPE}
value[key] = useValues[key]
mt.format[#mt.format + 1] = "|"
mt.format[#mt.format + 1] = key
mt.format[#mt.format + 1] = "="
mt.format[#mt.format + 1] = key
mt.format.params[#mt.format] = true
mt.format.req[key] = true
end
mt.format[#mt.format + 1] = "}}"
else
for _, key in ipairs(useValueKeys) do
value[key] = useValues[key]
end
mt.expand = useCite
end
-- (3) else, do some default rendering of name-value pairs, but only if at least "stated in", "reference URL" or "title" is present
elseif params[aliasesP.statedIn] or params[aliasesP.referenceURL] or params[aliasesP.title] then
mt.format = {params={}, req={}}
-- start by adding authors up front
if params[aliasesP.author] then
label = tostring(self.conf:getLabel(aliasesP.author))
if label == "" then
label = aliasesP.author
end
value[label] = params[aliasesP.author]
mt.format[1] = label
mt.format.params[1] = true
mt.format.req[label] = true
mt.format[2] = "; "
end
-- then add "reference URL" and "title", combining them into one link if both are present
if params[aliasesP.referenceURL] then
label = tostring(self.conf:getLabel(aliasesP.referenceURL))
if label == "" then
label = aliasesP.referenceURL
end
value[label] = params[aliasesP.referenceURL]
mt.format[#mt.format + 1] = '['
mt.format[#mt.format + 1] = label
mt.format.params[#mt.format] = true
mt.format.req[label] = true
mt.format[#mt.format + 1] = ' '
if not params[aliasesP.title] then
mt.format[#mt.format + 1] = label
mt.format.params[#mt.format] = true
mt.format.req[label] = true
mt.format[#mt.format + 1] = ']'
else
str = ']'
end
end
if params[aliasesP.title] then
label = tostring(self.conf:getLabel(aliasesP.title))
if label == "" then
label = aliasesP.title
end
value[label] = params[aliasesP.title]
mt.format[#mt.format + 1] = '"'
mt.format[#mt.format + 1] = label
mt.format.params[#mt.format] = true
mt.format.req[label] = true
mt.format[#mt.format + 1] = '"'
mt.format[#mt.format + 1] = str
end
-- then add "stated in"
if params[aliasesP.statedIn] then
label = tostring(self.conf:getLabel(aliasesP.statedIn))
if label == "" then
label = aliasesP.statedIn
end
value[label] = params[aliasesP.statedIn]
mt.format[#mt.format + 1] = "; "
mt.format[#mt.format + 1] = "''"
mt.format[#mt.format + 1] = label
mt.format.params[#mt.format] = true
mt.format.req[label] = true
mt.format[#mt.format + 1] = "''"
end
-- mark previously added parameters so that they won't be added a second time
skipKeys[aliasesP.author] = true
skipKeys[aliasesP.referenceURL] = true
skipKeys[aliasesP.title] = true
skipKeys[aliasesP.statedIn] = true
-- make sure that the parameters are added in the exact same order all the time to avoid conflict errors
table.sort(paramKeys)
-- add the rest of the parameters
for _, key in ipairs(paramKeys) do
if not skipKeys[key] then
label = tostring(self.conf:getLabel(key))
if label ~= "" then
value[label] = params[key]
mt.format[#mt.format + 1] = "; "
mt.format[#mt.format + 1] = label
mt.format[#mt.format + 1] = ": "
mt.format[#mt.format + 1] = label
mt.format.params[#mt.format] = true
mt.format.req[label] = true
end
end
end
mt.format[#mt.format + 1] = "."
end
if not next(params) or not next(value) then
return value -- empty value
end
value[1] = params
mt.hash = statement.hash
if not self.rawValue then
local curTime = ""
-- if this module is being substituted then add a timestamp to the hash to avoid future conflict errors,
-- which could occur when labels on Wikidata have been changed in the meantime while the substitution remains static
if mw.isSubsting() then
curTime = "-" .. self.conf.curTime
end
-- this should become a tag, so save the reference's hash for later
mt.tag = {"ref", {name = "wikidata-" .. statement.hash .. curTime .. "-v" .. (tonumber(i18n['cite']['version']) + version)}}
end
return value
end
-- gets a detail of one particular type for a reference
function State:getReferenceDetail(snaks, raw, link, anyLang)
local value
local switchLang = anyLang or false
local array = setmetatable({}, {sep={""}, __tostring=toString}) -- "sep" is needed to make this a recognizable array, even though it will not be used
if not snaks then
return array
end
-- if anyLang, first try the local language and otherwise any language
repeat
for _, snak in ipairs(snaks) do
value = self.conf:getValue(snak, raw, link, false, anyLang and not switchLang, false, false, true) -- noSpecial = true
if value[1] then
array[1] = value
return array
end
end
if not anyLang then
break
end
switchLang = not switchLang
until anyLang and switchLang
return array
end
-- gets the details of one particular type for a reference
function State:getReferenceDetails(snaks, raw, link, anyLang, sep)
local value
local array = setmetatable({}, {sep={sep or ""}, __tostring=toString})
if not snaks then
return array
end
for _, snak in ipairs(snaks) do
value = self.conf:getValue(snak, raw, link, false, anyLang, false, false, true) -- noSpecial = true
if value[1] then
array[#array + 1] = value
end
end
return array
end
-- level 1 hook
function State:getAlias(object)
local alias = object.value
local title = nil
if alias and self.linked then
if self.conf.entityID:sub(1,1) == "Q" then
title = mw.wikibase.getSitelink(self.conf.entityID)
elseif self.conf.entityID:sub(1,1) == "P" then
title = "d:Property:" .. self.conf.entityID
end
if title then
return ({buildWikilink(title, alias)})[1]
end
end
return setmetatable({alias}, {__tostring=toString})
end
-- level 1 hook
function State:getBadge(value)
return ({self.conf:getLabel(value, self.rawValue, self.linked, self.shortName)})[1]
end
-- level 1 hook
function State:getSeparator()
return self.conf.movSeparator
end
function State:addToResults(statement)
self.results[#self.results + 1] = self.resultsByStatement[statement][1]
if #self.results == self.maxResults then
return nil
end
return true
end
function State:getAndResetResults()
local results = setmetatable(self.results, {sep=self.separator, datatype=self.resultsDatatype, __tostring=toString})
-- reset results before iterating over next dataset
self.results = {}
self.resultsByStatement = {}
self.resultsDatatype = nil
if self.level == 1 and results[1] and results[#results][parameters.separator] then
results[#results][parameters.separator] = self.conf.puncMark
end
return results
end
-- this function may return nil, in which case the iterate function will break its loop
function State:callHooks(hooks, statement)
local lastResult = nil
local i = 1
-- loop through the hooks in order and stop if one gives a negative result
while hooks[i] do
lastResult = hooks[i](self, statement)
-- check if false or nil
if not lastResult then
return lastResult
end
i = i + 1
end
return lastResult
end
--cycle:
-- iterate(statements, hooks):
-- for statement in statements:
-- valueHook: state.resultsByStatement[statement][param or 1] = func(state, statement, param)
-- func: {if lvl 2 hook}{cycle}
-- {if param}{persistHook: state.resultsByStatement[statement][1][param] = state.resultsByStatement[statement][param]}
-- addToResults(statement): state.results[#state.results + 1] = state.resultsByStatement[statement][1]
-- :rof
-- getAndResetResults: return state.results {finally}{
-- state.results = {}
-- state.resultsByStatement = {}
-- }
--:elcyc
function State:iterate(statements, hooks)
hooks = hooks or self.hooks
for _, statement in ipairs(statements) do
-- call hooks and break if the returned result is nil, which typically happens
-- when addToResults found that we collected the maximum number of results
if (self:callHooks(hooks, statement) == nil) then
break
end
end
end
function State:iterateHooks(claims, hooks)
local i = 1
hooks = hooks or self.hooks
while hooks[i] do
local retry = false
for _, claim in ipairs(claims) do
local result = hooks[i](self, claim)
if not result then
if result == nil then
retry = true
end
break
end
end
if not retry then
i = i + 1
end
end
end
--==-- Public functions --==--
local function claimCommand(args, funcName)
local lastArg, hooks, claims, sortKey, sortKeys
local sortHooks = {}
local value = setmetatable({}, {__tostring=toString})
local cfg = Config:new()
cfg:processCommand(funcName) -- process first command (== function name)
-- set the date if given;
-- must come BEFORE processing the flags
if args[p.args.date] then
cfg.atDate = {parseDate(args[p.args.date])}
cfg.periods = {false, true, false} -- change default time constraint to 'current'
end
-- process flags and commands
repeat
lastArg = nextArg(args)
until not cfg:processCommandOrFlag(lastArg)
cfg.filterBeforeRank = cfg.filterBeforeRank or not (cfg.periods[1] and cfg.periods[2] and cfg.periods[3])
-- get the entity ID from either the positional argument, the eid argument or the page argument
cfg.entityID, cfg.propertyID = getEntityId(lastArg, args[p.args.eid], args[p.args.page])
if cfg.entityID == "" then
return value -- empty; we cannot continue without a valid entity ID
end
if not cfg.propertyID then
cfg.propertyID = nextArg(args)
end
cfg.propertyID = replaceAlias(cfg.propertyID)
if not cfg.propertyID then
return value -- empty; we cannot continue without a property ID
end
cfg.propertyID = cfg.propertyID:upper()
if cfg.statesByParam[parameters.qualifier.."1"] then
-- do further processing if a "qualifier(s)" command was given
if #args - args.pointer + 1 > cfg.qualifiersCount then
-- claim ID or literal value has been given
cfg.propertyValue = nextArg(args)
cfg.filterBeforeRank = true
end
-- for each given qualifier ID, check if it is an alias and add it
for i = 1, cfg.qualifiersCount do
local param
local qualifierID = nextArg(args)
if not qualifierID then
break
end
param = parameters.qualifier..i
qualifierID = replaceAlias(qualifierID):upper()
cfg.qualifierIDs[param] = qualifierID
cfg:addToStatesByID(cfg.statesByParam[param], qualifierID)
end
elseif cfg.statesByParam[parameters.reference] then
-- do further processing if "reference(s)" command was given
cfg.propertyValue = nextArg(args)
cfg.filterBeforeRank = true
end
-- process qualifier matching values, analogous to cfg.propertyValue
for i, v in npairs(args) do
local id = replaceAlias(i):upper()
if isPropertyID(id) then
cfg.qualifierIDsAndValues[id] = v
cfg.filterBeforeRank = true
end
end
-- potential optimization if only 'preferred' ranked claims are desired,
-- or if the 'best' flag was given while no other filter flags were given
if not (cfg.ranks[2] or cfg.ranks[3]) or (cfg.bestRank and not cfg.filterBeforeRank) then
-- returns either only 'preferred' ranked claims or only 'normal' ranked claims
claims = mw.wikibase.getBestStatements(cfg.entityID, cfg.propertyID)
if #claims == 0 then
-- no claims with rank 'preferred' or 'normal' found,
-- property might only contain claims with rank 'deprecated'
if not cfg.ranks[3] then
return value -- empty; we don't want 'deprecated' claims, so we're done
end
claims = nil -- get all statements instead
elseif not cfg.ranks[rankTable[claims[1].rank][1]] then
-- the best ranked claims don't have the desired rank
-- if the best ranked claims have rank 'normal' which isn't desired,
-- then the property might only contain claims with rank 'deprecated'
if claims[1].rank == "normal" and not cfg.ranks[3] then
return value -- empty; we don't want 'deprecated' claims, so we're done
end
claims = nil -- get all statements instead
end
end
if not claims then
claims = mw.wikibase.getAllStatements(cfg.entityID, cfg.propertyID)
end
if #claims == 0 then
return value -- empty; there is no use to continue without any claims
end
-- create a state for "properties" if it doesn't exist yet, which will be used as a base configuration for each claim iteration
if not cfg.statesByParam[parameters.property] then
cfg.curState = State:new(cfg, 1, parameters.property, PROP)
-- decrease potential overhead (in case this state will be used for sorting/matching)
cfg.curState.freeNumber = true
-- if the "single" flag has been given then this state should be equivalent to "property" (singular)
if cfg.singleClaim then
cfg.curState.maxResults = 1
end
else
cfg.curState = cfg.statesByParam[parameters.property]
cfg:addToStatesByID(cfg.curState, PROP)
end
-- parse the desired format, or choose an appropriate format
if args["format"] then
hooks = cfg.curState:parseFormat(args["format"])
elseif cfg.statesByParam[parameters.qualifier.."1"] then -- "qualifier(s)" command given
if cfg.statesByParam[parameters.property] then -- "propert(y|ies)" command given
hooks = cfg.curState:parseFormat(formats.propertyWithQualifier)
else
hooks = cfg.curState:parseFormat(formats.qualifier)
end
elseif cfg.statesByParam[parameters.property] then -- "propert(y|ies)" command given
hooks = cfg.curState:parseFormat(formats.property)
else -- "reference(s)" command given
hooks = cfg.curState:parseFormat(formats.reference)
end
hooks[#hooks + 1] = State.addToResults
-- if a "qualifier(s)" command and no "propert(y|ies)" command has been given, make the movable separator a semicolon
if cfg.statesByParam[parameters.qualifier.."1"] and not cfg.statesByParam[parameters.property] then
cfg.separators["sep%s"][1] = ";"
end
-- if only "reference(s)" has been given, set the default separator to none (except when raw)
if cfg.statesByParam[parameters.reference] and not cfg.statesByParam[parameters.property] and not cfg.statesByParam[parameters.qualifier.."1"]
and not cfg.statesByParam[parameters.reference].rawValue then
cfg.separators["sep"][1] = ""
end
-- if exactly one "qualifier(s)" command has been given, make "sep%q" point to "sep%q1" to make them equivalent
if cfg.qualifiersCount == 1 then
cfg.separators["sep%q"] = cfg.separators["sep%q1"]
end
-- process overridden separator values;
-- must come AFTER tweaking the default separators
cfg:processSeparators(args)
-- if the "sourced" flag has been given then create a state for "reference" if it doesn't exist yet, using default values,
-- which must exist in order to be able to determine if a claim has any references;
-- must come AFTER processing the commands and parsing the format
if cfg.sourcedOnly and not cfg.curState.hooksByParam[parameters.reference] then
if not cfg.statesByParam[parameters.reference] then
local refState = State:new(cfg, 2, parameters.reference)
refState.maxResults = 1 -- decrease overhead
end
cfg.curState:newValueHook(parameters.reference)
end
table.insert(hooks, 1, State.claimMatches)
-- if the best ranked claims are desired, we'll sort by rank first
if cfg.bestRank then
cfg.curState.sortPaths[1] = cfg.curState:prepareSortKey(RANK)
end
if args[p.args.sort] then
sortKeys = args[p.args.sort]
else
sortKeys = RANK -- by default, sort by rank
end
repeat
local sortPath, param, id, newID
sortKey, sortKeys = split(sortKeys, ",")
sortKey = mw.text.trim(sortKey)
-- additional sorting by rank is pointless if only the best rank is desired
if not (cfg.bestRank and sortKey:match('^'..RANK..'[+-]?$')) then
sortPath, param, id, newID = cfg.curState:prepareSortKey(sortKey)
if sortPath then
cfg.curState.sortPaths[#cfg.curState.sortPaths + 1] = sortPath
if param and not cfg.curState.valHooksByIdOrParam[id or param] then
sortHooks[#sortHooks + 1] = newOptionalHook{cfg.curState:newValidationHook(param, id, newID)}
end
end
end
until not sortKeys
cfg.curState:iterate(claims, sortHooks)
table.sort(claims, cfg.curState:newSortFunction())
-- then iterate through the claims to collect values
cfg.curState:iterate(claims, hooks) -- pass property state with level 1 hooks
value = cfg.curState:getAndResetResults()
-- if desired, add a clickable icon that may be used to edit the returned values on Wikidata
if cfg.editable and value[1] then
local mt = getmetatable(value)
mt.trail = cfg:getEditIcon()
end
return value
end
local function generalCommand(args, funcName)
local lastArg
local value = setmetatable({}, {__tostring=toString})
local cfg = Config:new()
-- process command (== function name); if false, then it's not "alias(es)" or "badge(s)"
if not cfg:processCommand(funcName, true) then
cfg.curState = State:new(cfg)
end
repeat
lastArg = nextArg(args)
until not cfg:processFlag(lastArg)
-- get the entity ID from either the positional argument, the eid argument or the page argument
cfg.entityID = getEntityId(lastArg, args[p.args.eid], args[p.args.page], true)
if cfg.entityID == "" or not mw.wikibase.entityExists(cfg.entityID) then
return value -- empty; we cannot continue without an entity
end
-- serve according to the given command
if funcName == p.generalCommands.label then
value = cfg:getLabel(cfg.entityID, cfg.curState.rawValue, cfg.curState.linked, cfg.curState.shortName)
elseif funcName == p.generalCommands.title then
cfg.inSitelinks = true
if cfg.entityID:sub(1,1) == "Q" then
value[1] = mw.wikibase.getSitelink(cfg.entityID)
end
if cfg.curState.linked and value[1] then
value = buildWikilink(value[1])
end
elseif funcName == p.generalCommands.description then
value[1] = mw.wikibase.getDescription(cfg.entityID)
else
local values
cfg.entity = mw.wikibase.getEntity(cfg.entityID)
if funcName == p.generalCommands.alias or funcName == p.generalCommands.aliases then
if not cfg.entity.aliases or not cfg.entity.aliases[cfg.langCode] then
return value -- empty; there is no use to continue without any aliasses
end
values = cfg.entity.aliases[cfg.langCode]
elseif funcName == p.generalCommands.badge or funcName == p.generalCommands.badges then
if not cfg.entity.sitelinks or not cfg.entity.sitelinks[cfg.siteID] or not cfg.entity.sitelinks[cfg.siteID].badges then
return value -- empty; there is no use to continue without any badges
end
cfg.inSitelinks = true
values = cfg.entity.sitelinks[cfg.siteID].badges
end
cfg.separators["sep"][1] = ", "
-- process overridden separator values;
-- must come AFTER tweaking the default separator
cfg:processSeparators(args)
-- iterate to collect values
cfg.curState:iterate(values)
value = cfg.curState:getAndResetResults()
end
-- if desired, add a clickable icon that may be used to edit the returned values on Wikidata
if cfg.editable and value[1] then
local mt = getmetatable(value)
mt.trail = cfg:getEditIcon()
end
return value
end
-- modules that include this module may call the functions with an underscore prepended, e.g.: p._property(args)
local function establishCommands(commandList, commandFunc)
for _, commandName in pairs(commandList) do
local function stringWrapper(frameOrArgs)
local frame, args
-- check if Wikidata is available to prevent errors
if not mw.wikibase then
return ""
end
-- assumption: a frame always has an args table
if frameOrArgs.args then
-- called by wikitext
frame = frameOrArgs
args = copyTable(frame.args)
else
-- called by module
args = frameOrArgs
end
args.pointer = 1
loadI18n(aliasesP, frame)
return tostring(commandFunc(args, commandName))
end
p[commandName] = stringWrapper
local function tableWrapper(args)
-- check if Wikidata is available to prevent errors
if not mw.wikibase then
return nil
end
args = copyTable(args)
args.pointer = 1
loadI18n(aliasesP)
return commandFunc(args, commandName)
end
p["_" .. commandName] = tableWrapper
end
end
establishCommands(p.claimCommands, claimCommand)
establishCommands(p.generalCommands, generalCommand)
-- main function that is supposed to be used by wrapper templates
function p.main(frame)
local f, args
loadI18n(aliasesP, frame)
-- get the parent frame to take the arguments that were passed to the wrapper template
frame = frame:getParent() or frame
if not frame.args[1] then
throwError("no-function-specified")
end
f = mw.text.trim(frame.args[1])
if f == "main" then
throwError("main-called-twice")
end
assert(p[f], errorText('no-such-function', f))
-- copy arguments from immutable to mutable table
args = copyTable(frame.args)
-- remove the function name from the list
table.remove(args, 1)
return p[f](args)
end
return p