Open main menu
Home
Random
Donate
Recent changes
Special pages
Community portal
Preferences
About Stockhub
Disclaimers
Search
User menu
Talk
Contributions
Create account
Log in
Editing
Module:Sandbox/Thayts/Wd
Warning:
You are not logged in. Your IP address will be publicly visible if you make any edits. If you
log in
or
create an account
, your edits will be attributed to your username, along with other benefits.
Anti-spam check. Do
not
fill this in!
-- 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[ <span style=\"font-size:85\\%\">(%q)</span>][%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 = {"[[", target, "|", 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 = '<span style="float:' if self.langObj:isRTL() then front = front .. 'left' else front = front .. 'right' end front = front .. '">' back = '</span>' 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 = {'<strong class="error">', value[1], '</strong>'} 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 = {'<strong class="error">', value[1], '</strong>'} 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 = {'<strong class="error">', value[1], '</strong>'} 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 <ref> 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
Summary:
Please note that all contributions to Stockhub may be edited, altered, or removed by other contributors. If you do not want your writing to be edited mercilessly, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource (see
Stockhub:Copyrights
for details).
Do not submit copyrighted work without permission!
Cancel
Editing help
(opens in new window)
Stockhub entities used in this page
Human Development Index
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
population
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
demonym
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
defining formula
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
VAT-rate
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
mains voltage
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
head of state
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
currency
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
geoshape
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
licence plate code
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
language of work or name
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
flag image
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
applies to part
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
start time
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
end time
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
point in time
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
coordinate location
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
in defining formula
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
retrieved
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
reference URL
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
symbol represents
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
Human Development Report
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
pharmaceutical drug
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
Q125649
: Miscellaneous (e.g. aliases, entity existence)
Q150747
: Miscellaneous (e.g. aliases, entity existence)
Q154287
: Miscellaneous (e.g. aliases, entity existence)
Q154946
: Miscellaneous (e.g. aliases, entity existence)
Q154952
: Miscellaneous (e.g. aliases, entity existence)
Q167086
: Miscellaneous (e.g. aliases, entity existence)
Q184872
: Miscellaneous (e.g. aliases, entity existence)
Earth
: Label: en, Label: en-gb, Miscellaneous (e.g. aliases, entity existence), Sitelink, Title
William I of the Netherlands
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
food
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
Q2118709
: Miscellaneous (e.g. aliases, entity existence)
Q21540096
: Miscellaneous (e.g. aliases, entity existence)
Q22321052
: Title
Q25250
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
Q27561
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
Q27686
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
water
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
Q29574
: Miscellaneous (e.g. aliases, entity existence)
sport
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
Q36846
: Miscellaneous (e.g. aliases, entity existence)
Q41298
: Miscellaneous (e.g. aliases, entity existence)
Antique
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
euro
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb, Title, Statement: P1813
United States dollar
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb, Title, Statement: P1813
Netherlands
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb, Statement: P395, Statement: P1082, Statement: P1081, Statement: P2855, Statement: P35, Statement: P38, Statement: P1549, Statement: P2884, Statement: P625, Statement: P41, Title, Description: en-gb
Q55187
: Miscellaneous (e.g. aliases, entity existence)
Template:Cite web
: Title
book
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
country
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb, Statement: P3896
Q688498
: Miscellaneous (e.g. aliases, entity existence)
art
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb
Utrecht
: Miscellaneous (e.g. aliases, entity existence), Label: en-gb, Title
Q788472
: Miscellaneous (e.g. aliases, entity existence)
Q832778
: Miscellaneous (e.g. aliases, entity existence)
Q915684
: Miscellaneous (e.g. aliases, entity existence), Statement: P2534, Statement: P7235
Templates used on this page:
Template:!((
(
edit
)
Template:0
(
edit
)
Template:Background color
(
edit
)
Template:Color
(
edit
)
Template:Dfn
(
edit
)
Template:Efn
(
edit
)
Template:Hash
(
edit
)
Template:Hatnote
(
edit
)
Template:Main other
(
edit
)
Template:Module other
(
edit
)
Template:Notelist
(
edit
)
Template:Nowrap
(
edit
)
Template:Reflist
(
edit
)
Template:Reflist/styles.css
(
edit
)
Template:Smallcaps
(
edit
)
Template:T
(
edit
)
Template:Tl
(
edit
)
Template:Wpl
(
edit
)
Module:Check for unknown parameters
(
edit
)
Module:Sandbox/Thayts/Wd
(
edit
)
Module:Sandbox/Thayts/Wd/doc
(
edit
)
Module:Sandbox/Thayts/Wd/i18n
(
edit
)