Module:Wikidata/Sandbox
Jump to navigation
Jump to search
Documentation for this module may be created at Module:Wikidata/Sandbox/doc
local entities = require('Module:Wikidata/FormatEntity')
local linguistic = require('Module:Linguistic')
local selectClaims = require('Module:Wikidata/GetClaims')
local tools = require('Module:Wikidata/Tools')
local wiki = {
langcode = mw.language.getContentLanguage().code
}
-- Wiki-specific parameters
local defaultlang = mw.getCurrentFrame():callParserFunction('Int', 'Lang')
local defaultlink = 'wikidata'
local i18n = {
["errors"] = {
["property-param-not-provided"] = "Property parameter not provided.",
["property-not-found"] = "Property not found.",
["entity-not-found"] = "Entity not found.",
["qualifier-not-found"] = "Qualifier not found.",
["unknown-claim-type"] = "Unknown claim type.",
["unknown-snak-type"] = "Unknown snak type.",
["unknown-datavalue-type"] = "Unknown datavalue type.",
["unknown-entity-type"] = "Unknown entity type.",
["unknown-value-module"] = "You must set both value-module and value-function parameters.",
["value-module-not-found"] = "The module pointed by value-module not found.",
["value-function-not-found"] = "The function pointed by value-function not found."
},
["somevalue"] = "Unknown value",
["novalue"] = "Missing value",
["monolingualtext"] = '<span lang="%language">%text</span>'
}
-- 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
local function getEntityFromId( id )
if id then
return mw.wikibase.getEntityObject( id )
end
return mw.wikibase.getEntityObject()
end
local function getEntityIdFromValue( value )
local prefix = ''
if value['entity-type'] == 'item' then
prefix = 'Q'
elseif value['entity-type'] == 'property' then
prefix = 'P'
else
return formatError( 'unknown-entity-type' )
end
return prefix .. value['numeric-id']
end
local function formatError( key )
return '<span class="error">' .. i18n.errors[key] .. '</span>'
end
local function removeBlanks(args)
for i = #args, 1, -1 do
if (args[i] == '') or (args[i] == '-') then
table.remove(args, i)
end
end
return args
end
local function formatStatements( options )
if not options.property then
return formatError( 'property-param-not-provided' )
end
--Get entity
local entity = getEntityFromId( options.entityId )
if not entity then
return -- formatError( 'entity-not-found' )
end
if (entity.claims == nil) or (not entity.claims[string.upper(options.property)]) then
return '' --TODO error?
end
--Format statement and concat them cleanly
local formattedStatements = {}
for i, statement in pairs( entity.claims[string.upper(options.property)] ) do
table.insert( formattedStatements, formatStatement( statement, options ) )
end
if options.first then
return formattedStatements[1]
else
return mw.text.listToText( formattedStatements, options.separator, options.conjunction )
end
end
local function formatStatement( statement, options )
if not statement.type or statement.type ~= 'statement' then
return formatError( 'unknown-claim-type' )
end
return formatSnak( statement.mainsnak, options )
end
local function formatSnak( snak, options )
if snak.snaktype == 'somevalue' then
return i18n['somevalue']
elseif snak.snaktype == 'novalue' then
return i18n['novalue']
elseif snak.snaktype == 'value' then
return formatDatavalue( snak.datavalue, options )
else
return formatError( 'unknown-snak-type' )
end
end
local function formatGlobeCoordinate( value, options )
if options['subvalue'] == 'latitude' then
return value['latitude']
elseif options['subvalue'] == 'longitude' then
return value['longitude']
else
local eps = 0.0000001 -- < 1/360000
local globe = '' -- TODO
local lat = {}
lat['abs'] = math.abs(value['latitude'])
lat['ns'] = value['latitude'] >= 0 and 'N' or 'S'
lat['d'] = math.floor(lat['abs'] + eps)
lat['m'] = math.floor((lat['abs'] - lat['d']) * 60 + eps)
lat['s'] = math.max(0, ((lat['abs'] - lat['d']) * 60 - lat['m']) * 60)
local lon = {}
lon['abs'] = math.abs(value['longitude'])
lon['ew'] = value['longitude'] >= 0 and 'E' or 'W'
lon['d'] = math.floor(lon['abs'] + eps)
lon['m'] = math.floor((lon['abs'] - lon['d']) * 60 + eps)
lon['s'] = math.max(0, ((lon['abs'] - lon['d']) * 60 - lon['m']) * 60)
local coord = '{{coord'
if (value['precision'] == nil) or (value['precision'] < 1/60) then -- по умолчанию с точностью до секунды
coord = coord .. '|' .. lat['d'] .. '|' .. lat['m'] .. '|' .. lat['s'] .. '|' .. lat['ns']
coord = coord .. '|' .. lon['d'] .. '|' .. lon['m'] .. '|' .. lon['s'] .. '|' .. lon['ew']
elseif value['precision'] < 1 then
coord = coord .. '|' .. lat['d'] .. '|' .. lat['m'] .. '|' .. lat['ns']
coord = coord .. '|' .. lon['d'] .. '|' .. lon['m'] .. '|' .. lon['ew']
else
coord = coord .. '|' .. lat['d'] .. '|' .. lat['ns']
coord = coord .. '|' .. lon['d'] .. '|' .. lon['ew']
end
coord = coord .. '|globe:' .. globe
if options['display'] then
coord = coord .. '|display=' .. options.display
else
coord = coord .. '|display=title'
end
coord = coord .. '}}'
return g_frame:preprocess(coord)
end
end
local function formatDatavalue( datavalue, options )
--Use the customize handler if provided
if options['value-module'] or options['value-function'] then
if not options['value-module'] or not options['value-function'] then
return formatError( 'unknown-value-module' )
end
local formatter = require ('Module:' .. options['value-module'])
if formatter == nil then
return formatError( 'value-module-not-found' )
end
local fun = formatter[options['value-function']]
if fun == nil then
return formatError( 'value-function-not-found' )
end
return fun( datavalue.value, options )
end
--Default formatters
if datavalue.type == 'wikibase-entityid' then
return formatEntityId( getEntityIdFromValue( datavalue.value ), options )
elseif datavalue.type == 'string' then
if options.pattern and options.pattern ~= '' then
return formatFromPattern( datavalue.value, options )
else
return datavalue.value
end
elseif datavalue.type == 'globecoordinate' then
return formatGlobeCoordinate( datavalue.value, options )
elseif datavalue.type == 'time' then
local Time = require 'Module:Time'
return Time.newFromWikidataValue( datavalue.value ):toHtml()
else
return formatError( 'unknown-datavalue-type' )
end
end
local function formatEntityId( entityId, options )
if (options.format == 'id') then
return entityId
end
local label = mw.wikibase.label( entityId )
local link = mw.wikibase.sitelink( entityId )
if link and (options.format ~= 'label') then
if label then
return '[[' .. link .. '|' .. label .. ']]'
else
return '[[' .. link .. ']]'
end
else
return label --TODO what if no links and label + fallback language?
end
end
local p = {}
-- == FUNCTIONS HANDLING SEVERAL STATEMENTS ===
p.getClaims = selectClaims.getClaims
function p.formatSnak(snak, params)
-- special values: 'novalue' and 'somevalue'
local datatype = snak.datatype
if snak.snaktype == 'somevalue' then
return i18n('somevalue')
elseif snak.snaktype == 'novalue' then
return i18n('novalue')
end
local value = snak.datavalue.value
-- user-defined displayformat
local displayformat = params.displayformat
if type(displayformat) == 'function' then
return displayformat(snak, params)
end
if datatype == 'wikibase-item' or datatype == 'wikibase-property' then
return entities.formatEntity(tools.getId(snak), params)
elseif datatype == 'url' then
return weblink.makelink(value, params.text, params.displayformat)
elseif datatype == 'math' then
return mw.getCurrentFrame():extensionTag('math', value)
elseif datatype == 'string' or datatype == 'external-id' or datatype == 'commonsMedia' then
if params.urlpattern then
local urlpattern = params.urlpattern
if type(urlpattern) == 'function' then
urlpattern = urlpattern(value)
end
value = '[' .. mw.ustring.gsub(urlpattern, '$1', value) .. ' ' .. (params.text or value) .. ']'
end
return value
elseif datatype == 'time' then -- format example: +00000001809-02-12T00:00:00Z
return dates.formatTimeSnak(snak, params)
elseif datatype == 'globe-coordinate' then
-- default return a table with latitude, longitude, precision and globe that can be used by another module
if displayformat == 'latitude' then
return value.latitude
elseif displayformat == 'longitude' then
return value.longitude
elseif displayformat == 'qualifier' then
local coord = require('Module:Coordinates')
value.globe = require('Module:Wikidata/Globes')[value.globe]
value.precision = nil
return coord._coord(value)
else
value.globe = require('Module:Wikidata/Globes')[value.globe] -- get English name for geohack
return value
end
elseif datatype == 'quantity' then
-- TODO: handle precision parameters
if displayformat == 'raw' then
return tonumber(value.amount)
else
local formatNum = require('Module:Formatnum')
local number = formatNum.formatNum(value.amount)
local unit = mw.ustring.match(value.unit, '(Q%d+)')
if unit then
number = number .. ' ' .. entities.formatEntity(unit, params)
end
return number
end
elseif datatype == 'monolingualtext' then
if displayformat == 'raw' then
-- Don't use HTML
local byte, char = string.byte, mw.ustring.char
local lang = value.language:lower()
local tag = {}
table.insert(tag, char(0x2068)) -- U+2068: First Strong Isolate (FSI)
table.insert(tag, char(0xE0001)) -- U+E0001: Language tag
for i = 1, #lang do
local b = byte(lang, i)
if b >= 0x61 and b <= 0x7A or b == 0x2D or b == 0x5F then -- 'a'..'z', '-', '_'
table.insert(tag, char(0xE0000 + b)) -- U+E0020..U+E007E: Tag characters (remaps ASCII only)
end
end
table.insert(tag, value.text)
table.insert(tag, char(0xE007F)) -- U+E007F: Cancel Tag
table.insert(tag, char(0x2069)) -- U+2069: Pop Directional Isolate (PDI)
return table.concat(tag)
else
return '<bdi lang="' .. value.language .. '">' .. value.text .. '</bdi>'
end
else
return formatError('unknown-datavalue-type', datatype )
end
end
function p.formatStatementQualifiers(statement, qualifs, params)
if not params then params = {} end
local qualiftable = getQualifiers(statement, qualifs, params)
if not qualiftable then
return nil
end
for i, j in pairs(qualiftable) do
local params = params
if j.datatype == 'globe-coordinate' then
params.displayformat = 'qualifier'
end
qualiftable[i] = p.formatSnak(j, params)
end
return linguistic.conj(qualiftable, params.lang or defaultlang)
end
function p.formatStatement(statement, args)
if not statement.type or statement.type ~= 'statement' then
return formatError('unknown-claim-type', statement.type)
end
if not args then args = {} end
local lang = args.lang or defaultlang
local str = p.formatSnak(statement.mainsnak, args)
local qualifs = args.showqualifiers
if qualifs then
local qualifStr = p.formatStatementQualifiers(statement, qualifs, args)
if qualifStr then
if args.delimiter then
str = str .. args.delimiter .. qualifStr
else
-- str = str .. linguistic.inparentheses(qualifStr, lang)
str = str .. ' (' .. qualifStr .. ')'
end
end
end
if args.showdate then -- when `showdate` and `p.chronosort` are both set, date retrieval is performed twice
local params
local date = dates.getFormattedDate(statement, params)
if date then
-- str = str .. ' <small>' .. linguistic.inparentheses(date, lang) .. '</small>'
str = str .. ' <small>(' .. date ..')</small>'
end
end
if args.showsource and statement.references then
local cite = require('Module:Cite')
local frame = mw.getCurrentFrame()
local sourcestring = ''
for i, ref in pairs(statement.references) do
if ref.snaks.P248 then
for j, source in pairs(ref.snaks.P248) do
if not tools.isSpecial(source) then
local page
if ref.snaks.P304 and not tools.isSpecial(ref.snaks.P304[1]) then
page = ref.snaks.P304[1].datavalue.value
end
local s = cite.citeitem('Q' .. source.datavalue.value['numeric-id'], lang, page)
s = frame:extensionTag('ref', s)
sourcestring = sourcestring .. s
end
end
elseif ref.snaks.P854 and not tools.isSpecial(ref.snaks.P854[1]) then
s = frame:extensionTag('ref', p.getDatavalue(ref.snaks.P854[1]))
sourcestring = sourcestring .. s
end
end
str = str .. sourcestring
end
return str
end
function p.stringTable(args) -- find the relevant claims, and formats them as a list of strings
-- Get claims
local claims = p.getClaims(args)
if not claims then
return nil
end
-- Define the formatter function
local formatterFun = p.formatStatement
if args.type == 'date' then
formatterFun = dates.getFormattedDate
elseif args.type == 'qualifiers' then
formatterFun = formatStatementQualifiers
end
-- Format claims
for i = #claims, 1, -1 do
claims[i] = formatterFun(claims[i], args)
if not claims[i] then
table.remove(claims, i)
end
end
return claims
end
function p.formatStatements(args)--Format statements and concat them cleanly
-- If a value is already set, use it
if args.value == '-' then
return nil
end
if args.value then
return args.value
end
-- Obsolete parameters
if args.item then
args.entity = args.item
end
local values = p.stringTable(args) -- gets statements, and format each of them
if not values then
return nil
end
return linguistic.conj(values, args.lang or defaultlang, args.conjtype) -- concatenate them
end
-- == FRAME FUNCTIONS ==
function p.formatStatementsE(frame)
local args = {}
if frame == mw.getCurrentFrame() then
args = frame:getParent().args -- paramètres du modèle appelant (est-ce vraiment une bonne idée ?)
for k, v in pairs(frame.args) do
args[k] = v
end
else
args = frame
end
args = removeBlanks(args)
return p.formatStatements(args)
end
local function formatDatavalueCoordinate(data, parameter)
-- data fields: latitude [double], longitude [double], altitude [double], precision [double], globe [wikidata URI, usually http://www.wikidata.org/entity/Q2 [earth]]
if parameter then
if parameter == "globe" then data.globe = mw.ustring.match(data.globe, "Q%d+") end -- extract entity id from the globe URI
return data[parameter]
else
return data.latitude .. "/" .. data.longitude -- combine latitude and longitude, which can be decomposed using the #titleparts wiki function
end
end
local function formatDatavalueQuantity(data, parameter)
-- data fields: amount [number], unit [string], upperBound [number], lowerBound [number]
if parameter then
return data[paramater]
else
return tonumber(data.amount)
end
end
local function formatDatavalueEntity(data, parameter)
-- data fields: entity-type [string], numeric-id [int, Wikidata id]
local id = "Q" .. data["numeric-id"]
if parameter then
if parameter == "link" then
return "[[" .. (mw.wikibase.sitelink(id) or (":d:" .. id)) .. "|" .. (mw.wikibase.label(id) or id) .. "]]"
else
return data[parameter]
end
else
if data["entity-type"] == "item" then return mw.wikibase.label("Q" .. data["numeric-id"]) or id else formatError("unknown-entity-type") end
end
end
local function formatDatavalueMonolingualText(data, parameter)
-- data fields: language [string], text [string]
if parameter then
return data[parameter]
else
return mw.ustring.gsub(mw.ustring.gsub(i18n.monolingualtext, "%%language", data["language"]), "%%text", data["text"])
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)
-- snaks have three types: "novalue" for null/nil, "somevalue" for not null/not nil, or "value" for actual data
if snak.snaktype == "novalue" then
return i18n["novalue"]
elseif snak.snaktype == "somevalue" then
return i18n["somevalue"]
elseif snak.snaktype ~= "value" then
return nil, formatError("unknown-snak-type")
end
-- call the respective snak parser
if snak.datavalue.type == "string" then return snak.datavalue.value
elseif snak.datavalue.type == "globecoordinate" then return formatDatavalueCoordinate(snak.datavalue.value, parameter)
elseif snak.datavalue.type == "quantity" then return formatDatavalueQuantity(snak.datavalue.value, parameter)
elseif snak.datavalue.type == "wikibase-entityid" then return formatDatavalueEntity(snak.datavalue.value, parameter)
elseif snak.datavalue.type == "monolingualtext" then return formatDatavalueMonolingualText(snak.datavalue.value, parameter)
else return nil, formatError("unknown-datavalue-type")
end
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, formatError("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
function p.claim(frame)
local property = frame.args[1] or ""
local id = frame.args["id"] -- "id" must be nil, as access to other Wikidata objects is disabled in Mediawiki configuration
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.getEntityObject(id)
if not entity then
if showerrors then return formatError("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 formatError("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
return p