Line 1: |
Line 1: |
| + | -- vim: set noexpandtab ft=lua ts=4 sw=4: |
| + | require('Module:No globals') |
| | | |
| + | local p = {} |
| + | local debug = false |
| + | |
| + | |
| + | ------------------------------------------------------------------------------ |
| + | -- module local variables and functions |
| + | |
| + | local wiki = |
| + | { |
| + | langcode = mw.language.getContentLanguage().code |
| + | } |
| + | |
| + | -- internationalisation |
| + | local i18n = |
| + | { |
| + | ["errors"] = |
| + | { |
| + | ["property-not-found"] = "Property not found.", |
| + | ["entity-not-found"] = "Wikidata entity not found.", |
| + | ["unknown-claim-type"] = "Unknown claim type.", |
| + | ["unknown-entity-type"] = "Unknown entity type.", |
| + | ["qualifier-not-found"] = "Qualifier not found.", |
| + | ["site-not-found"] = "Wikimedia project not found.", |
| + | ["unknown-datetime-format"] = "Unknown datetime format.", |
| + | ["local-article-not-found"] = "Article is not yet available in this wiki." |
| + | }, |
| + | ["datetime"] = |
| + | { |
| + | -- $1 is a placeholder for the actual number |
| + | [0] = "$1 billion years", -- precision: billion years |
| + | [1] = "$100 million years", -- precision: hundred million years |
| + | [2] = "$10 million years", -- precision: ten million years |
| + | [3] = "$1 million years", -- precision: million years |
| + | [4] = "$100,000 years", -- precision: hundred thousand years |
| + | [5] = "$10,000 years", -- precision: ten thousand years |
| + | [6] = "$1 millennium", -- precision: millennium |
| + | [7] = "$1 century", -- precision: century |
| + | [8] = "$1s", -- precision: decade |
| + | -- the following use the format of #time parser function |
| + | [9] = "Y", -- precision: year, |
| + | [10] = "F Y", -- precision: month |
| + | [11] = "F j, Y", -- precision: day |
| + | [12] = "F j, Y ga", -- precision: hour |
| + | [13] = "F j, Y g:ia", -- precision: minute |
| + | [14] = "F j, Y g:i:sa", -- precision: second |
| + | ["beforenow"] = "$1 BCE", -- how to format negative numbers for precisions 0 to 5 |
| + | ["afternow"] = "$1 CE", -- how to format positive numbers for precisions 0 to 5 |
| + | ["bc"] = '$1 "BCE"', -- how print negative years |
| + | ["ad"] = "$1", -- how print positive years |
| + | -- the following are for function getDateValue() and getQualifierDateValue() |
| + | ["default-format"] = "dmy", -- default value of the #3 (getDateValue) or |
| + | -- #4 (getQualifierDateValue) argument |
| + | ["default-addon"] = "BC", -- default value of the #4 (getDateValue) or |
| + | -- #5 (getQualifierDateValue) argument |
| + | ["prefix-addon"] = false, -- set to true for languages put "BC" in front of the |
| + | -- datetime string; or the addon will be suffixed |
| + | ["addon-sep"] = " ", -- separator between datetime string and addon (or inverse) |
| + | ["format"] = -- options of the 3rd argument |
| + | { |
| + | ["mdy"] = "F j, Y", |
| + | ["my"] = "F Y", |
| + | ["y"] = "Y", |
| + | ["dmy"] = "j F Y", |
| + | ["ymd"] = "Y-m-d", |
| + | ["ym"] = "Y-m" |
| + | } |
| + | }, |
| + | ["monolingualtext"] = '<span lang="%language">%text</span>', |
| + | ["warnDump"] = "[[Category:Called function 'Dump' from module Wikidata]]", |
| + | ["ordinal"] = |
| + | { |
| + | [1] = "st", |
| + | [2] = "nd", |
| + | [3] = "rd", |
| + | ["default"] = "th" |
| + | } |
| + | } |
| + | |
| + | if wiki.langcode ~= "en" then |
| + | --require("Module:i18n").loadI18n("Module:Wikidata/i18n", i18n) |
| + | -- got idea from [[:w:Module:Wd]] |
| + | local module_title; if ... == nil then |
| + | module_title = mw.getCurrentFrame():getTitle() |
| + | else |
| + | module_title = ... |
| + | end |
| + | require('Module:i18n').loadI18n(module_title..'/i18n', i18n) |
| + | end |
| + | |
| + | -- this function needs to be internationalised along with the above: |
| + | -- takes cardinal numer as a numeric and returns the ordinal as a string |
| + | -- we need three exceptions in English for 1st, 2nd, 3rd, 21st, .. 31st, etc. |
| + | local function makeOrdinal (cardinal) |
| + | local ordsuffix = i18n.ordinal.default |
| + | if cardinal % 10 == 1 then |
| + | ordsuffix = i18n.ordinal[1] |
| + | elseif cardinal % 10 == 2 then |
| + | ordsuffix = i18n.ordinal[2] |
| + | elseif cardinal % 10 == 3 then |
| + | ordsuffix = i18n.ordinal[3] |
| + | end |
| + | -- In English, 1, 21, 31, etc. use 'st', but 11, 111, etc. use 'th' |
| + | -- similarly for 12 and 13, etc. |
| + | if (cardinal % 100 == 11) or (cardinal % 100 == 12) or (cardinal % 100 == 13) then |
| + | ordsuffix = i18n.ordinal.default |
| + | end |
| + | return tostring(cardinal) .. ordsuffix |
| + | end |
| + | |
| + | local function printError(code) |
| + | return '<span class="error">' .. (i18n.errors[code] or code) .. '</span>' |
| + | end |
| + | local function parseDateFormat(f, timestamp, addon, prefix_addon, addon_sep) |
| + | local year_suffix |
| + | local tstr = "" |
| + | local lang_obj = mw.language.new(wiki.langcode) |
| + | local f_parts = mw.text.split(f, 'Y', true) |
| + | for idx, f_part in pairs(f_parts) do |
| + | year_suffix = '' |
| + | if string.match(f_part, "x[mijkot]$") then |
| + | -- for non-Gregorian year |
| + | f_part = f_part .. 'Y' |
| + | elseif idx < #f_parts then |
| + | -- supress leading zeros in year |
| + | year_suffix = lang_obj:formatDate('Y', timestamp) |
| + | year_suffix = string.gsub(year_suffix, '^0+', '', 1) |
| + | end |
| + | tstr = tstr .. lang_obj:formatDate(f_part, timestamp) .. year_suffix |
| + | end |
| + | if addon ~= "" and prefix_addon then |
| + | return addon .. addon_sep .. tstr |
| + | elseif addon ~= "" then |
| + | return tstr .. addon_sep .. addon |
| + | else |
| + | return tstr |
| + | end |
| + | end |
| + | local function parseDateValue(timestamp, date_format, date_addon) |
| + | local prefix_addon = i18n["datetime"]["prefix-addon"] |
| + | local addon_sep = i18n["datetime"]["addon-sep"] |
| + | local addon = "" |
| + | |
| + | -- check for negative date |
| + | if string.sub(timestamp, 1, 1) == '-' then |
| + | timestamp = '+' .. string.sub(timestamp, 2) |
| + | addon = date_addon |
| + | end |
| + | local _date_format = i18n["datetime"]["format"][date_format] |
| + | if _date_format ~= nil then |
| + | return parseDateFormat(_date_format, timestamp, addon, prefix_addon, addon_sep) |
| + | else |
| + | return printError("unknown-datetime-format") |
| + | end |
| + | end |
| + | |
| + | -- This local function combines the year/month/day/BC/BCE handling of parseDateValue{} |
| + | -- with the millennium/century/decade handling of formatDate() |
| + | local function parseDateFull(timestamp, precision, date_format, date_addon) |
| + | local prefix_addon = i18n["datetime"]["prefix-addon"] |
| + | local addon_sep = i18n["datetime"]["addon-sep"] |
| + | local addon = "" |
| + | |
| + | -- check for negative date |
| + | if string.sub(timestamp, 1, 1) == '-' then |
| + | timestamp = '+' .. string.sub(timestamp, 2) |
| + | addon = date_addon |
| + | end |
| + | |
| + | -- get the next four characters after the + (should be the year now in all cases) |
| + | -- ok, so this is dirty, but let's get it working first |
| + | local intyear = tonumber(string.sub(timestamp, 2, 5)) |
| + | if intyear == 0 and precision <= 9 then |
| + | return "" |
| + | end |
| + | |
| + | -- precision is 10000 years or more |
| + | if precision <= 5 then |
| + | local factor = 10 ^ ((5 - precision) + 4) |
| + | local y2 = math.ceil(math.abs(intyear) / factor) |
| + | local relative = mw.ustring.gsub(i18n.datetime[precision], "$1", tostring(y2)) |
| + | if addon ~= "" then |
| + | -- negative date |
| + | relative = mw.ustring.gsub(i18n.datetime.beforenow, "$1", relative) |
| + | else |
| + | relative = mw.ustring.gsub(i18n.datetime.afternow, "$1", relative) |
| + | end |
| + | return relative |
| + | end |
| + | |
| + | -- precision is decades (8), centuries (7) and millennia (6) |
| + | local era, card |
| + | if precision == 6 then |
| + | card = math.floor((intyear - 1) / 1000) + 1 |
| + | era = mw.ustring.gsub(i18n.datetime[6], "$1", makeOrdinal(card)) |
| + | end |
| + | if precision == 7 then |
| + | card = math.floor((intyear - 1) / 100) + 1 |
| + | era = mw.ustring.gsub(i18n.datetime[7], "$1", makeOrdinal(card)) |
| + | end |
| + | if precision == 8 then |
| + | era = mw.ustring.gsub(i18n.datetime[8], "$1", tostring(math.floor(math.abs(intyear) / 10) * 10)) |
| + | end |
| + | if era then |
| + | if addon ~= "" then |
| + | era = mw.ustring.gsub(mw.ustring.gsub(i18n.datetime.bc, '"', ""), "$1", era) |
| + | else |
| + | era = mw.ustring.gsub(mw.ustring.gsub(i18n.datetime.ad, '"', ""), "$1", era) |
| + | end |
| + | return era |
| + | end |
| + | |
| + | local _date_format = i18n["datetime"]["format"][date_format] |
| + | if _date_format ~= nil then |
| + | -- check for precision is year and override supplied date_format |
| + | if precision == 9 then |
| + | _date_format = i18n["datetime"][9] |
| + | end |
| + | return parseDateFormat(_date_format, timestamp, addon, prefix_addon, addon_sep) |
| + | else |
| + | return printError("unknown-datetime-format") |
| + | end |
| + | end |
| + | |
| + | -- the "qualifiers" and "snaks" field have a respective "qualifiers-order" and "snaks-order" field |
| + | -- use these as the second parameter and this function instead of the built-in "pairs" function |
| + | -- to iterate over all qualifiers and snaks in the intended order. |
| + | local function orderedpairs(array, order) |
| + | if not order then return pairs(array) end |
| + | |
| + | -- return iterator function |
| + | local i = 0 |
| + | return function() |
| + | i = i + 1 |
| + | if order[i] then |
| + | return order[i], array[order[i]] |
| + | end |
| + | end |
| + | end |
| + | |
| + | -- precision: 0 - billion years, 1 - hundred million years, ..., 6 - millennia, 7 - century, 8 - decade, 9 - year, 10 - month, 11 - day, 12 - hour, 13 - minute, 14 - second |
| + | local function normalizeDate(date) |
| + | date = mw.text.trim(date, "+") |
| + | -- extract year |
| + | local yearstr = mw.ustring.match(date, "^\-?%d+") |
| + | local year = tonumber(yearstr) |
| + | -- remove leading zeros of year |
| + | return year .. mw.ustring.sub(date, #yearstr + 1), year |
| + | end |
| + | |
| + | local function formatDate(date, precision, timezone) |
| + | precision = precision or 11 |
| + | local date, year = normalizeDate(date) |
| + | if year == 0 and precision <= 9 then return "" end |
| + | |
| + | -- precision is 10000 years or more |
| + | if precision <= 5 then |
| + | local factor = 10 ^ ((5 - precision) + 4) |
| + | local y2 = math.ceil(math.abs(year) / factor) |
| + | local relative = mw.ustring.gsub(i18n.datetime[precision], "$1", tostring(y2)) |
| + | if year < 0 then |
| + | relative = mw.ustring.gsub(i18n.datetime.beforenow, "$1", relative) |
| + | else |
| + | relative = mw.ustring.gsub(i18n.datetime.afternow, "$1", relative) |
| + | end |
| + | return relative |
| + | end |
| + | |
| + | -- precision is decades, centuries and millennia |
| + | local era |
| + | if precision == 6 then era = mw.ustring.gsub(i18n.datetime[6], "$1", tostring(math.floor((math.abs(year) - 1) / 1000) + 1)) end |
| + | if precision == 7 then era = mw.ustring.gsub(i18n.datetime[7], "$1", tostring(math.floor((math.abs(year) - 1) / 100) + 1)) end |
| + | if precision == 8 then era = mw.ustring.gsub(i18n.datetime[8], "$1", tostring(math.floor(math.abs(year) / 10) * 10)) end |
| + | if era then |
| + | if year < 0 then era = mw.ustring.gsub(mw.ustring.gsub(i18n.datetime.bc, '"', ""), "$1", era) |
| + | elseif year > 0 then era = mw.ustring.gsub(mw.ustring.gsub(i18n.datetime.ad, '"', ""), "$1", era) end |
| + | return era |
| + | end |
| + | |
| + | -- precision is year |
| + | if precision == 9 then |
| + | return year |
| + | end |
| + | |
| + | -- precision is less than years |
| + | if precision > 9 then |
| + | --[[ the following code replaces the UTC suffix with the given negated timezone to convert the global time to the given local time |
| + | timezone = tonumber(timezone) |
| + | if timezone and timezone ~= 0 then |
| + | timezone = -timezone |
| + | timezone = string.format("%.2d%.2d", timezone / 60, timezone % 60) |
| + | if timezone[1] ~= '-' then timezone = "+" .. timezone end |
| + | date = mw.text.trim(date, "Z") .. " " .. timezone |
| + | end |
| + | ]]-- |
| + | |
| + | local formatstr = i18n.datetime[precision] |
| + | if year == 0 then formatstr = mw.ustring.gsub(formatstr, i18n.datetime[9], "") |
| + | elseif year < 0 then |
| + | -- Mediawiki formatDate doesn't support negative years |
| + | date = mw.ustring.sub(date, 2) |
| + | formatstr = mw.ustring.gsub(formatstr, i18n.datetime[9], mw.ustring.gsub(i18n.datetime.bc, "$1", i18n.datetime[9])) |
| + | elseif year > 0 and i18n.datetime.ad ~= "$1" then |
| + | formatstr = mw.ustring.gsub(formatstr, i18n.datetime[9], mw.ustring.gsub(i18n.datetime.ad, "$1", i18n.datetime[9])) |
| + | end |
| + | return mw.language.new(wiki.langcode):formatDate(formatstr, date) |
| + | end |
| + | end |
| + | |
| + | local function printDatavalueEntity(data, parameter) |
| + | -- data fields: entity-type [string], numeric-id [int, Wikidata id] |
| + | local id |
| + | |
| + | if data["entity-type"] == "item" then id = "Q" .. data["numeric-id"] |
| + | elseif data["entity-type"] == "property" then id = "P" .. data["numeric-id"] |
| + | else return printError("unknown-entity-type") |
| + | end |
| + | |
| + | if parameter then |
| + | if parameter == "link" then |
| + | local linkTarget = mw.wikibase.getSitelink(id) |
| + | local linkName = mw.wikibase.getLabel(id) |
| + | if linkTarget then |
| + | -- if there is a local Wikipedia article link to it using the label or the article title |
| + | return "[[" .. linkTarget .. "|" .. (linkName or linkTarget) .. "]]" |
| + | else |
| + | -- if there is no local Wikipedia article output the label or link to the Wikidata object to let the user input a proper label |
| + | if linkName then return linkName else return "[[:d:" .. id .. "|" .. id .. "]]" end |
| + | end |
| + | else |
| + | return data[parameter] |
| + | end |
| + | else |
| + | return mw.wikibase.getLabel(id) or id |
| + | end |
| + | end |
| + | |
| + | local function printDatavalueTime(data, parameter) |
| + | -- data fields: time [ISO 8601 time], timezone [int in minutes], before [int], after [int], precision [int], calendarmodel [wikidata URI] |
| + | -- precision: 0 - billion years, 1 - hundred million years, ..., 6 - millennia, 7 - century, 8 - decade, 9 - year, 10 - month, 11 - day, 12 - hour, 13 - minute, 14 - second |
| + | -- calendarmodel: e.g. http://www.wikidata.org/entity/Q1985727 for the proleptic Gregorian calendar or http://www.wikidata.org/wiki/Q11184 for the Julian calendar] |
| + | if parameter then |
| + | if parameter == "calendarmodel" then data.calendarmodel = mw.ustring.match(data.calendarmodel, "Q%d+") -- extract entity id from the calendar model URI |
| + | elseif parameter == "time" then data.time = normalizeDate(data.time) end |
| + | return data[parameter] |
| + | else |
| + | return formatDate(data.time, data.precision, data.timezone) |
| + | end |
| + | end |
| + | |
| + | local function printDatavalueMonolingualText(data, parameter) |
| + | -- data fields: language [string], text [string] |
| + | if parameter then |
| + | return data[parameter] |
| + | else |
| + | local result = mw.ustring.gsub(mw.ustring.gsub(i18n.monolingualtext, "%%language", data["language"]), "%%text", data["text"]) |
| + | return result |
| + | end |
| + | end |
| + | |
| + | local function findClaims(entity, property) |
| + | if not property or not entity or not entity.claims then return end |
| + | |
| + | if mw.ustring.match(property, "^P%d+$") then |
| + | -- if the property is given by an id (P..) access the claim list by this id |
| + | return entity.claims[property] |
| + | else |
| + | property = mw.wikibase.resolvePropertyId(property) |
| + | if not property then return end |
| + | |
| + | return entity.claims[property] |
| + | end |
| + | end |
| + | |
| + | local function getSnakValue(snak, parameter) |
| + | if snak.snaktype == "value" then |
| + | -- call the respective snak parser |
| + | if snak.datavalue.type == "string" then return snak.datavalue.value |
| + | elseif snak.datavalue.type == "globecoordinate" then return printDatavalueCoordinate(snak.datavalue.value, parameter) |
| + | elseif snak.datavalue.type == "quantity" then return printDatavalueQuantity(snak.datavalue.value, parameter) |
| + | elseif snak.datavalue.type == "time" then return printDatavalueTime(snak.datavalue.value, parameter) |
| + | elseif snak.datavalue.type == "wikibase-entityid" then return printDatavalueEntity(snak.datavalue.value, parameter) |
| + | elseif snak.datavalue.type == "monolingualtext" then return printDatavalueMonolingualText(snak.datavalue.value, parameter) |
| + | end |
| + | end |
| + | return mw.wikibase.renderSnak(snak) |
| + | end |
| + | |
| + | local function getQualifierSnak(claim, qualifierId) |
| + | -- a "snak" is Wikidata terminology for a typed key/value pair |
| + | -- a claim consists of a main snak holding the main information of this claim, |
| + | -- as well as a list of attribute snaks and a list of references snaks |
| + | if qualifierId then |
| + | -- search the attribute snak with the given qualifier as key |
| + | if claim.qualifiers then |
| + | local qualifier = claim.qualifiers[qualifierId] |
| + | if qualifier then return qualifier[1] end |
| + | end |
| + | return nil, printError("qualifier-not-found") |
| + | else |
| + | -- otherwise return the main snak |
| + | return claim.mainsnak |
| + | end |
| + | end |
| + | |
| + | local function getValueOfClaim(claim, qualifierId, parameter) |
| + | local error |
| + | local snak |
| + | snak, error = getQualifierSnak(claim, qualifierId) |
| + | if snak then |
| + | return getSnakValue(snak, parameter) |
| + | else |
| + | return nil, error |
| + | end |
| + | end |
| + | |
| + | local function getReferences(frame, claim) |
| + | local result = "" |
| + | -- traverse through all references |
| + | for ref in pairs(claim.references or {}) do |
| + | local refparts |
| + | -- traverse through all parts of the current reference |
| + | for snakkey, snakval in orderedpairs(claim.references[ref].snaks or {}, claim.references[ref]["snaks-order"]) do |
| + | if refparts then refparts = refparts .. ", " else refparts = "" end |
| + | -- output the label of the property of the reference part, e.g. "imported from" for P143 |
| + | refparts = refparts .. tostring(mw.wikibase.getLabel(snakkey)) .. ": " |
| + | -- output all values of this reference part, e.g. "German Wikipedia" and "English Wikipedia" if the referenced claim was imported from both sites |
| + | for snakidx = 1, #snakval do |
| + | if snakidx > 1 then refparts = refparts .. ", " end |
| + | refparts = refparts .. getSnakValue(snakval[snakidx]) |
| + | end |
| + | end |
| + | if refparts then result = result .. frame:extensionTag("ref", refparts) end |
| + | end |
| + | return result |
| + | end |
| + | |
| + | local function parseInput(frame) |
| + | local qid = frame.args.qid |
| + | if qid and (#qid == 0) then qid = nil end |
| + | local propertyID = mw.text.trim(frame.args[1] or "") |
| + | local input_parm = mw.text.trim(frame.args[2] or "") |
| + | if input_parm ~= "FETCH_WIKIDATA" then |
| + | return false, input_parm, nil, nil |
| + | end |
| + | local entity = mw.wikibase.getEntity(qid) |
| + | local claims |
| + | if entity and entity.claims then |
| + | claims = entity.claims[propertyID] |
| + | if not claims then |
| + | return false, "", nil, nil |
| + | end |
| + | else |
| + | return false, "", nil, nil |
| + | end |
| + | return true, entity, claims, propertyID |
| + | end |
| + | local function isType(claims, type) |
| + | return claims[1] and claims[1].mainsnak.snaktype == "value" and claims[1].mainsnak.datavalue.type == type |
| + | end |
| + | local function getValue(entity, claims, propertyID, delim, labelHook) |
| + | if labelHook == nil then |
| + | labelHook = function (qnumber) |
| + | return nil; |
| + | end |
| + | end |
| + | if isType(claims, "wikibase-entityid") then |
| + | local out = {} |
| + | for k, v in pairs(claims) do |
| + | local qnumber = "Q" .. v.mainsnak.datavalue.value["numeric-id"] |
| + | local sitelink = mw.wikibase.getSitelink(qnumber) |
| + | local label = labelHook(qnumber) or mw.wikibase.getLabel(qnumber) or qnumber |
| + | if sitelink then |
| + | out[#out + 1] = "[[" .. sitelink .. "|" .. label .. "]]" |
| + | else |
| + | out[#out + 1] = "[[:d:" .. qnumber .. "|" .. label .. "]]<abbr title='" .. i18n["errors"]["local-article-not-found"] .. "'>[*]</abbr>" |
| + | end |
| + | end |
| + | return table.concat(out, delim) |
| + | else |
| + | -- just return best values |
| + | return entity:formatPropertyValues(propertyID).value |
| + | end |
| + | end |
| + | |
| + | ------------------------------------------------------------------------------ |
| + | -- module global functions |
| + | |
| + | if debug then |
| + | function p.inspectI18n(frame) |
| + | local val = i18n |
| + | for _, key in pairs(frame.args) do |
| + | key = mw.text.trim(key) |
| + | val = val[key] |
| + | end |
| + | return val |
| + | end |
| + | end |
| + | |
| + | function p.descriptionIn(frame) |
| + | local langcode = frame.args[1] |
| + | local id = frame.args[2] |
| + | -- return description of a Wikidata entity in the given language or the default language of this Wikipedia site |
| + | return mw.wikibase.getEntity(id):getDescription(langcode or wiki.langcode) |
| + | end |
| + | |
| + | function p.labelIn(frame) |
| + | local langcode = frame.args[1] |
| + | local id = frame.args[2] |
| + | -- return label of a Wikidata entity in the given language or the default language of this Wikipedia site |
| + | return mw.wikibase.getEntity(id):getLabel(langcode or wiki.langcode) |
| + | end |
| + | |
| + | -- This is used to get a value, or a comma separated list of them if multiple values exist |
| + | p.getValue = function(frame) |
| + | local delimdefault = ", " -- **internationalise later** |
| + | local delim = frame.args.delimiter or "" |
| + | delim = string.gsub(delim, '"', '') |
| + | if #delim == 0 then |
| + | delim = delimdefault |
| + | end |
| + | local go, errorOrentity, claims, propertyID = parseInput(frame) |
| + | if not go then |
| + | return errorOrentity |
| + | end |
| + | return getValue(errorOrentity, claims, propertyID, delim) |
| + | end |
| + | |
| + | -- Same as above, but uses the short name property for label if available. |
| + | p.getValueShortName = function(frame) |
| + | local go, errorOrentity, claims, propertyID = parseInput(frame) |
| + | if not go then |
| + | return errorOrentity |
| + | end |
| + | local entity = errorOrentity |
| + | -- if wiki-linked value output as link if possible |
| + | local function labelHook (qnumber) |
| + | local label |
| + | local claimEntity = mw.wikibase.getEntity(qnumber) |
| + | if claimEntity ~= nil then |
| + | if claimEntity.claims.P1813 then |
| + | for k2, v2 in pairs(claimEntity.claims.P1813) do |
| + | if v2.mainsnak.datavalue.value.language == "en" then |
| + | label = v2.mainsnak.datavalue.value.text |
| + | end |
| + | end |
| + | end |
| + | end |
| + | if label == nil or label == "" then return nil end |
| + | return label |
| + | end |
| + | return getValue(errorOrentity, claims, propertyID, ", ", labelHook); |
| + | end |
| + | |
| + | -- This is used to get a value, or a comma separated list of them if multiple values exist |
| + | -- from an arbitrary entry by using its QID. |
| + | -- Use : {{#invoke:Wikidata|getValueFromID|<ID>|<Property>|FETCH_WIKIDATA}} |
| + | -- E.g.: {{#invoke:Wikidata|getValueFromID|Q151973|P26|FETCH_WIKIDATA}} - to fetch value of 'spouse' (P26) from 'Richard Burton' (Q151973) |
| + | -- Please use sparingly - this is an *expensive call*. |
| + | p.getValueFromID = function(frame) |
| + | local itemID = mw.text.trim(frame.args[1] or "") |
| + | local propertyID = mw.text.trim(frame.args[2] or "") |
| + | local input_parm = mw.text.trim(frame.args[3] or "") |
| + | if input_parm == "FETCH_WIKIDATA" then |
| + | local entity = mw.wikibase.getEntity(itemID) |
| + | local claims |
| + | if entity and entity.claims then |
| + | claims = entity.claims[propertyID] |
| + | end |
| + | if claims then |
| + | return getValue(entity, claims, propertyID, ", ") |
| + | else |
| + | return "" |
| + | end |
| + | else |
| + | return input_parm |
| + | end |
| + | end |
| + | local function getQualifier(frame, outputHook) |
| + | local propertyID = mw.text.trim(frame.args[1] or "") |
| + | local qualifierID = mw.text.trim(frame.args[2] or "") |
| + | local input_parm = mw.text.trim(frame.args[3] or "") |
| + | if input_parm == "FETCH_WIKIDATA" then |
| + | local entity = mw.wikibase.getEntity() |
| + | if entity.claims[propertyID] ~= nil then |
| + | local out = {} |
| + | for k, v in pairs(entity.claims[propertyID]) do |
| + | for k2, v2 in pairs(v.qualifiers[qualifierID]) do |
| + | if v2.snaktype == 'value' then |
| + | out[#out + 1] = outputHook(v2); |
| + | end |
| + | end |
| + | end |
| + | return table.concat(out, ", "), true |
| + | else |
| + | return "", false |
| + | end |
| + | else |
| + | return input_parm, false |
| + | end |
| + | end |
| + | p.getQualifierValue = function(frame) |
| + | local function outputValue(value) |
| + | local qnumber = "Q" .. value.datavalue.value["numeric-id"] |
| + | if (mw.wikibase.getSitelink(qnumber)) then |
| + | return "[[" .. mw.wikibase.getSitelink(qnumber) .. "]]" |
| + | else |
| + | return "[[:d:" .. qnumber .. "|" ..qnumber .. "]]<abbr title='" .. i18n["errors"]["local-article-not-found"] .. "'>[*]</abbr>" |
| + | end |
| + | end |
| + | return (getQualifier(frame, outputValue)) |
| + | end |
| + | |
| + | -- This is used to get a value like 'male' (for property p21) which won't be linked and numbers without the thousand separators |
| + | p.getRawValue = function(frame) |
| + | local go, errorOrentity, claims, propertyID = parseInput(frame) |
| + | if not go then |
| + | return errorOrentity |
| + | end |
| + | local entity = errorOrentity |
| + | local result = entity:formatPropertyValues(propertyID, mw.wikibase.entity.claimRanks).value |
| + | -- if number type: remove thousand separators, bounds and units |
| + | if isType(claims, "quantity") then |
| + | result = mw.ustring.gsub(result, "(%d),(%d)", "%1%2") |
| + | result = mw.ustring.gsub(result, "(%d)±.*", "%1") |
| + | end |
| + | return result |
| + | end |
| + | |
| + | -- This is used to get the unit name for the numeric value returned by getRawValue |
| + | p.getUnits = function(frame) |
| + | local go, errorOrentity, claims, propertyID = parseInput(frame) |
| + | if not go then |
| + | return errorOrentity |
| + | end |
| + | local entity = errorOrentity |
| + | local result = entity:formatPropertyValues(propertyID, mw.wikibase.entity.claimRanks).value |
| + | if isType(claims, "quantity") then |
| + | result = mw.ustring.sub(result, mw.ustring.find(result, " ")+1, -1) |
| + | end |
| + | return result |
| + | end |
| + | |
| + | -- This is used to get the unit's QID to use with the numeric value returned by getRawValue |
| + | p.getUnitID = function(frame) |
| + | local go, errorOrentity, claims = parseInput(frame) |
| + | if not go then |
| + | return errorOrentity |
| + | end |
| + | local entity = errorOrentity |
| + | local result |
| + | if isType(claims, "quantity") then |
| + | -- get the url for the unit entry on Wikidata: |
| + | result = claims[1].mainsnak.datavalue.value.unit |
| + | -- and just reurn the last bit from "Q" to the end (which is the QID): |
| + | result = mw.ustring.sub(result, mw.ustring.find(result, "Q"), -1) |
| + | end |
| + | return result |
| + | end |
| + | |
| + | p.getRawQualifierValue = function(frame) |
| + | local function outputHook(value) |
| + | if value.datavalue.value["numeric-id"] then |
| + | return mw.wikibase.getLabel("Q" .. value.datavalue.value["numeric-id"]) |
| + | else |
| + | return value.datavalue.value |
| + | end |
| + | end |
| + | local ret, gotData = getQualifier(frame, outputHook) |
| + | if gotData then |
| + | ret = string.upper(string.sub(ret, 1, 1)) .. string.sub(ret, 2) |
| + | end |
| + | return ret |
| + | end |
| + | |
| + | -- This is used to get a date value for date_of_birth (P569), etc. which won't be linked |
| + | -- Dates and times are stored in ISO 8601 format (sort of). |
| + | -- At present the local formatDate(date, precision, timezone) function doesn't handle timezone |
| + | -- So I'll just supply "Z" in the call to formatDate below: |
| + | p.getDateValue = function(frame) |
| + | local date_format = mw.text.trim(frame.args[3] or i18n["datetime"]["default-format"]) |
| + | local date_addon = mw.text.trim(frame.args[4] or i18n["datetime"]["default-addon"]) |
| + | local go, errorOrentity, claims = parseInput(frame) |
| + | if not go then |
| + | return errorOrentity |
| + | end |
| + | local entity = errorOrentity |
| + | local out = {} |
| + | for k, v in pairs(claims) do |
| + | if v.mainsnak.datavalue.type == 'time' then |
| + | local timestamp = v.mainsnak.datavalue.value.time |
| + | local dateprecision = v.mainsnak.datavalue.value.precision |
| + | -- A year can be stored like this: "+1872-00-00T00:00:00Z", |
| + | -- which is processed here as if it were the day before "+1872-01-01T00:00:00Z", |
| + | -- and that's the last day of 1871, so the year is wrong. |
| + | -- So fix the month 0, day 0 timestamp to become 1 January instead: |
| + | timestamp = timestamp:gsub("%-00%-00T", "-01-01T") |
| + | out[#out + 1] = parseDateFull(timestamp, dateprecision, date_format, date_addon) |
| + | end |
| + | end |
| + | return table.concat(out, ", ") |
| + | end |
| + | p.getQualifierDateValue = function(frame) |
| + | local date_format = mw.text.trim(frame.args[4] or i18n["datetime"]["default-format"]) |
| + | local date_addon = mw.text.trim(frame.args[5] or i18n["datetime"]["default-addon"]) |
| + | local function outputHook(value) |
| + | local timestamp = value.datavalue.value.time |
| + | return parseDateValue(timestamp, date_format, date_addon) |
| + | end |
| + | return (getQualifier(frame, outputHook)) |
| + | end |
| + | |
| + | -- This is used to fetch all of the images with a particular property, e.g. image (P18), Gene Atlas Image (P692), etc. |
| + | -- Parameters are | propertyID | value / FETCH_WIKIDATA / nil | separator (default=space) | size (default=frameless) |
| + | -- It will return a standard wiki-markup [[File:Filename | size]] for each image with a selectable size and separator (which may be html) |
| + | -- e.g. {{#invoke:Wikidata|getImages|P18|FETCH_WIKIDATA}} |
| + | -- e.g. {{#invoke:Wikidata|getImages|P18|FETCH_WIKIDATA|<br>|250px}} |
| + | -- If a property is chosen that is not of type "commonsMedia", it will return empty text. |
| + | p.getImages = function(frame) |
| + | local sep = mw.text.trim(frame.args[3] or " ") |
| + | local imgsize = mw.text.trim(frame.args[4] or "frameless") |
| + | local go, errorOrentity, claims = parseInput(frame) |
| + | if not go then |
| + | return errorOrentity |
| + | end |
| + | local entity = errorOrentity |
| + | if (claims[1] and claims[1].mainsnak.datatype == "commonsMedia") then |
| + | local out = {} |
| + | for k, v in pairs(claims) do |
| + | local filename = v.mainsnak.datavalue.value |
| + | out[#out + 1] = "[[File:" .. filename .. "|" .. imgsize .. "]]" |
| + | end |
| + | return table.concat(out, sep) |
| + | else |
| + | return "" |
| + | end |
| + | end |
| + | |
| + | -- This is used to get the TA98 (Terminologia Anatomica first edition 1998) values like 'A01.1.00.005' (property P1323) |
| + | -- which are then linked to http://www.unifr.ch/ifaa/Public/EntryPage/TA98%20Tree/Entity%20TA98%20EN/01.1.00.005%20Entity%20TA98%20EN.htm |
| + | -- uses the newer mw.wikibase calls instead of directly using the snaks |
| + | -- formatPropertyValues returns a table with the P1323 values concatenated with ", " so we have to split them out into a table in order to construct the return string |
| + | p.getTAValue = function(frame) |
| + | local ent = mw.wikibase.getEntity() |
| + | local props = ent:formatPropertyValues('P1323') |
| + | local out = {} |
| + | local t = {} |
| + | for k, v in pairs(props) do |
| + | if k == 'value' then |
| + | t = mw.text.split( v, ", ") |
| + | for k2, v2 in pairs(t) do |
| + | out[#out + 1] = "[http://www.unifr.ch/ifaa/Public/EntryPage/TA98%20Tree/Entity%20TA98%20EN/" .. string.sub(v2, 2) .. "%20Entity%20TA98%20EN.htm " .. v2 .. "]" |
| + | end |
| + | end |
| + | end |
| + | local ret = table.concat(out, "<br> ") |
| + | if #ret == 0 then |
| + | ret = "Invalid TA" |
| + | end |
| + | return ret |
| + | end |
| + | |
| + | --[[ |
| + | This is used to return an image legend from Wikidata |
| + | image is property P18 |
| + | image legend is property P2096 |
| + | |
| + | Call as {{#invoke:Wikidata |getImageLegend | <PARAMETER> | lang=<ISO-639code> |id=<QID>}} |
| + | Returns PARAMETER, unless it is equal to "FETCH_WIKIDATA", from Item QID (expensive call) |
| + | If QID is omitted or blank, the current article is used (not an expensive call) |
| + | If lang is omitted, it uses the local wiki language, otherwise it uses the provided ISO-639 language code |
| + | ISO-639: https://docs.oracle.com/cd/E13214_01/wli/docs92/xref/xqisocodes.html#wp1252447 |
| + | |
| + | Ranks are: 'preferred' > 'normal' |
| + | This returns the label from the first image with 'preferred' rank |
| + | Or the label from the first image with 'normal' rank if preferred returns nothing |
| + | Ranks: https://www.mediawiki.org/wiki/Extension:Wikibase_Client/Lua |
| + | ]] |
| + | |
| + | p.getImageLegend = function(frame) |
| + | -- look for named parameter id; if it's blank make it nil |
| + | local id = frame.args.id |
| + | if id and (#id == 0) then |
| + | id = nil |
| + | end |
| + | |
| + | -- look for named parameter lang |
| + | -- it should contain a two-character ISO-639 language code |
| + | -- if it's blank fetch the language of the local wiki |
| + | local lang = frame.args.lang |
| + | if (not lang) or (#lang < 2) then |
| + | lang = mw.language.getContentLanguage().code |
| + | end |
| + | |
| + | -- first unnamed parameter is the local parameter, if supplied |
| + | local input_parm = mw.text.trim(frame.args[1] or "") |
| + | if input_parm == "FETCH_WIKIDATA" then |
| + | local ent = mw.wikibase.getEntity(id) |
| + | local imgs |
| + | if ent and ent.claims then |
| + | imgs = ent.claims.P18 |
| + | end |
| + | local imglbl |
| + | if imgs then |
| + | -- look for an image with 'preferred' rank |
| + | for k1, v1 in pairs(imgs) do |
| + | if v1.rank == "preferred" and v1.qualifiers and v1.qualifiers.P2096 then |
| + | local imglbls = v1.qualifiers.P2096 |
| + | for k2, v2 in pairs(imglbls) do |
| + | if v2.datavalue.value.language == lang then |
| + | imglbl = v2.datavalue.value.text |
| + | break |
| + | end |
| + | end |
| + | end |
| + | end |
| + | -- if we don't find one, look for an image with 'normal' rank |
| + | if (not imglbl) then |
| + | for k1, v1 in pairs(imgs) do |
| + | if v1.rank == "normal" and v1.qualifiers and v1.qualifiers.P2096 then |
| + | local imglbls = v1.qualifiers.P2096 |
| + | for k2, v2 in pairs(imglbls) do |
| + | if v2.datavalue.value.language == lang then |
| + | imglbl = v2.datavalue.value.text |
| + | break |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| + | return imglbl |
| + | else |
| + | return input_parm |
| + | end |
| + | end |
| + | |
| + | -- This is used to get the QIDs of all of the values of a property, as a comma separated list if multiple values exist |
| + | -- Usage: {{#invoke:Wikidata |getPropertyIDs |<PropertyID> |FETCH_WIKIDATA}} |
| + | -- Usage: {{#invoke:Wikidata |getPropertyIDs |<PropertyID> |<InputParameter> |qid=<QID>}} |
| + | |
| + | p.getPropertyIDs = function(frame) |
| + | local go, errorOrentity, propclaims = parseInput(frame) |
| + | if not go then |
| + | return errorOrentity |
| + | end |
| + | local entity = errorOrentity |
| + | -- if wiki-linked value collect the QID in a table |
| + | if (propclaims[1] and propclaims[1].mainsnak.snaktype == "value" and propclaims[1].mainsnak.datavalue.type == "wikibase-entityid") then |
| + | local out = {} |
| + | for k, v in pairs(propclaims) do |
| + | out[#out + 1] = "Q" .. v.mainsnak.datavalue.value["numeric-id"] |
| + | end |
| + | return table.concat(out, ", ") |
| + | else |
| + | -- not a wikibase-entityid, so return empty |
| + | return "" |
| + | end |
| + | end |
| + | |
| + | -- returns the page id (Q...) of the current page or nothing of the page is not connected to Wikidata |
| + | function p.pageId(frame) |
| + | return mw.wikibase.getEntityIdForCurrentPage() |
| + | end |
| + | |
| + | function p.claim(frame) |
| + | local property = frame.args[1] or "" |
| + | local id = frame.args["id"] |
| + | local qualifierId = frame.args["qualifier"] |
| + | local parameter = frame.args["parameter"] |
| + | local list = frame.args["list"] |
| + | local references = frame.args["references"] |
| + | local showerrors = frame.args["showerrors"] |
| + | local default = frame.args["default"] |
| + | if default then showerrors = nil end |
| + | |
| + | -- get wikidata entity |
| + | local entity = mw.wikibase.getEntity(id) |
| + | if not entity then |
| + | if showerrors then return printError("entity-not-found") else return default end |
| + | end |
| + | -- fetch the first claim of satisfying the given property |
| + | local claims = findClaims(entity, property) |
| + | if not claims or not claims[1] then |
| + | if showerrors then return printError("property-not-found") else return default end |
| + | end |
| + | |
| + | -- get initial sort indices |
| + | local sortindices = {} |
| + | for idx in pairs(claims) do |
| + | sortindices[#sortindices + 1] = idx |
| + | end |
| + | -- sort by claim rank |
| + | local comparator = function(a, b) |
| + | local rankmap = { deprecated = 2, normal = 1, preferred = 0 } |
| + | local ranka = rankmap[claims[a].rank or "normal"] .. string.format("%08d", a) |
| + | local rankb = rankmap[claims[b].rank or "normal"] .. string.format("%08d", b) |
| + | return ranka < rankb |
| + | end |
| + | table.sort(sortindices, comparator) |
| + | |
| + | local result |
| + | local error |
| + | if list then |
| + | local value |
| + | -- iterate over all elements and return their value (if existing) |
| + | result = {} |
| + | for idx in pairs(claims) do |
| + | local claim = claims[sortindices[idx]] |
| + | value, error = getValueOfClaim(claim, qualifierId, parameter) |
| + | if not value and showerrors then value = error end |
| + | if value and references then value = value .. getReferences(frame, claim) end |
| + | result[#result + 1] = value |
| + | end |
| + | result = table.concat(result, list) |
| + | else |
| + | -- return first element |
| + | local claim = claims[sortindices[1]] |
| + | result, error = getValueOfClaim(claim, qualifierId, parameter) |
| + | if result and references then result = result .. getReferences(frame, claim) end |
| + | end |
| + | |
| + | if result then return result else |
| + | if showerrors then return error else return default end |
| + | end |
| + | end |
| + | |
| + | -- look into entity object |
| + | function p.ViewSomething(frame) |
| + | local f = (frame.args[1] or frame.args.id) and frame or frame:getParent() |
| + | local id = f.args.id |
| + | if id and (#id == 0) then |
| + | id = nil |
| + | end |
| + | local data = mw.wikibase.getEntity(id) |
| + | if not data then |
| + | return nil |
| + | end |
| + | |
| + | local i = 1 |
| + | while true do |
| + | local index = f.args[i] |
| + | if not index then |
| + | if type(data) == "table" then |
| + | return mw.text.jsonEncode(data, mw.text.JSON_PRESERVE_KEYS + mw.text.JSON_PRETTY) |
| + | else |
| + | return tostring(data) |
| + | end |
| + | end |
| + | |
| + | data = data[index] or data[tonumber(index)] |
| + | if not data then |
| + | return |
| + | end |
| + | |
| + | i = i + 1 |
| + | end |
| + | end |
| + | |
| + | -- getting sitelink of a given wiki |
| + | -- get sitelink of current item if qid not supplied |
| + | function p.getSiteLink(frame) |
| + | local qid = frame.args.qid |
| + | if qid == "" then qid = nil end |
| + | local f = mw.text.trim( frame.args[1] or "") |
| + | local entity = mw.wikibase.getEntity(qid) |
| + | if not entity then |
| + | return |
| + | end |
| + | local link = entity:getSitelink( f ) |
| + | if not link then |
| + | return |
| + | end |
| + | return link |
| + | end |
| + | |
| + | function p.Dump(frame) |
| + | local f = (frame.args[1] or frame.args.id) and frame or frame:getParent() |
| + | local data = mw.wikibase.getEntity(f.args.id) |
| + | if not data then |
| + | return i18n.warnDump |
| + | end |
| + | |
| + | local i = 1 |
| + | while true do |
| + | local index = f.args[i] |
| + | if not index then |
| + | return "<pre>"..mw.dumpObject(data).."</pre>".. i18n.warnDump |
| + | end |
| + | |
| + | data = data[index] or data[tonumber(index)] |
| + | if not data then |
| + | return i18n.warnDump |
| + | end |
| + | |
| + | i = i + 1 |
| + | end |
| + | end |
| + | |
| + | return p |