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