Модуль:Wikidata: различия между версиями
Admin (обсуждение | вклад) Нет описания правки |
Admin (обсуждение | вклад) мНет описания правки |
||
(не показано 6 промежуточных версий 2 участников) | |||
Строка 1: | Строка 1: | ||
-- settings, may differ from project to project | ---settings, may differ from project to project | ||
local fileDefaultSize = '267x400px' | local fileDefaultSize = '267x400px' | ||
local outputReferences = true | local outputReferences = true | ||
-- Ссылки на используемые модули, которые потребуются в 99% случаев загрузки страниц (чтобы иметь на виду при переименовании) | ---Ссылки на используемые модули, которые потребуются в 99% случаев загрузки страниц (чтобы иметь на виду при переименовании) | ||
local moduleSources = require( 'Module:Sources' ) | local moduleSources = require( 'Module:Sources' ) | ||
local WDS = require( 'Module:WikidataSelectors' ) | local WDS = require( 'Module:WikidataSelectors' ) | ||
-- Константы | ---Константы | ||
local | ---@type string | ||
local CONTENT_LANGUAGE_CODE = mw.language.getContentLanguage():getCode() | |||
local p = {} | |||
local g_config, g_frame | |||
local formatDatavalue, formatEntityId, formatRefs, formatSnak, formatStatement, | local formatDatavalue, formatEntityId, formatRefs, formatSnak, formatStatement, | ||
formatStatementDefault, getSourcingCircumstances, getPropertyParams | |||
---@param obj table | |||
---@param target table | |||
---@param skipEmpty boolean | nil | |||
---@return table | |||
local function copyTo( obj, target, skipEmpty ) | local function copyTo( obj, target, skipEmpty ) | ||
for key, val in pairs( obj ) do | |||
if skipEmpty ~= true or ( val ~= nil and val ~= '' ) then | |||
target[ key ] = val | |||
end | |||
end | |||
return target | |||
end | end | ||
---@param prev number | nil | |||
---@param next number | nil | |||
---@return number | nil | |||
local function min( prev, next ) | local function min( prev, next ) | ||
if prev == nil or prev > next then | |||
return next | |||
end | |||
return prev | |||
end | end | ||
---@param prev number | nil | |||
---@param next number | nil | |||
---@return number | nil | |||
local function max( prev, next ) | local function max( prev, next ) | ||
if prev == nil or prev < next then | |||
return next | |||
end | |||
return prev | |||
end | end | ||
---@param section string | |||
---@param code string | |||
---@return any | nil | |||
local function getConfig( section, code ) | local function getConfig( section, code ) | ||
if g_config == nil then | |||
g_config = require( 'Module:Wikidata/config' ) | |||
end | |||
if not g_config then | |||
g_config = {} | |||
end | |||
if not section then | |||
return g_config | |||
end | |||
if not code then | |||
return g_config[ section ] or {} | |||
end | |||
if not g_config[ section ] then | |||
return nil | |||
end | |||
return g_config[ section ][ code ] | |||
end | end | ||
local function getCategoryByCode( code, | ---@param code string | ||
---@param sortKey string | nil | |||
---@return string | |||
local function getCategoryByCode( code, sortKey ) | |||
local value = getConfig( 'categories', code ) | |||
if not value or value == '' then | |||
return '' | |||
end | |||
if sortKey ~= nil then | |||
return '[[Category:' .. value .. '|' .. sortKey .. ']]'; -- экранировать? | |||
else | |||
return '[[Category:' .. value .. ']]' | |||
end | |||
end | end | ||
local function splitISO8601( | ---@param isoStr string | table | ||
---@return table | nil | |||
local function splitISO8601( isoStr ) | |||
if 'table' == type( isoStr ) then | |||
if isoStr.args and isoStr.args[ 1 ] then | |||
isoStr = '' .. isoStr.args[ 1 ] | |||
else | |||
return 'unknown argument type: ' .. type( isoStr ) .. ': ' .. table.tostring( isoStr ) | |||
end | |||
end | |||
local Y, M, D = ( function( str ) | |||
local pattern = "(%-?%d+)%-(%d+)%-(%d+)T" | |||
local _Y, _M, _D = mw.ustring.match( str, pattern ) | |||
return tonumber( _Y ), tonumber( _M ), tonumber( _D ) | |||
end )( isoStr ) | |||
local h, m, s = ( function( str ) | |||
local pattern = "T(%d+):(%d+):(%d+)%Z" | |||
local _H, _M, _S = mw.ustring.match( str, pattern ) | |||
return tonumber( _H ), tonumber( _M ), tonumber( _S ) | |||
end )( isoStr ) | |||
local oh, om = ( function( str ) | |||
if str:sub(-1) == "Z" then -- ends with Z, Zulu time | |||
return 0, 0 | |||
end | |||
-- matches ±hh:mm, ±hhmm or ±hh; else returns nils | |||
local pattern = "([-+])(%d%d):?(%d?%d?)$" | |||
local sign, oh, om = mw.ustring.match( str, pattern ) | |||
sign, oh, om = sign or "+", oh or "00", om or "00" | |||
return tonumber( sign .. oh ), tonumber( sign .. om ) | |||
end )( isoStr ) | |||
return { year=Y, month=M, day=D, hour=( h + oh ), min=( m + om ), sec=s } | |||
end | end | ||
---@param time string | |||
---@param precision number | |||
---@return table | nil | |||
local function parseTimeBoundaries( time, precision ) | local function parseTimeBoundaries( time, precision ) | ||
local s = splitISO8601( time ) | |||
if not s then | |||
return nil | |||
end | |||
if precision >= 0 and precision <= 8 then | |||
local powers = { 1000000000 , 100000000, 10000000, 1000000, 100000, 10000, 1000, 100, 10 } | |||
local power = powers[ precision + 1 ] | |||
local left = s.year - ( s.year % power ) | |||
return { tonumber( os.time( { year=left, month=1, day=1, hour=0, min=0, sec=0 } ) ) * 1000, | |||
tonumber( os.time( { year=left + power - 1, month=12, day=31, hour=29, min=59, sec=58 } ) ) * 1000 + 1999 } | |||
end | |||
if precision == 9 then | |||
return { tonumber( os.time( { year=s.year, month=1, day=1, hour=0, min=0, sec=0} )) * 1000, | |||
tonumber( os.time( { year=s.year, month=12, day=31, hour=23, min=59, sec=58} )) * 1000 + 1999 } | |||
end | |||
if precision == 10 then | |||
local lastDays = { 31, 28.25, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } | |||
local lastDay = lastDays[ s.month ] | |||
return { tonumber( os.time( { year=s.year, month=s.month, day=1, hour=0, min=0, sec=0 } ) ) * 1000, | |||
tonumber( os.time( { year=s.year, month=s.month, day=lastDay, hour=23, min=59, sec=58 } ) ) * 1000 + 1999 } | |||
end | |||
if precision == 11 then | |||
return { tonumber( os.time( { year=s.year, month=s.month, day=s.day, hour=0, min=0, sec=0 } ) ) * 1000, | |||
tonumber( os.time( { year=s.year, month=s.month, day=s.day, hour=23, min=59, sec=58 } ) ) * 1000 + 1999 } | |||
end | |||
if precision == 12 then | |||
return { tonumber( os.time( { year=s.year, month=s.month, day=s.day, hour=s.hour, min=0, sec=0 } ) ) * 1000, | |||
tonumber( os.time( { year=s.year, month=s.month, day=s.day, hour=s.hour, min=59, sec=58 } ) ) * 1000 + 1999 } | |||
end | |||
if precision == 13 then | |||
return { tonumber( os.time( { year=s.year, month=s.month, day=s.day, hour=s.hour, min=s.min, sec=0 } ) ) * 1000, | |||
tonumber( os.time( { year=s.year, month=s.month, day=s.day, hour=s.hour, min=s.min, sec=58 } ) ) * 1000 + 1999 } | |||
end | |||
if precision == 14 then | |||
local t = tonumber( os.time( { year=s.year, month=s.month, day=s.day, hour=s.hour, min=s.min, sec=0 } ) ) | |||
return { t * 1000, t * 1000 + 999 } | |||
end | |||
error( 'Unsupported precision: ' .. precision ) | |||
end | end | ||
-- | ---Функция для формирования категории на основе wikidata/config | ||
---@param options table | |||
---@param entityId string | |||
local function extractCategory( options, | ---@return string | ||
local function extractCategory( options, entityId ) | |||
if not entityId or not options.category or options.nocat then | |||
return '' | |||
end | |||
if type( entityId ) ~= 'string' then | |||
entityId = entityId.id | |||
end | |||
local claims = WDS.load( entityId, options.category ) | |||
if not claims then | |||
return '' | |||
end | |||
for _, claim in pairs( claims ) do | |||
if claim | |||
and claim.mainsnak | |||
and claim.mainsnak.datavalue | |||
and claim.mainsnak.datavalue.type == 'wikibase-entityid' | |||
then | |||
local catEntityId = claim.mainsnak.datavalue.value.id | |||
local wbStatus, catSiteLink = pcall( mw.wikibase.getSitelink, catEntityId ) | |||
if wbStatus and catSiteLink then | |||
return '[[' .. catSiteLink .. ']]' | |||
end | |||
end | |||
end | |||
return '' | |||
end | end | ||
-- | ---Преобразует строку в булевое значение | ||
---@param valueToParse string | |||
---@return boolean Преобразованное значение, если его удалось распознать, или defaultValue во всех остальных случаях | |||
local function toBoolean( valueToParse, defaultValue ) | local function toBoolean( valueToParse, defaultValue ) | ||
if valueToParse ~= nil then | |||
if valueToParse == false or valueToParse == '' or valueToParse == 'false' or valueToParse == '0' then | |||
return false | |||
end | |||
return true | |||
end | |||
return defaultValue | |||
end | end | ||
-- Обрачивает отформатированное значение в инлайновый или блочный тег. | ---Обрачивает отформатированное значение в инлайновый или блочный тег. | ||
-- @param value | ---@param value string value | ||
-- @param attributes | ---@param attributes table of attributes | ||
-- @return string HTML tag with value | ---@return string HTML tag with value | ||
local function wrapValue( value, attributes ) | local function wrapValue( value, attributes ) | ||
local tagName = 'span' | |||
local spacer = '' | |||
if string.match( value, '\n' ) | |||
or string.match( value, '<t[dhr][ >]' ) | |||
or string.match( value, '<div[ >]' ) | |||
or string.find( value, 'UNIQ%-%-imagemap' ) | |||
then | |||
tagName = 'div' | |||
spacer = '\n' | |||
end | |||
local attrString = '' | |||
for key, val in pairs( attributes or {} ) do | |||
local _key = mw.text.trim( key ) | |||
local _value = mw.text.encode( mw.text.trim( val ) ) | |||
attrString = attrString .. _key .. '="' .. _value .. '" ' | |||
end | |||
return '<' .. tagName .. ' ' .. attrString .. '>' .. spacer .. value .. '</' .. tagName .. '>' | |||
end | end | ||
-- Wraps formatted snak value into HTML tag with attributes. | ---Wraps formatted snak value into HTML tag with attributes. | ||
-- @param value | ---@param value string value of snak | ||
-- @param hash | ---@param hash string | ||
-- @param attributes | ---@param attributes table of extra attributes | ||
-- @return string HTML tag with value | ---@return string HTML tag with value | ||
local function wrapSnak( value, hash, attributes ) | local function wrapSnak( value, hash, attributes ) | ||
local newAttributes = mw.clone( attributes or {} ) | |||
newAttributes[ 'class' ] = ( newAttributes[ 'class' ] or '' ) .. ' wikidata-snak' | |||
if hash then | |||
newAttributes[ 'data-wikidata-hash'] = hash | |||
else | |||
newAttributes[ 'class' ] = newAttributes[ 'class' ] .. ' wikidata-main-snak' | |||
end | |||
return wrapValue( value, newAttributes ) | |||
end | end | ||
-- Wraps formatted statement value into HTML tag with attributes. | ---Wraps formatted statement value into HTML tag with attributes. | ||
-- @param value | ---@param value string value of statement | ||
-- @param propertyId | ---@param propertyId string PID of property | ||
-- @param claimId | ---@param claimId string ID of claim or nil for local value | ||
-- @param attributes | ---@param attributes table of extra attributes | ||
-- @return string HTML tag with value | ---@return string HTML tag with value | ||
local function wrapStatement( value, propertyId, claimId, attributes ) | local function wrapStatement( value, propertyId, claimId, attributes ) | ||
local newAttributes = mw.clone( attributes or {} ) | |||
newAttributes[ 'class' ] = newAttributes[ 'class' ] or '' | |||
newAttributes[ 'data-wikidata-property-id' ] = string.upper( propertyId ) | |||
if claimId then | |||
newAttributes[ 'class' ] = newAttributes[ 'class' ] .. ' wikidata-claim' | |||
newAttributes[ 'data-wikidata-claim-id' ] = claimId | |||
else | |||
newAttributes[ 'class' ] = newAttributes[ 'class' ] .. ' no-wikidata' | |||
end | |||
return wrapValue( value, newAttributes ) | |||
end | end | ||
-- Wraps formatted qualifier's statement value into HTML tag with attributes. | ---Wraps formatted qualifier's statement value into HTML tag with attributes. | ||
-- @param value | ---@param value string value of qualifier's statement | ||
-- @param | ---@param qualifierId string PID of qualifier | ||
-- @param attributes | ---@param attributes table of extra attributes | ||
-- @return string HTML tag with value | ---@return string HTML tag with value | ||
local function wrapQualifier( value, qualifierId, attributes ) | local function wrapQualifier( value, qualifierId, attributes ) | ||
local newAttributes = mw.clone( attributes or {} ) | |||
newAttributes[ 'data-wikidata-qualifier-id' ] = string.upper( qualifierId ) | |||
return wrapValue( value, newAttributes ) | |||
end | end | ||
-- | ---Функция для получения сущности (еntity) для текущей страницы | ||
---Подробнее о сущностях см. d:Wikidata:Glossary/ru | |||
---@param id string Идентификатор (типа P18, Q42) | |||
---@return table Таблица, элементы которой индексируются с нуля | |||
local function getEntityFromId( id ) | |||
local entity | |||
local wbStatus | |||
if id then | |||
wbStatus, entity = pcall( mw.wikibase.getEntity, id ) | |||
else | |||
--- Удаляем из-за ошибки wbStatus, entity = pcall( mw.wikibase.getEntity ) | |||
end | |||
-- | |||
-- | |||
return entity | |||
end | |||
---Внутренняя функция для формирования сообщения об ошибке | |||
---@param key string Ключ элемента в таблице config.errors (например entity-not-found) | |||
---@return void | |||
local function throwError( key ) | local function throwError( key ) | ||
error( getConfig( 'errors', key ) ) | |||
end | end | ||
-- | ---Функция для получения идентификатора сущностей | ||
---@param value table | |||
---@return string | |||
local function getEntityIdFromValue( value ) | |||
local prefix = '' | |||
if value[ 'entity-type' ] == 'item' then | |||
prefix = 'Q' | |||
elseif value[ 'entity-type' ] == 'property' then | |||
prefix = 'P' | |||
else | |||
throwError( 'unknown-entity-type' ) | |||
end | |||
return prefix .. value[ 'numeric-id' ] | |||
end | |||
---Проверка на наличие специализированной функции в опциях | |||
---@param options table | |||
---@param prefix string | |||
-- | ---@return function | ||
-- | |||
local function getUserFunction( options, prefix, defaultFunction ) | local function getUserFunction( options, prefix, defaultFunction ) | ||
-- проверка на указание специализированных обработчиков в параметрах, | |||
-- переданных при вызове | |||
if options[ prefix .. '-module' ] or options[ prefix .. '-function' ] then | |||
-- проверка на пустые строки в параметрах или их отсутствие | |||
if not options[ prefix .. '-module' ] or not options[ prefix .. '-function' ] then | |||
throwError( 'unknown-' .. prefix .. '-module' ) | |||
end | |||
-- динамическая загруза модуля с обработчиком указанным в параметре | |||
local formatter = require( 'Module:' .. options[ prefix .. '-module' ] ) | |||
if formatter == nil then | |||
throwError( prefix .. '-module-not-found' ) | |||
end | |||
local fun = formatter[ options[ prefix .. '-function' ] ] | |||
if fun == nil then | |||
throwError( prefix .. '-function-not-found' ) | |||
end | |||
return fun | |||
end | |||
return defaultFunction | |||
end | end | ||
-- Выбирает свойства по property id, дополнительно фильтруя их по рангу | ---Выбирает свойства по property id, дополнительно фильтруя их по рангу | ||
---@param context table | |||
---@param options table | |||
---@param propertySelector string | |||
---@return table | nil | |||
local function selectClaims( context, options, propertySelector ) | local function selectClaims( context, options, propertySelector ) | ||
if not context then error( 'context not specified' ); end | |||
if not options then error( 'options not specified' ); end | |||
if not options.entityId then error( 'options.entity is missing' ); end | |||
if not propertySelector then error( 'propertySelector not specified' ); end | |||
local result = WDS.load( options.entityId, propertySelector ) | |||
if not result or #result == 0 then | |||
return nil | |||
end | |||
if options.limit and options.limit ~= '' and options.limit ~= '-' then | |||
local limit = tonumber( options.limit, 10 ) | |||
while #result > limit do | |||
table.remove( result ) | |||
end | |||
end | |||
return result | |||
end | end | ||
-- | ---Функция для получения значения свойства элемента в заданный момент времени. | ||
---@param entityId string | |||
---@param boundaries table Временные границы | |||
---@param propertyIds table<string> | |||
---@param selectors table<string> | |||
---@return table Таблица соответствующих значений свойства | |||
local function getPropertyInBoundaries( context, entityId, boundaries, propertyIds, selectors ) | local function getPropertyInBoundaries( context, entityId, boundaries, propertyIds, selectors ) | ||
if type( entityId ) ~= 'string' then error( 'type of entityId argument expected string, but was ' .. type(entityId)); end | |||
local results = {} | |||
if not propertyIds or #propertyIds == 0 then | |||
return results | |||
end | |||
for i, propertyId in ipairs( propertyIds ) do | |||
local selector | |||
if selectors ~= nil then | |||
selector = selectors[ i ] or selectors[ propertyId ] or propertyId | |||
else | |||
selector = propertyId | |||
end | |||
local fakeAllClaims = {} | |||
fakeAllClaims[ propertyId ] = mw.wikibase.getAllStatements( entityId, propertyId ) | |||
local filteredClaims = WDS.filter( fakeAllClaims, selector .. '[rank:preferred, rank:normal]' ) | |||
if filteredClaims then | |||
for _, claim in pairs( filteredClaims ) do | |||
if not boundaries then | |||
table.insert( results, claim.mainsnak ) | |||
else | |||
local startBoundaries = p.getTimeBoundariesFromQualifier( context.frame, context, claim, 'P580' ) | |||
local endBoundaries = p.getTimeBoundariesFromQualifier( context.frame, context, claim, 'P582' ) | |||
if ( startBoundaries == nil or startBoundaries[ 2 ] <= boundaries[ 1 ] ) and | |||
( endBoundaries == nil or endBoundaries[ 1 ] >= boundaries[ 2 ] ) | |||
then | |||
table.insert( results, claim.mainsnak ) | |||
end | |||
end | |||
end | |||
end | |||
if #results > 0 then | |||
break | |||
end | |||
end | |||
return results | |||
end | end | ||
-- | ---@param context table | ||
---@param statement table | |||
---@param qualifierId string | |||
function p.getTimeBoundariesFromQualifier( | ---@return table | nil | ||
function p.getTimeBoundariesFromQualifier( _, context, statement, qualifierId ) | |||
-- only support exact date so far, but need improvement | |||
local left, right | |||
if statement.qualifiers and statement.qualifiers[ qualifierId ] then | |||
for _, qualifier in pairs( statement.qualifiers[ qualifierId ] ) do | |||
local boundaries = context.parseTimeBoundariesFromSnak( qualifier ) | |||
if not boundaries then | |||
return nil | |||
end | |||
left = min( left, boundaries[ 1 ] ) | |||
right = max( right, boundaries[ 2 ] ) | |||
end | |||
end | |||
if not left or not right then | |||
return nil | |||
end | |||
return { left, right } | |||
end | end | ||
-- | ---@param frame table | ||
---@param context table | |||
---@param statement table | |||
---@param qualifierIds table<string> | |||
---@return table | nil | |||
function p.getTimeBoundariesFromQualifiers( frame, context, statement, qualifierIds ) | function p.getTimeBoundariesFromQualifiers( frame, context, statement, qualifierIds ) | ||
if not qualifierIds then | |||
qualifierIds = { 'P582', 'P580', 'P585' } | |||
end | |||
for _, qualifierId in pairs( qualifierIds ) do | |||
local result = p.getTimeBoundariesFromQualifier( frame, context, statement, qualifierId ) | |||
if result then | |||
return result | |||
end | |||
end | |||
return nil | |||
end | end | ||
---@type table<string> | |||
local getLabelWithLang_DEFAULT_PROPERTIES = { | local getLabelWithLang_DEFAULT_PROPERTIES = { 'P1813', 'P1448', 'P1705' } | ||
---@type table<string> | |||
local getLabelWithLang_DEFAULT_SELECTORS = { | local getLabelWithLang_DEFAULT_SELECTORS = { | ||
'P1813[language:' .. CONTENT_LANGUAGE_CODE .. '][!P3831,P3831:Q105690470]', | |||
'P1448[language:' .. CONTENT_LANGUAGE_CODE .. '][!P3831,P3831:Q105690470]', | |||
'P1705[language:' .. CONTENT_LANGUAGE_CODE .. '][!P3831,P3831:Q105690470]' | |||
} | } | ||
---Функция для получения метки элемента в заданный момент времени. | |||
---@param context table | |||
---@param options table | |||
---@param entityId string | |||
---@param boundaries table | |||
---@param propertyIds table | |||
---@param selectors table<string> | |||
---@return string, string Текстовая метка элемента, язык метки | |||
local function getLabelWithLang( context, options, entityId, boundaries, propertyIds, selectors ) | local function getLabelWithLang( context, options, entityId, boundaries, propertyIds, selectors ) | ||
if type( entityId ) ~= 'string' then error( 'type of entityId argument expected string, but was ' .. type( entityId ) ); end | |||
if not entityId then | |||
return nil | |||
end | |||
local langCode = CONTENT_LANGUAGE_CODE | |||
-- name from label | |||
local label | |||
if options.text and options.text ~= '' then | |||
label = options.text | |||
else | |||
if not propertyIds then | |||
propertyIds = getLabelWithLang_DEFAULT_PROPERTIES | |||
selectors = getLabelWithLang_DEFAULT_SELECTORS | |||
end | |||
-- name from properties | |||
local results = getPropertyInBoundaries( context, entityId, boundaries, propertyIds, selectors ) | |||
for _, result in pairs( results ) do | |||
if result.datavalue and result.datavalue.value then | |||
if result.datavalue.type == 'monolingualtext' and result.datavalue.value.text then | |||
label = result.datavalue.value.text | |||
langCode = result.datavalue.value.language | |||
break | |||
elseif result.datavalue.type == 'string' then | |||
label = result.datavalue.value | |||
break | |||
end | |||
end | |||
end | |||
if not label then | |||
label, langCode = mw.wikibase.getLabelWithLang( entityId ) | |||
if not langCode then | |||
return nil | |||
end | |||
end | |||
end | |||
return label, langCode | |||
end | end | ||
---@param context table | |||
---@param options table | |||
---@return string | |||
local function formatPropertyDefault( context, options ) | local function formatPropertyDefault( context, options ) | ||
if not context then error( 'context not specified' ); end | |||
if not options then error( 'options not specified' ); end | |||
if not options.entityId then error( 'options.entityId missing' ); end | |||
local claims | |||
if options.property then -- TODO: Почему тут может не быть property? | |||
if options.rank then -- передать настройки ранга из конфига | |||
claims = context.selectClaims( options, options.property .. options.rank ) | |||
else | |||
claims = context.selectClaims( options, options.property ) | |||
end | |||
end | |||
if claims == nil then | |||
return '' --TODO error? | |||
end | |||
-- Обход всех заявлений утверждения и с накоплением оформленных предпочтительных | |||
-- заявлений в таблице | |||
local formattedClaims = {} | |||
for _, claim in pairs( claims ) do | |||
local formattedStatement = context.formatStatement( options, claim ) | |||
-- здесь может вернуться либо оформленный текст заявления, либо строка ошибки, либо nil | |||
if formattedStatement and formattedStatement ~= '' then | |||
if not options.plain then | |||
formattedStatement = context.wrapStatement( formattedStatement, options.property, claim.id ) | |||
end | |||
table.insert( formattedClaims, formattedStatement ) | |||
end | |||
end | |||
-- создание текстовой строки со списком оформленых заявлений из таблицы | |||
local out = mw.text.listToText( formattedClaims, options.separator, options.conjunction ) | |||
if out ~= '' then | |||
if options.before then | |||
out = options.before .. out | |||
end | |||
if options.after then | |||
out = out .. options.after | |||
end | |||
end | |||
return out | |||
end | end | ||
-- | ---Create context | ||
local function initContext( | ---@param initOptions table | ||
---@return table | nil | |||
local function initContext( initOptions ) | |||
local context = { | |||
entityId = initOptions.entityId, | |||
entity = initOptions.entity, | |||
extractCategory = extractCategory, | |||
formatSnak = formatSnak, | |||
formatPropertyDefault = formatPropertyDefault, | |||
formatStatementDefault = formatStatementDefault, | |||
getPropertyInBoundaries = getPropertyInBoundaries, | |||
getTimeBoundariesFromQualifier = p.getTimeBoundariesFromQualifier, | |||
getTimeBoundariesFromQualifiers = p.getTimeBoundariesFromQualifiers, | |||
wrapSnak = wrapSnak, | |||
wrapStatement = wrapStatement, | |||
wrapQualifier = wrapQualifier, | |||
} | |||
context.cloneOptions = function( options ) | |||
local entity = options.entity | |||
options.entity = nil | |||
local newOptions = mw.clone( options ) | |||
options.entity = entity | |||
newOptions.entity = entity | |||
newOptions.frame = options.frame; -- На склонированном фрейме frame:expandTemplate() | |||
return newOptions | |||
end | |||
context.formatProperty = function( options ) | |||
local func = getUserFunction( options, 'property', context.formatPropertyDefault ) | |||
return func( context, options ) | |||
end | |||
context.formatStatement = function( options, statement ) return formatStatement( context, options, statement ) end | |||
context.formatSnak = function( options, snak, circumstances ) return formatSnak( context, options, snak, circumstances ) end | |||
context.formatRefs = function( options, statement ) return formatRefs( context, options, statement ) end | |||
context.parseTimeFromSnak = function( snak ) | |||
if snak and snak.datavalue and snak.datavalue.value and snak.datavalue.value.time then | |||
return tonumber( os.time( splitISO8601( tostring( snak.datavalue.value.time ) ) ) ) * 1000 | |||
end | |||
return nil | |||
end | |||
context.parseTimeBoundariesFromSnak = function( snak ) | |||
if snak and snak.datavalue and snak.datavalue.value and snak.datavalue.value.time and snak.datavalue.value.precision then | |||
return parseTimeBoundaries( snak.datavalue.value.time, snak.datavalue.value.precision ) | |||
end | |||
return nil | |||
end | |||
context.getSourcingCircumstances = function( statement ) | |||
return getSourcingCircumstances( statement ) | |||
end | |||
context.selectClaims = function( options, propertyId ) | |||
return selectClaims( context, options, propertyId ) | |||
end | |||
return context | |||
end | end | ||
-- | ---Функция для оформления утверждений (statement) | ||
---Подробнее о утверждениях см. d:Wikidata:Glossary/ru | |||
---@param options table | |||
---@return string Formatted wikitext. | |||
local function formatProperty( options ) | |||
-- Получение сущности по идентификатору | |||
local entity = getEntityFromId( options.entityId ) | |||
if not entity then | |||
return -- throwError( 'entity-not-found' ) | |||
end | |||
-- проверка на присутсвие у сущности заявлений (claim) | |||
-- подробнее о заявлениях см. d:Викиданные:Глоссарий | |||
if not entity.claims then | |||
return '' --TODO error? | |||
end | |||
-- improve options | |||
options.frame = g_frame | |||
options.entity = entity | |||
options.extends = function( self, newOptions ) | |||
return copyTo( newOptions, copyTo( self, {} ) ) | |||
end | |||
if options.i18n then | |||
options.i18n = copyTo( options.i18n, copyTo( getConfig( 'i18n' ), {} ) ) | |||
else | |||
options.i18n = getConfig( 'i18n' ) | |||
end | |||
local context = initContext( options ) | |||
return context.formatProperty( options ) | |||
end | end | ||
-- | ---Функция для оформления одного утверждения (statement) | ||
---@param context table | |||
---@param options table | |||
---@param statement table | |||
---@return string Formatted wikitext. | |||
function formatStatement( context, options, statement ) | function formatStatement( context, options, statement ) | ||
if not statement then | |||
error( 'statement is not specified or nil' ) | |||
end | |||
if not statement.type or statement.type ~= 'statement' then | |||
throwError( 'unknown-claim-type' ) | |||
end | |||
local functionToCall = getUserFunction( options, 'claim', context.formatStatementDefault ) | |||
return functionToCall( context, options, statement ) | |||
end | end | ||
---@param statement table | |||
---@return table | |||
function getSourcingCircumstances( statement ) | function getSourcingCircumstances( statement ) | ||
if not statement then | |||
error( 'statement is not specified' ) | |||
end | |||
local circumstances = {} | |||
if statement.qualifiers and statement.qualifiers.P1480 then | |||
for _, qualifier in pairs( statement.qualifiers.P1480 ) do | |||
if qualifier | |||
and qualifier.datavalue | |||
and qualifier.datavalue.type == 'wikibase-entityid' | |||
and qualifier.datavalue.value | |||
and qualifier.datavalue.value[ 'entity-type'] == 'item' | |||
then | |||
table.insert( circumstances, qualifier.datavalue.value.id ) | |||
end | |||
end | |||
end | |||
return circumstances | |||
end | end | ||
-- | ---Функция для оформления одного утверждения (statement) | ||
---@param context table Context. | |||
---@param options table Parameters. | |||
---@param statement table | |||
---@return string Formatted wikitext. | |||
function formatStatementDefault( context, options, statement ) | |||
if not context then error( 'context is not specified' ) end | |||
if not options then error( 'options is not specified' ) end | |||
if not statement then error( 'statement is not specified' ) end | |||
local circumstances = context.getSourcingCircumstances( statement ) | |||
options.qualifiers = statement.qualifiers | |||
local result = context.formatSnak( options, statement.mainsnak, circumstances ) | |||
if options.qualifier and statement.qualifiers and statement.qualifiers[ options.qualifier ] then | |||
local qualifierConfig = getPropertyParams( options.qualifier, nil, {} ) | |||
if | if options.i18n then | ||
qualifierConfig.i18n = options.i18n | |||
end | |||
if qualifierConfig.datatype == 'time' then | |||
qualifierConfig.nolinks = true | |||
end | |||
local qualifierValues = {} | |||
for _, qualifierSnak in pairs( statement.qualifiers[ options.qualifier ] ) do | |||
local snakValue = context.formatSnak( qualifierConfig, qualifierSnak ) | |||
if snakValue and snakValue ~= '' then | |||
table.insert( qualifierValues, snakValue ) | |||
end | |||
end | |||
if result and result ~= '' and #qualifierValues then | |||
if qualifierConfig.invisible then | |||
result = result .. table.concat( qualifierValues, ', ' ) | |||
else | |||
result = result .. ' (' .. table.concat( qualifierValues, ', ' ) .. ')' | |||
end | |||
end | end | ||
end | end | ||
if result and result ~= '' and options.references then | |||
result = result .. context.formatRefs( options, statement ) | |||
end | |||
return result | |||
end | end | ||
-- | ---Функция для оформления части утверждения (snak) | ||
---Подробнее о snak см. d:Викиданные:Глоссарий | |||
---@param context table Context. | |||
---@param options table Parameters. | |||
---@param snak table | |||
---@param circumstances table | |||
---@return string Formatted wikitext. | |||
function formatSnak( context, options, snak, circumstances ) | |||
circumstances = circumstances or {} | |||
local result | |||
if snak.snaktype == 'somevalue' then | |||
if options[ 'somevalue' ] and options[ 'somevalue' ] ~= '' then | |||
]] | result = options[ 'somevalue' ] | ||
else | |||
result = options.i18n[ 'somevalue' ] | |||
end | |||
elseif snak.snaktype == 'novalue' then | |||
if options[ 'novalue' ] and options[ 'novalue' ] ~= '' then | |||
result = options[ 'novalue' ] | |||
else | |||
result = options.i18n[ 'novalue' ] | |||
end | |||
elseif snak.snaktype == 'value' then | |||
result = formatDatavalue( context, options, snak.datavalue, snak.datatype ) | |||
for _, item in pairs( circumstances ) do | |||
if options.i18n[ item ] then | |||
result = options.i18n[ item ] .. result | |||
end | |||
end | |||
else | |||
throwError( 'unknown-snak-type' ) | |||
end | |||
if not result or result == '' then | |||
return nil | |||
end | |||
if options.plain then | |||
return result | |||
end | end | ||
return context.wrapSnak( result, snak.hash ) | |||
end | end | ||
-- | ---Функция для оформления объектов-значений с географическими координатами | ||
---@param value string Raw value. | |||
---@param options table Parameters. | |||
---@return string Formatted string. | |||
local function formatGlobeCoordinate( value, options ) | |||
-- проверка на требование в параметрах вызова на возврат сырого значения | |||
if options[ 'subvalue' ] == 'latitude' then -- широты | |||
return value[ 'latitude' ] | |||
elseif options[ 'subvalue' ] == 'longitude' then -- долготы | |||
return value[ 'longitude' ] | |||
elseif options[ 'nocoord' ] and options[ 'nocoord' ] ~= '' then | |||
-- если передан параметр nocoord, то не выводить координаты | |||
-- обычно это делается при использовании нескольких карточек на странице | |||
return '' | |||
else | |||
-- в противном случае формируются параметры для вызова шаблона {{coord}} | |||
-- нужно дописать в документации шаблона, что он отсюда вызывается, и что | |||
-- любое изменние его парамеров должно быть согласовано с кодом тут | |||
local coordModule = require( 'Module:Coordinates' ) | |||
local globe = options.globe or '' | |||
local | if globe == '' and value[ 'globe' ] then | ||
local globes = require( 'Module:Wikidata/Globes' ) | |||
globe = globes[ value[ 'globe' ] ] or '' | |||
end | |||
local display = 'inline' | |||
if options.display and options.display ~= '' then | |||
display = options.display | |||
elseif ( options.property:upper() == 'P625' ) then | |||
display = 'title' | |||
end | |||
local format = options.format or '' | |||
if format == '' then | |||
format = 'dms' | |||
if value[ 'precision' ] then | |||
local precision = value[ 'precision' ] * 60 | |||
if precision >= 60 then | |||
format = 'd' | |||
elseif precision >= 1 then | |||
format = 'dm' | |||
end | |||
end | |||
end | |||
g_frame.args = { | |||
tostring( value[ 'latitude' ] ), | |||
tostring( value[ 'longitude' ] ), | |||
globe = globe, | |||
type = options.type and options.type or '', | |||
scale = options.scale and options.scale or '', | |||
display = display, | |||
format = format, | |||
} | |||
return coordModule.coord(g_frame) | |||
end | |||
end | end | ||
-- | ---Функция для оформления объектов-значений с файлами с Викисклада | ||
---@param value string Raw value. | |||
---@param options table Parameters. | |||
---@return string Formatted string. | |||
local function formatCommonsMedia( value, options ) | |||
local image = value | |||
local caption = '' | |||
if options[ 'caption' ] and options[ 'caption' ] ~= '' then | |||
] | caption = options[ 'caption' ] | ||
end | |||
if caption ~= '' then | |||
caption = wrapQualifier( caption, 'P2096', { class = 'media-caption', style = 'display:block' } ) | |||
end | |||
if not string.find( value, '[%[%]%{%}]' ) and not string.find( value, 'UNIQ%-%-imagemap' ) then | |||
-- если в value не содержится викикод или imagemap, то викифицируем имя файла | |||
-- ищем слово imagemap в строке, потому что вставляется плейсхолдер: [[phab:T28213]] | |||
image = '[[File:' .. value .. '|frameless' | |||
if options[ 'border' ] and options[ 'border' ] ~= '' then | |||
image = image .. '|border' | |||
end | |||
local size = options[ 'size' ] | |||
if size and size ~= '' then | |||
-- TODO: check localized pixel names too | |||
if not string.match( size, 'px$' ) then | |||
size = size .. 'px' | |||
end | |||
else | |||
size = fileDefaultSize | |||
end | |||
image = image .. '|' .. size | |||
if options[ 'alt' ] and options[ 'alt' ] ~= '' then | |||
image = image .. '|alt=' .. options[ 'alt' ] | |||
end | |||
if caption ~= '' then | |||
image = image .. '|' .. caption | |||
end | |||
image = image .. ']]' | |||
if caption ~= '' then | |||
image = image .. '<br>' .. caption | |||
end | |||
else | |||
image = image .. caption .. getCategoryByCode( 'media-contains-markup' ) | |||
end | |||
return image | |||
end | end | ||
-- | ---Function for render math formulas | ||
---@param value string Value. | |||
---@param options table Parameters. | |||
---@return string Formatted string. | |||
local function formatMath( value, options ) | local function formatMath( value, options ) | ||
return options.frame:extensionTag{ name = 'math', content = value } | |||
end | end | ||
-- | ---Функция для оформления внешних идентификаторов | ||
---@param value string | |||
---@param options table | |||
---@return string | |||
local function formatExternalId( value, options ) | |||
local formatter = options.formatter | |||
local propertyId = options.property:upper() | |||
if not formatter or formatter == '' then | |||
local isGoodFormat = false | |||
local | local wbStatus, formatRegexStatements = pcall( mw.wikibase.getBestStatements, propertyId, 'P1793' ) | ||
if wbStatus and formatRegexStatements then | |||
for _, statement in pairs( formatRegexStatements ) do | |||
if statement.mainsnak.snaktype == 'value' then | |||
local pattern = mw.ustring.gsub( statement.mainsnak.datavalue.value, '\\', '%' ) | |||
pattern = mw.ustring.gsub( pattern, '{%d+,?%d*}', '+' ) | |||
if ( string.find( pattern, '|' ) or string.find( pattern, '%)%?' ) | |||
or mw.ustring.match( value, '^' .. pattern .. '$' ) ~= nil ) then | |||
isGoodFormat = true | |||
break | |||
end | |||
end | |||
end | |||
end | |||
if isGoodFormat then | |||
local formatterStatements | |||
wbStatus, formatterStatements = pcall( mw.wikibase.getBestStatements, propertyId, 'P1630' ) | |||
if wbStatus and formatterStatements then | |||
for _, statement in pairs( formatterStatements ) do | |||
if statement.mainsnak.snaktype == 'value' then | |||
formatter = statement.mainsnak.datavalue.value | |||
break | |||
end | |||
end | |||
end | |||
end | |||
end | |||
if formatter and formatter ~= '' then | |||
local encodedValue = mw.ustring.gsub( value, '%%', '%%%%' ) -- ломается, если подставить внутрь другого mw.ustring.gsub | |||
local link = mw.ustring.gsub( | |||
mw.ustring.gsub( formatter, '$1', encodedValue ), '.', | |||
{ [ ' ' ] = '%20', [ '+' ] = '%2b', [ '[' ] = '%5B', [ ']' ] = '%5D' } ) | |||
local title = options.title | |||
if not title or title == '' then | |||
title = '$1' | |||
end | |||
title = mw.ustring.gsub( | |||
mw.ustring.gsub( title, '$1', encodedValue ), '.', | |||
{ [ '[' ] = '(', [ ']' ] = ')' } ) | |||
return '[' .. link .. ' ' .. title .. ']' | |||
end | |||
return value | |||
end | end | ||
-- | ---Функция для оформления числовых значений | ||
---@param value table Объект-значение | |||
---@param options table Таблица параметров | |||
---@return string Оформленный текст | |||
local function formatQuantity( value, options ) | |||
-- диапазон значений | |||
local amount = string.gsub( value.amount, '^%+', '' ) | |||
local lang = mw.language.getContentLanguage() | |||
local langCode = lang:getCode() | |||
local function formatNum( number, sigfig ) | |||
local multiplier = '' | |||
if options.countByThousands then | |||
local powers = options.i18n.thousandPowers | |||
local pos = 1 | |||
while math.abs( number ) >= 1000 and pos < #powers do | |||
number = number / 1000 | |||
pos = pos + 1 | |||
end | |||
multiplier = powers[ pos ] | |||
if math.abs( number ) >= 100 then | |||
sigfig = sigfig or 0 | |||
elseif math.abs( number ) >= 10 then | |||
local | sigfig = sigfig or 1 | ||
else | |||
sigfig = sigfig or 2 | |||
end | |||
else | |||
sigfig = sigfig or 12 -- округление до 12 знаков после запятой, на 13-м возникает ошибка в точности | |||
end | |||
local iMultiplier = 10^sigfig | |||
number = math.floor( number * iMultiplier + 0.5 ) / iMultiplier | |||
return string.gsub( lang:formatNum( number ), '^-', '−' ) .. multiplier | |||
end | |||
local out = formatNum( tonumber( amount ) ) | |||
if value.upperBound then | |||
local diff = tonumber( value.upperBound ) - tonumber( amount ) | |||
if diff > 0 then -- временная провека, пока у большинства значений не будет убрано ±0 | |||
-- Пробуем понять до какого знака округлять | |||
local integer, dot, decimals, _ = value.upperBound:match( '^+?-?(%d*)(%.?)(%d*)(.*)' ) | |||
local precision | |||
if dot == '' then | |||
precision = -integer:match( '0*$' ):len() | |||
else | |||
precision = #decimals | |||
end | |||
local bound = formatNum( diff, precision ) | |||
if string.match( bound, 'E%-(%d+)' ) then -- если в экспоненциальном формате | |||
local digits = tonumber( string.match( bound, 'E%-(%d+)' ) ) - 2 | |||
bound = formatNum( diff * 10 ^ digits, precision ) | |||
bound = string.sub( bound, 0, 2 ) .. string.rep( '0', digits ) .. string.sub( bound, -string.len( bound ) + 2 ) | |||
end | |||
out = out .. ' ± ' .. bound | |||
end | |||
end | |||
if options.unit and options.unit ~= '' then | |||
if options.unit ~= '-' then | |||
out = out .. ' ' .. options.unit | |||
end | |||
elseif value.unit and string.match( value.unit, 'http://www.wikidata.org/entity/' ) then | |||
local unitEntityId = string.gsub( value.unit, 'http://www.wikidata.org/entity/', '' ) | |||
if unitEntityId ~= 'undefined' then | |||
local wbStatus, unitEntity = pcall( mw.wikibase.getEntity, unitEntityId ) | |||
if wbStatus == true and unitEntity then | |||
if unitEntity.claims.P2370 and | |||
unitEntity.claims.P2370[ 1 ].mainsnak.snaktype == 'value' and | |||
not value.upperBound and | |||
options.siConversion == true | |||
then | |||
local conversionToSiUnit = string.gsub( unitEntity.claims.P2370[ 1 ].mainsnak.datavalue.value.amount, '^%+', '' ) | |||
if math.floor( math.log10( conversionToSiUnit ) ) ~= math.log10( conversionToSiUnit ) then | |||
-- Если не степени десятки (переводить сантиметры в метры не надо!) | |||
local outValue = tonumber( amount ) * conversionToSiUnit | |||
if outValue > 0 then | |||
-- Пробуем понять до какого знака округлять | |||
local integer, dot, decimals, _ = amount:match( '^(%d*)(%.?)(%d*)(.*)' ) | |||
local precision | |||
if dot == '' then | |||
precision = -integer:match( '0*$' ):len() | |||
else | |||
precision = #decimals | |||
end | |||
local adjust = math.log10( math.abs( conversionToSiUnit ) ) + math.log10( 2 ) | |||
local minPrecision = 1 - math.floor( math.log10( outValue ) + 2e-14 ) | |||
out = formatNum( outValue, math.max( math.floor( precision + adjust ), minPrecision ) ) | |||
else | |||
out = formatNum( outValue, 0 ) | |||
end | |||
unitEntityId = string.gsub( unitEntity.claims.P2370[ 1 ].mainsnak.datavalue.value.unit, 'http://www.wikidata.org/entity/', '' ) | |||
wbStatus, unitEntity = pcall( mw.wikibase.getEntity, unitEntityId ) | |||
end | |||
end | |||
local writingSystemElementId = 'Q8209' | |||
local langElementId = 'Q7737' | |||
local label = getLabelWithLang( context, options, unitEntity.id, nil, { "P5061", "P558", "P558" }, { | |||
'P5061[language:' .. langCode .. ']', | |||
'P558[P282:' .. writingSystemElementId .. ', P407:' .. langElementId .. ']', | |||
'P558[!P282][!P407]' | |||
} ) | |||
out = out .. ' ' .. label | |||
end | |||
end | |||
end | |||
return out | |||
end | end | ||
-- Функция для оформления URL | ---Функция для оформления URL | ||
---@param context table | |||
---@param options table | |||
---@param value string | |||
local function formatUrlValue( context, options, value ) | local function formatUrlValue( context, options, value ) | ||
if not options.length or options.length == '' then | |||
options.length = 25 | |||
end | |||
local moduleUrl = require( 'Module:URL' ) | |||
return moduleUrl.formatUrlSingle( context, options, value ) | |||
end | end | ||
local DATATYPE_CACHE = {} | local DATATYPE_CACHE = {} | ||
-- | ---Get property datatype by ID. | ||
---@param propertyId string Property ID, e.g. 'P123'. | |||
---@return string Property datatype, e.g. 'commonsMedia', 'time' or 'url'. | |||
local function getPropertyDatatype( propertyId ) | |||
if not propertyId or not string.match( propertyId, '^P%d+$' ) then | |||
return nil | |||
end | |||
local cached = DATATYPE_CACHE[ propertyId ] | |||
if cached ~= nil then | |||
return cached | |||
end | |||
local wbStatus, propertyEntity = pcall( mw.wikibase.getEntity, propertyId ) | |||
if wbStatus ~= true or not propertyEntity then | |||
return nil | |||
end | |||
mw.log("Loaded datatype " .. propertyEntity.datatype .. " of " .. propertyId .. ' from wikidata, consider passing datatype argument to formatProperty call or to Wikidata/config' ) | |||
DATATYPE_CACHE[ propertyId ] = propertyEntity.datatype | |||
return propertyEntity.datatype | |||
end | |||
---@param datavalue table | |||
---@return function | |||
local function getPlainValueFunction( datavalue, _ ) | |||
if datavalue.type == 'wikibase-entityid' then | |||
return function( _, _, value ) | |||
return getEntityIdFromValue( value ) | |||
end | |||
elseif datavalue.type == 'string' then | |||
return function( _, _, value ) | |||
return value | |||
end | |||
elseif datavalue.type == 'monolingualtext' then | |||
return function( _, _, value ) | |||
return value.text | |||
end | |||
elseif datavalue.type == 'globecoordinate' then | |||
return function( _, _, value ) | |||
return value.latitude .. ',' .. value.longitude | |||
end | |||
elseif datavalue.type == 'quantity' then | |||
return function( _, _, value ) | |||
return value.amount | |||
end | |||
elseif datavalue.type == 'time' then | |||
return function( _, _, value ) | |||
return value.time | |||
end | |||
end | |||
throwError( 'unknown-datavalue-type' ) | |||
end | end | ||
---@param datavalue table | |||
---@param datatype string | |||
---@return function | |||
local function getDefaultValueFunction( datavalue, datatype ) | local function getDefaultValueFunction( datavalue, datatype ) | ||
-- вызов обработчиков по умолчанию для известных типов значений | |||
if datavalue.type == 'wikibase-entityid' then | |||
-- Entity ID | |||
return function( context, options, value ) | |||
return formatEntityId( context, options, getEntityIdFromValue( value ) ) | |||
end | |||
elseif datavalue.type == 'string' then | |||
-- String | |||
if datatype and datatype == 'commonsMedia' then | |||
-- Media | |||
return function( _, options, value ) | |||
return formatCommonsMedia( value, options ) | |||
end | |||
elseif datatype and datatype == 'external-id' then | |||
-- External ID | |||
return function( _, options, value ) | |||
return formatExternalId( value, options ) | |||
end | |||
elseif datatype and datatype == 'math' then | |||
-- Math formula | |||
return function( _, options, value ) | |||
return formatMath( value, options ) | |||
end | |||
elseif datatype and datatype == 'url' then | |||
-- URL | |||
return formatUrlValue | |||
end | |||
return function( _, _, value ) | |||
return value | |||
end | |||
elseif datavalue.type == 'monolingualtext' then | |||
-- моноязычный текст (строка с указанием языка) | |||
return function( _, options, value ) | |||
if options.monolingualLangTemplate == 'lang' then | |||
if value.language == CONTENT_LANGUAGE_CODE then | |||
return value.text | |||
end | |||
return options.frame:expandTemplate{ title = 'lang-' .. value.language, args = { value.text } } | |||
elseif options.monolingualLangTemplate == 'ref' then | |||
return '<span class="lang" lang="' .. value.language .. '">' .. value.text .. '</span>' .. options.frame:expandTemplate{ title = 'ref-' .. value.language } | |||
else | |||
return '<span class="lang" lang="' .. value.language .. '">' .. value.text .. '</span>' | |||
end | |||
end | |||
elseif datavalue.type == 'globecoordinate' then | |||
-- географические координаты | |||
return function( _, options, value ) | |||
return formatGlobeCoordinate( value, options ) | |||
end | |||
elseif datavalue.type == 'quantity' then | |||
return function( _, options, value ) | |||
return formatQuantity( value, options ) | |||
end | |||
elseif datavalue.type == 'time' then | |||
return function( context, options, value ) | |||
local moduleDate = require( 'Module:Wikidata/date' ) | |||
return moduleDate.formatDate( context, options, value ) | |||
end | |||
end | |||
-- во всех стальных случаях возвращаем ошибку | |||
throwError( 'unknown-datavalue-type' ) | |||
end | end | ||
-- | ---Функция для оформления значений (value) | ||
---Подробнее о значениях см. d:Wikidata:Glossary/ru | |||
---@param context table | |||
---@param options table | |||
---@param datavalue table | |||
---@param datatype string | |||
---@return string Оформленный текст | |||
function formatDatavalue( context, options, datavalue, datatype ) | function formatDatavalue( context, options, datavalue, datatype ) | ||
if not context then error( 'context not specified' ); end | |||
if not options then error( 'options not specified' ); end | |||
if not datavalue then error( 'datavalue not specified' ); end | |||
if not datavalue.value then error( 'datavalue.value is missing' ); end | |||
-- проверка на указание специализированных обработчиков в параметрах, | |||
-- переданных при вызове | |||
if options.plain then | |||
context.formatValueDefault = getPlainValueFunction( datavalue, datatype ) | |||
else | |||
context.formatValueDefault = getDefaultValueFunction( datavalue, datatype ) | |||
end | |||
local functionToCall = getUserFunction( options, 'value', context.formatValueDefault ) | |||
return functionToCall( context, options, datavalue.value ) | |||
end | end | ||
local DEFAULT_BOUNDARIES = { os.time() * 1000, os.time() * 1000} | local DEFAULT_BOUNDARIES = { os.time() * 1000, os.time() * 1000} | ||
-- | ---Функция для оформления идентификатора сущности | ||
---@param context table | |||
---@param options table | |||
---@param entityId string | |||
---@return string Оформленный текст | |||
function formatEntityId( context, options, entityId ) | function formatEntityId( context, options, entityId ) | ||
-- получение локализованного названия | |||
local boundaries | |||
if options.qualifiers then | |||
boundaries = p.getTimeBoundariesFromQualifiers( context.frame, context, { qualifiers = options.qualifiers } ) | |||
end | |||
if not boundaries then | |||
boundaries = DEFAULT_BOUNDARIES | |||
end | |||
local label, labelLanguageCode = getLabelWithLang( context, options, entityId, boundaries ) | |||
-- определение соответствующей показываемому элементу категории | |||
local category = context.extractCategory( options, { id = entityId } ) | |||
-- получение ссылки по идентификатору | |||
local link = mw.wikibase.sitelink( entityId ) | |||
if link then | |||
-- ссылка на категорию, а не добавление страницы в неё | |||
if mw.ustring.match( link, '^' .. mw.site.namespaces[ 14 ].name .. ':' ) then | |||
link = ':' .. link | |||
end | |||
if label and not options.rawArticle then | |||
if labelLanguageCode ~= CONTENT_LANGUAGE_CODE then | |||
label = '<span lang="' .. label .. '">' .. label .. '</span>' | |||
end | |||
local a = '[[' .. link .. '|' .. label .. ']]' | |||
if CONTENT_LANGUAGE_CODE ~= labelLanguageCode and 'mul' ~= labelLanguageCode then | |||
a = a .. getCategoryByCode( 'links-to-entities-with-missing-local-language-label' ) | |||
end | |||
return a .. category | |||
else | |||
return '[[' .. link .. ']]' .. category | |||
end | |||
end | |||
if label then -- TODO: возможно, лучше просто mw.wikibase.getLabel(entityId) | |||
-- красная ссылка | |||
-- TODO: разобраться, почему не всегда есть options.frame | |||
local title = mw.title.new( label ) | |||
if title and not title.exists and options.frame then | |||
local moduleRedLink = require( 'Module:Wikidata/redLink' ) | |||
local rawLabel = mw.wikibase.getLabel(entityId) or label -- без |text= и boundaries; or label - костыль | |||
local redLink = moduleRedLink.formatRedLinkWithInfobox(rawLabel, label, entityId) | |||
if CONTENT_LANGUAGE_CODE ~= labelLanguageCode and 'mul' ~= labelLanguageCode then | |||
redLink = '<span lang="' .. labelLanguageCode .. '">' .. redLink .. '</span>' .. | |||
getCategoryByCode( 'links-to-entities-with-missing-local-language-label' ) | |||
end | |||
return redLink .. '<sup>[[:d:' .. entityId .. '|[d]]]</sup>' .. category | |||
end | |||
-- TODO: перенести до проверки на существование статьи | |||
local sup = '' | |||
if ( not options.format or options.format ~= 'text' ) | |||
and entityId ~= 'Q6581072' and entityId ~= 'Q6581097' -- TODO: переписать на format=text | |||
then | |||
sup = '<sup class="plainlinks noprint">[//www.wikidata.org/wiki/' .. entityId .. '?uselang=' .. CONTENT_LANGUAGE_CODE .. ' [d]]</sup>' | |||
end | |||
-- одноимённая статья уже существует - выводится текст и ссылка на ВД | |||
return '<span class="iw" data-title="' .. label .. '">' .. label | |||
.. sup | |||
.. '</span>' .. category | |||
end | |||
-- сообщение об отсутвии локализованного названия | |||
-- not good, but better than nothing | |||
return '[[:d:' .. entityId .. '|' .. entityId .. ']]<span style="border-bottom: 1px dotted; cursor: help; white-space: nowrap" title="В Викиданных нет русской подписи к элементу. Вы можете помочь, указав русский вариант подписи.">?</span>' .. getCategoryByCode( 'links-to-entities-with-missing-label' ) .. category | |||
end | end | ||
-- | ---Функция для оформления утверждений (statement) | ||
---Подробнее о утверждениях см. d:Wikidata:Glossary/ru | |||
---@deprecated Use p.formatProperty() instead | |||
---@param frame table | |||
---@return string Строка оформленного текста, предназначенная для отображения в статье | |||
function p.formatStatements( frame ) | function p.formatStatements( frame ) | ||
return p.formatProperty( frame ) | |||
end | end | ||
-- | ---Получение параметров, которые обычно используются для вывода свойства. | ||
---@param propertyId string | |||
---@param datatype string | |||
---@param params table | |||
function getPropertyParams( propertyId, datatype, params ) | function getPropertyParams( propertyId, datatype, params ) | ||
local config = getConfig() | |||
-- Различные уровни настройки параметров, по убыванию приоритета | |||
local propertyParams = {} | |||
-- 1. Параметры, указанные явно при вызове | |||
if params then | |||
for key, value in pairs( params ) do | |||
if value ~= '' then | |||
propertyParams[ key ] = value | |||
end | |||
end | end | ||
end | |||
if toBoolean( propertyParams.plain, false ) then | |||
propertyParams.separator = propertyParams.separator or ', ' | |||
propertyParams.conjunction = propertyParams.conjunction or ', ' | |||
else | |||
-- 2. Настройки конкретного параметра | |||
if config.properties and config.properties[ propertyId ] then | |||
for key, value in pairs( config.properties[ propertyId ] ) do | |||
if propertyParams[ key ] == nil then | |||
propertyParams[ key ] = value | |||
end | |||
end | |||
end | |||
-- 3. Указанный пресет настроек | |||
if propertyParams.preset and config.presets and | |||
config.presets[ propertyParams.preset ] | |||
then | |||
for key, value in pairs( config.presets[ propertyParams.preset ] ) do | |||
if propertyParams[ key ] == nil then | |||
propertyParams[ key ] = value | |||
end | |||
end | |||
end | |||
datatype = datatype or params.datatype or propertyParams.datatype or getPropertyDatatype( propertyId ) | |||
if propertyParams.datatype == nil then | |||
propertyParams.datatype = datatype | |||
end | |||
-- 4. Настройки для типа данных | |||
if datatype and config.datatypes and config.datatypes[ datatype ] then | |||
for key, value in pairs( config.datatypes[ datatype ] ) do | |||
if propertyParams[ key ] == nil then | |||
propertyParams[ key ] = value | |||
end | |||
end | |||
end | |||
-- 5. Общие настройки для всех свойств | |||
if config.global then | |||
for key, value in pairs( config.global ) do | |||
if propertyParams[ key ] == nil then | |||
propertyParams[ key ] = value | |||
end | |||
end | |||
end | |||
end | end | ||
return propertyParams | |||
end | end | ||
---Функция для оформления утверждений (statement) | |||
---Подробнее о утверждениях см. d:Wikidata:Glossary/ru | |||
---@param frame table | |||
---@return string Строка оформленного текста, предназначенная для отображения в статье | |||
function p.formatProperty( frame ) | function p.formatProperty( frame ) | ||
local args = copyTo( frame.args, {} ) | |||
-- проверка на отсутствие обязательного параметра property | |||
if not args.property then | |||
throwError( 'property-param-not-provided' ) | |||
end | |||
local override | |||
local propertyId = mw.language.getContentLanguage():ucfirst( string.gsub( args.property, '([^Pp0-9].*)$', function(w) | |||
if string.sub( w, 1, 1 ) == '~' then | |||
override = w | |||
end | |||
return '' | |||
end ) ) | |||
if override then | |||
args[ override:match( '[,~]([^=]*)=' ) ] = override:match( '=(.*)' ) | |||
args.property = propertyId | |||
end | |||
-- проброс всех параметров из шаблона {wikidata} и параметра from откуда угодно | |||
local p_frame = frame | |||
while p_frame do | |||
if p_frame:getTitle() == mw.site.namespaces[ 10 ].name .. ':Wikidata' then | |||
copyTo( p_frame.args, args, true ) | |||
end | |||
if p_frame.args and p_frame.args.from and p_frame.args.from ~= '' then | |||
args.entityId = p_frame.args.from | |||
else | |||
--- args.entityId = mw.wikibase.getEntityIdForCurrentPage() | |||
end | |||
p_frame = p_frame:getParent() | |||
end | |||
args = getPropertyParams( propertyId, nil, args ) | |||
local datatype = args.datatype | |||
-- перевод итоговых значений флагов в true/false и добавление значений | |||
-- по умолчанию только в том случае, если они нигде не были указаны ранее | |||
args.plain = toBoolean( args.plain, false ) | |||
args.nocat = not args.plain and toBoolean( args.nocat, false ) | |||
args.references = not args.plain and toBoolean( args.references, true ) | |||
-- если значение передано в параметрах вызова то выводим только его | |||
if args.value and args.value ~= '' then | |||
-- специальное значение для скрытия Викиданных | |||
if args.value == '-' then | |||
return '' | |||
end | |||
local value = args.value | |||
-- опция, запрещающая оформление значения, поэтому никак не трогаем | |||
if args.plain then | |||
return value | |||
end | |||
local context = initContext( args ) | |||
-- обработчики по типу значения | |||
local wrapperExtraArgs = {} | |||
if args[ 'value-module' ] and args[ 'value-function' ] and not string.find( value, '[%[%]%{%}]' ) then | |||
local func = getUserFunction( args, 'value' ) | |||
value = func( context, args, value ) | |||
elseif datatype == 'commonsMedia' then | |||
value = formatCommonsMedia( value, args ) | |||
elseif datatype == 'external-id' and not string.find( value, '[%[%]%{%}]' ) then | |||
wrapperExtraArgs[ 'data-wikidata-external-id' ] = mw.text.killMarkers( value ) | |||
value = formatExternalId( value, args ) | |||
--elseif datatype == 'math' then | |||
-- args.frame = frame -- костыль: в formatMath нужно frame:extensionTag | |||
-- value = formatMath( value, args ) | |||
elseif datatype == 'url' then | |||
value = formatUrlValue( context, args, value ) | |||
end | |||
-- оборачиваем в тег для JS-функций | |||
if string.match( propertyId, '^P%d+$' ) then | |||
value = mw.text.trim( value ) | |||
-- временная штрафная категория для исправления табличных вставок | |||
local allowTables = getPropertyParams( propertyId, nil, {} ).allowTables | |||
if not allowTables | |||
and string.match( value, '<t[dhr][ >]' ) | |||
-- and not string.match( value, '<table[ >]' ) | |||
-- and not string.match( value, '^%{%|' ) | |||
then | |||
value = value .. getCategoryByCode( 'value-contains-table', propertyId ) | |||
else | |||
value = wrapStatement( value, propertyId, nil, wrapperExtraArgs ) | |||
end | |||
end | |||
return value | |||
end | |||
-- ability to disable loading Wikidata | |||
if args.entityId == '-' then | |||
return '' | |||
end | |||
g_frame = frame | |||
-- после проверки всех аргументов -- вызов функции оформления для свойства (набора утверждений) | |||
return formatProperty( args ) | |||
end | end | ||
-- | ---Функция проверки на присутствие источника в списке нерекомендованных. | ||
---@param snaks table | |||
---@return boolean | |||
local function isReferenceDeprecated( snaks ) | |||
if not snaks then | |||
return false | |||
function isReferenceDeprecated( snaks ) | end | ||
if snaks.P248 | |||
and snaks.P248[ 1 ] | |||
and snaks.P248[ 1 ].datavalue | |||
and snaks.P248[ 1 ].datavalue.value.id | |||
then | |||
local entityId = snaks.P248[ 1 ].datavalue.value.id | |||
if getConfig( 'deprecatedSources', entityId ) then | |||
return true | |||
end | |||
elseif snaks.P1433 | |||
and snaks.P1433[ 1 ] | |||
and snaks.P1433[ 1 ].datavalue | |||
and snaks.P1433[ 1 ].datavalue.value.id | |||
then | |||
local entityId = snaks.P1433[ 1 ].datavalue.value.id | |||
if getConfig( 'deprecatedSources', entityId ) then | |||
return true | |||
end | |||
end | |||
return false | |||
end | end | ||
-- | ---Функция оформления ссылок на источники (reference) | ||
---Подробнее о ссылках на источники см. d:Wikidata:Glossary/ru | |||
--- | |||
---Экспортируется в качестве зарезервированной точки для вызова из функций-расширения вида claim-module/claim-function через context | |||
---Вызов из других модулей напрямую осуществляться не должен (используйте frame:expandTemplate вместе с одним из специлизированных шаблонов вывода значения свойства). | |||
---@param context table | |||
---@param options table | |||
---@param statement table | |||
---@return string Оформленные примечания для отображения в статье | |||
function formatRefs( context, options, statement ) | function formatRefs( context, options, statement ) | ||
if not context then error( 'context not specified' ); end | |||
if not options then error( 'options not specified' ); end | |||
if not options.entityId then error( 'options.entityId missing' ); end | |||
if not statement then error( 'statement not specified' ); end | |||
if not outputReferences then | |||
return '' | |||
end | |||
---@type string[] | |||
local references = {} | |||
if statement.references then | |||
local hasNotDeprecated = false | |||
local displayCount = 0 | |||
for _, reference in pairs( statement.references ) do | |||
if not isReferenceDeprecated( reference.snaks ) then | |||
hasNotDeprecated = true | |||
end | |||
end | |||
for _, reference in pairs( statement.references ) do | |||
local display = true | |||
if hasNotDeprecated then | |||
if isReferenceDeprecated( reference.snaks ) then | |||
display = false | |||
end | |||
end | |||
if displayCount >= 2 then | |||
if options.entityId and options.property then | |||
local propertyId = mw.ustring.match( options.property, '^[Pp][0-9]+' ) -- TODO: обрабатывать не тут, а раньше | |||
local moreReferences = '<sup>[[d:' .. options.entityId .. '#' .. string.upper( propertyId ) .. '|[…]]]</sup>' | |||
table.insert( references, moreReferences ) | |||
end | |||
break | |||
end | |||
if display == true then | |||
---@type string | |||
local refText = moduleSources.renderReference( g_frame, options.entityId, reference ) | |||
if refText and refText ~= '' then | |||
table.insert( references, refText ) | |||
displayCount = displayCount + 1 | |||
end | |||
end | |||
end | |||
end | |||
return table.concat( references ) | |||
end | end | ||
return p | return p |
Текущая версия от 09:28, 22 июня 2023
Внимание! Это один из самых используемых модулей. |
Файл:Padlock-silver.svg | Этот модуль относится к критическим. У него очень много включений или он используется с подстановкой. Из-за опасности вандализма или ошибочного редактирования он был защищён. |
Используется в {{Wikidata}} (см. описания параметров там же). Настраивается при помощи Модуль:Wikidata/config.
Прежде чем вносить какие-либо изменения в данный модуль, просьба оттестировать их в /песочнице. Обратите внимание, что не всё корректно работает в песочнице.
Общие сведенияПравить
Функции данного модуля не предназначены для прямого вызова из шаблонов карточек или других модулей, не являющихся функциями расширения данного. Для вызова из шаблонов карточек используйте шаблон {{wikidata}} или один из специализированных шаблонов для свойств. Для вызова функций Викиданных предназначенных для отображения чаще всего достаточно вызова frame:expandTemplate{}
с вызовом шаблона, ответственного за отрисовку свойства. С другой стороны, вызов определённых функций модуля (в основном это касается getEntity()
) может в будущем стать предпочтительным. Данный Lua-функционал в любом случае стоит рассматривать как unstable с точки зрения сохранения совместимости на уровне кода (вместе с соответствующими функциями API для Wikibase Client).
Далее описывается внутренняя документация. Названия функций и параметров могут изменяться. При их изменении автор изменений обязан обновить шаблон {{wikidata}} и специализированные шаблоны свойств. Изменения в других местах, если кто-то всё-таки вызывает функции модуля напрямую, остаются на совести автора «костыля». Итак, при вызове шаблона {{wikidata}} или специализированного шаблона свойства управление отдаётся на функцию formatStatements, которая принимает frame. Из frame достаются следующие опции, которые так или иначе передаются в остальные функции:
plain
— булевый переключатель (по умолчанию false). Если true, результат совпадает с обычным вызовом{{#property:pNNN}}
(по факту им и будет являться)references
— булевый переключатель (по умолчанию true). Если true, после вывода значения параметра дополнительно выводит ссылки на источники, указанные в Викиданных. Для вывода используется Модуль:Sources. Обычно отключается для тех свойств, которые являются «самоописываемыми», например, внешними идентификаторами или ссылками (когда такая ссылка является доказательством своей актуальности), например, идентификаторы IMDb.value
— значение, которое надо выводить вместо значений из Викиданных (используется, если что-то задано уже в карточке в виде т. н. локального свойства)
По умолчанию модуль поддерживает вывод следующих значений без дополнительных настроек:
- географические координаты (coordinates)
- количественные значения (quantity)
- моноязычный текст (monolingualtext)
- строки (string)
- даты (time)
Остальные типы данных требуют указания функции форматирования значения.
КастомизацияПравить
Поддерживаются три типа параметров-функций, которые дополнительно указывают, как надо форматировать значения:
property-module
,property-function
— название модуля и функции модуля, которые отвечают за форматирование вывода массива значений свойства (statements, claims) с учётом квалификаторов, ссылок и прочего. Например, оформляет множество выводов в таблицу или график. Характерные примеры:- вывод таблицы и графика населения в {{wikidata/Population}} и Модуль:Wikidata/Population.
- Спецификация функции:
function p.…( context, options )
, поведение по умолчанию: Модуль:Wikidata#formatPropertyDefault.
claim-module
,claim-function
— название модуля и функции модуля, которые отвечают за форматирование вывода значения свойства (statement, claim) с учётом квалификаторов, ссылок и прочего. Может, например, дополнительно к основному значению (main snak) вывести значения квалификаторов. Характерные примеры:- вывод вышестоящих административных единиц и страны в Модуль:Wikidata/Places;
- вывод авторов латинского названия и даты публикации в Модуль:Wikidata/Biology;
- вывод операционной системы и даты релиза в Модуль:Wikidata/Software;
- вывод количества и даты, на которую оно верно, в Модуль:Wikidata/number;
- Спецификация функции:
function p.…( context, options, statement )
value-module
,value-function
— название модуля и функции модуля, которые отвечают за форматирование значения (snak, snak data value), в зависимости от контекста, как значений свойства, так и значений квалификатора (если вызывается изclaim-module/claim-function
). Необходимо для изменения отображения свойства, например, генерации викиссылки вместо простой строки или даже вставки изображения вместо отображения имени файла изображения (так как ссылки на изображения хранятся как строки). Характерные примеры:- вывод ссылки на Викисклад в Модуль:Wikidata/media
- вывод ссылок на внешние сайты в Модуль:Wikidata/link
- Спецификация функции:
function p.…( value, options )
Заготовки функцийПравить
<syntaxhighlight lang="lua"> function p.formatSomeProperty( context, options ) local claims = context.selectClaims( options, options.property ); if claims == nil then return end
local formattedStatements = {} for _, claim in ipairs( claims ) do local formattedStatement = context.formatStatement( options, claim ) -- local formattedStatement = p.formatSomeStatement( context, options, claim ) if ( formattedStatement and formattedStatement ~= ) then formattedStatement = context.wrapStatement( formattedStatement, options.property, claim.id ) table.insert( formattedStatements, formattedStatement ) end end
return mw.text.listToText( formattedStatements, options.separator, options.conjunction )
end
</syntaxhighlight>
Также см. код метода formatPropertyDefault
ниже, в нём присутствует больше проверок и работа параметрами вызова.
<syntaxhighlight lang="lua"> function formatSomeClaim( context, options, statement ) local circumstances = context.getSourcingCircumstances( statement ); options.qualifiers = statement.qualifiers; local result = context.formatSnak( options, statement.mainsnak, circumstances );
if ( result and result ~= and options.references ) then result = result .. context.formatRefs( options, statement ); end
return result;
end
</syntaxhighlight>
Также см. код метода formatStatementDefault
ниже, в нём есть пример работы с квалификаторами.
<syntaxhighlight lang="lua">
function formatSomeValue( context, options, value )
return value;
end
</syntaxhighlight>
Также см. код метода formatUrlSingle
в модуле Модуль:URL.
Context APIПравить
Этот раздел не завершён. |
ПеременныеПравить
entity
frame
МетодыПравить
cloneOptions( options )
getSourcingCircumstances( statement )
formatProperty( options )
formatPropertyDefault( context, options )
formatSnak( options, snak, circumstances )
formatStatement( options, statement )
formatStatementDefault( context, options, statement )
formatRefs( options, statement )
formatValueDefault( context, options, value )
parseTimeBoundariesFromSnak( snak )
parseTimeFromSnak( snak )
selectClaims( options, propertyId )
wrapSnak( value, hash, attributes )
wrapStatement( value, propertyId, claimId, attributes )
wrapQualifier( value, qualifierId, attributes )
Функции для форматированияПравить
property-functionПравить
- Wikidata/date::formatDateIntervalProperty
- Wikidata/Medals::formatProperty
- Wikidata/Software::formatVersionProperty
- Wikidata/P512::formatAcademicDegree
- Wikidata/number::formatPropertyWithMostRecentClaimAndIndicator
- Wikidata/number::formatColorIndex
claim-functionПравить
- Wikidata/Places::formatCountryClaimWithFlag
- Wikidata/Places::formatPlaceWithQualifiers
- Wikidata/item::formatEntityWithGenderClaim
- Wikidata/Biology::formatTaxonNameClaim
- Wikidata/item::applyDefaultTemplate
- Wikidata/date::formatDateOfBirthClaim
- Wikidata/date::formatDateOfDeathClaim
value-functionПравить
- Wikidata::extractCategory
- Wikidata/link::fromModule
- Wikidata/Medals::formatValue
- Wikidata/media::formatCommonsCategory
- Wikidata/Software::formatExtension
- Wikidata/number::formatRA
- Wikidata/number::formatDMS
- Wikidata/url::formatUrlValue
- Wikidata/url::formatLangRefs
См. такжеПравить
- Модуль:Wikibase
- Независимые иноязычные аналоги:
---settings, may differ from project to project local fileDefaultSize = '267x400px' local outputReferences = true ---Ссылки на используемые модули, которые потребуются в 99% случаев загрузки страниц (чтобы иметь на виду при переименовании) local moduleSources = require( 'Module:Sources' ) local WDS = require( 'Module:WikidataSelectors' ) ---Константы ---@type string local CONTENT_LANGUAGE_CODE = mw.language.getContentLanguage():getCode() local p = {} local g_config, g_frame local formatDatavalue, formatEntityId, formatRefs, formatSnak, formatStatement, formatStatementDefault, getSourcingCircumstances, getPropertyParams ---@param obj table ---@param target table ---@param skipEmpty boolean | nil ---@return table local function copyTo( obj, target, skipEmpty ) for key, val in pairs( obj ) do if skipEmpty ~= true or ( val ~= nil and val ~= '' ) then target[ key ] = val end end return target end ---@param prev number | nil ---@param next number | nil ---@return number | nil local function min( prev, next ) if prev == nil or prev > next then return next end return prev end ---@param prev number | nil ---@param next number | nil ---@return number | nil local function max( prev, next ) if prev == nil or prev < next then return next end return prev end ---@param section string ---@param code string ---@return any | nil local function getConfig( section, code ) if g_config == nil then g_config = require( 'Module:Wikidata/config' ) end if not g_config then g_config = {} end if not section then return g_config end if not code then return g_config[ section ] or {} end if not g_config[ section ] then return nil end return g_config[ section ][ code ] end ---@param code string ---@param sortKey string | nil ---@return string local function getCategoryByCode( code, sortKey ) local value = getConfig( 'categories', code ) if not value or value == '' then return '' end if sortKey ~= nil then return '[[Category:' .. value .. '|' .. sortKey .. ']]'; -- экранировать? else return '[[Category:' .. value .. ']]' end end ---@param isoStr string | table ---@return table | nil local function splitISO8601( isoStr ) if 'table' == type( isoStr ) then if isoStr.args and isoStr.args[ 1 ] then isoStr = '' .. isoStr.args[ 1 ] else return 'unknown argument type: ' .. type( isoStr ) .. ': ' .. table.tostring( isoStr ) end end local Y, M, D = ( function( str ) local pattern = "(%-?%d+)%-(%d+)%-(%d+)T" local _Y, _M, _D = mw.ustring.match( str, pattern ) return tonumber( _Y ), tonumber( _M ), tonumber( _D ) end )( isoStr ) local h, m, s = ( function( str ) local pattern = "T(%d+):(%d+):(%d+)%Z" local _H, _M, _S = mw.ustring.match( str, pattern ) return tonumber( _H ), tonumber( _M ), tonumber( _S ) end )( isoStr ) local oh, om = ( function( str ) if str:sub(-1) == "Z" then -- ends with Z, Zulu time return 0, 0 end -- matches ±hh:mm, ±hhmm or ±hh; else returns nils local pattern = "([-+])(%d%d):?(%d?%d?)$" local sign, oh, om = mw.ustring.match( str, pattern ) sign, oh, om = sign or "+", oh or "00", om or "00" return tonumber( sign .. oh ), tonumber( sign .. om ) end )( isoStr ) return { year=Y, month=M, day=D, hour=( h + oh ), min=( m + om ), sec=s } end ---@param time string ---@param precision number ---@return table | nil local function parseTimeBoundaries( time, precision ) local s = splitISO8601( time ) if not s then return nil end if precision >= 0 and precision <= 8 then local powers = { 1000000000 , 100000000, 10000000, 1000000, 100000, 10000, 1000, 100, 10 } local power = powers[ precision + 1 ] local left = s.year - ( s.year % power ) return { tonumber( os.time( { year=left, month=1, day=1, hour=0, min=0, sec=0 } ) ) * 1000, tonumber( os.time( { year=left + power - 1, month=12, day=31, hour=29, min=59, sec=58 } ) ) * 1000 + 1999 } end if precision == 9 then return { tonumber( os.time( { year=s.year, month=1, day=1, hour=0, min=0, sec=0} )) * 1000, tonumber( os.time( { year=s.year, month=12, day=31, hour=23, min=59, sec=58} )) * 1000 + 1999 } end if precision == 10 then local lastDays = { 31, 28.25, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } local lastDay = lastDays[ s.month ] return { tonumber( os.time( { year=s.year, month=s.month, day=1, hour=0, min=0, sec=0 } ) ) * 1000, tonumber( os.time( { year=s.year, month=s.month, day=lastDay, hour=23, min=59, sec=58 } ) ) * 1000 + 1999 } end if precision == 11 then return { tonumber( os.time( { year=s.year, month=s.month, day=s.day, hour=0, min=0, sec=0 } ) ) * 1000, tonumber( os.time( { year=s.year, month=s.month, day=s.day, hour=23, min=59, sec=58 } ) ) * 1000 + 1999 } end if precision == 12 then return { tonumber( os.time( { year=s.year, month=s.month, day=s.day, hour=s.hour, min=0, sec=0 } ) ) * 1000, tonumber( os.time( { year=s.year, month=s.month, day=s.day, hour=s.hour, min=59, sec=58 } ) ) * 1000 + 1999 } end if precision == 13 then return { tonumber( os.time( { year=s.year, month=s.month, day=s.day, hour=s.hour, min=s.min, sec=0 } ) ) * 1000, tonumber( os.time( { year=s.year, month=s.month, day=s.day, hour=s.hour, min=s.min, sec=58 } ) ) * 1000 + 1999 } end if precision == 14 then local t = tonumber( os.time( { year=s.year, month=s.month, day=s.day, hour=s.hour, min=s.min, sec=0 } ) ) return { t * 1000, t * 1000 + 999 } end error( 'Unsupported precision: ' .. precision ) end ---Функция для формирования категории на основе wikidata/config ---@param options table ---@param entityId string ---@return string local function extractCategory( options, entityId ) if not entityId or not options.category or options.nocat then return '' end if type( entityId ) ~= 'string' then entityId = entityId.id end local claims = WDS.load( entityId, options.category ) if not claims then return '' end for _, claim in pairs( claims ) do if claim and claim.mainsnak and claim.mainsnak.datavalue and claim.mainsnak.datavalue.type == 'wikibase-entityid' then local catEntityId = claim.mainsnak.datavalue.value.id local wbStatus, catSiteLink = pcall( mw.wikibase.getSitelink, catEntityId ) if wbStatus and catSiteLink then return '[[' .. catSiteLink .. ']]' end end end return '' end ---Преобразует строку в булевое значение ---@param valueToParse string ---@return boolean Преобразованное значение, если его удалось распознать, или defaultValue во всех остальных случаях local function toBoolean( valueToParse, defaultValue ) if valueToParse ~= nil then if valueToParse == false or valueToParse == '' or valueToParse == 'false' or valueToParse == '0' then return false end return true end return defaultValue end ---Обрачивает отформатированное значение в инлайновый или блочный тег. ---@param value string value ---@param attributes table of attributes ---@return string HTML tag with value local function wrapValue( value, attributes ) local tagName = 'span' local spacer = '' if string.match( value, '\n' ) or string.match( value, '<t[dhr][ >]' ) or string.match( value, '<div[ >]' ) or string.find( value, 'UNIQ%-%-imagemap' ) then tagName = 'div' spacer = '\n' end local attrString = '' for key, val in pairs( attributes or {} ) do local _key = mw.text.trim( key ) local _value = mw.text.encode( mw.text.trim( val ) ) attrString = attrString .. _key .. '="' .. _value .. '" ' end return '<' .. tagName .. ' ' .. attrString .. '>' .. spacer .. value .. '</' .. tagName .. '>' end ---Wraps formatted snak value into HTML tag with attributes. ---@param value string value of snak ---@param hash string ---@param attributes table of extra attributes ---@return string HTML tag with value local function wrapSnak( value, hash, attributes ) local newAttributes = mw.clone( attributes or {} ) newAttributes[ 'class' ] = ( newAttributes[ 'class' ] or '' ) .. ' wikidata-snak' if hash then newAttributes[ 'data-wikidata-hash'] = hash else newAttributes[ 'class' ] = newAttributes[ 'class' ] .. ' wikidata-main-snak' end return wrapValue( value, newAttributes ) end ---Wraps formatted statement value into HTML tag with attributes. ---@param value string value of statement ---@param propertyId string PID of property ---@param claimId string ID of claim or nil for local value ---@param attributes table of extra attributes ---@return string HTML tag with value local function wrapStatement( value, propertyId, claimId, attributes ) local newAttributes = mw.clone( attributes or {} ) newAttributes[ 'class' ] = newAttributes[ 'class' ] or '' newAttributes[ 'data-wikidata-property-id' ] = string.upper( propertyId ) if claimId then newAttributes[ 'class' ] = newAttributes[ 'class' ] .. ' wikidata-claim' newAttributes[ 'data-wikidata-claim-id' ] = claimId else newAttributes[ 'class' ] = newAttributes[ 'class' ] .. ' no-wikidata' end return wrapValue( value, newAttributes ) end ---Wraps formatted qualifier's statement value into HTML tag with attributes. ---@param value string value of qualifier's statement ---@param qualifierId string PID of qualifier ---@param attributes table of extra attributes ---@return string HTML tag with value local function wrapQualifier( value, qualifierId, attributes ) local newAttributes = mw.clone( attributes or {} ) newAttributes[ 'data-wikidata-qualifier-id' ] = string.upper( qualifierId ) return wrapValue( value, newAttributes ) end ---Функция для получения сущности (еntity) для текущей страницы ---Подробнее о сущностях см. d:Wikidata:Glossary/ru ---@param id string Идентификатор (типа P18, Q42) ---@return table Таблица, элементы которой индексируются с нуля local function getEntityFromId( id ) local entity local wbStatus if id then wbStatus, entity = pcall( mw.wikibase.getEntity, id ) else --- Удаляем из-за ошибки wbStatus, entity = pcall( mw.wikibase.getEntity ) end return entity end ---Внутренняя функция для формирования сообщения об ошибке ---@param key string Ключ элемента в таблице config.errors (например entity-not-found) ---@return void local function throwError( key ) error( getConfig( 'errors', key ) ) end ---Функция для получения идентификатора сущностей ---@param value table ---@return string local function getEntityIdFromValue( value ) local prefix = '' if value[ 'entity-type' ] == 'item' then prefix = 'Q' elseif value[ 'entity-type' ] == 'property' then prefix = 'P' else throwError( 'unknown-entity-type' ) end return prefix .. value[ 'numeric-id' ] end ---Проверка на наличие специализированной функции в опциях ---@param options table ---@param prefix string ---@return function local function getUserFunction( options, prefix, defaultFunction ) -- проверка на указание специализированных обработчиков в параметрах, -- переданных при вызове if options[ prefix .. '-module' ] or options[ prefix .. '-function' ] then -- проверка на пустые строки в параметрах или их отсутствие if not options[ prefix .. '-module' ] or not options[ prefix .. '-function' ] then throwError( 'unknown-' .. prefix .. '-module' ) end -- динамическая загруза модуля с обработчиком указанным в параметре local formatter = require( 'Module:' .. options[ prefix .. '-module' ] ) if formatter == nil then throwError( prefix .. '-module-not-found' ) end local fun = formatter[ options[ prefix .. '-function' ] ] if fun == nil then throwError( prefix .. '-function-not-found' ) end return fun end return defaultFunction end ---Выбирает свойства по property id, дополнительно фильтруя их по рангу ---@param context table ---@param options table ---@param propertySelector string ---@return table | nil local function selectClaims( context, options, propertySelector ) if not context then error( 'context not specified' ); end if not options then error( 'options not specified' ); end if not options.entityId then error( 'options.entity is missing' ); end if not propertySelector then error( 'propertySelector not specified' ); end local result = WDS.load( options.entityId, propertySelector ) if not result or #result == 0 then return nil end if options.limit and options.limit ~= '' and options.limit ~= '-' then local limit = tonumber( options.limit, 10 ) while #result > limit do table.remove( result ) end end return result end ---Функция для получения значения свойства элемента в заданный момент времени. ---@param entityId string ---@param boundaries table Временные границы ---@param propertyIds table<string> ---@param selectors table<string> ---@return table Таблица соответствующих значений свойства local function getPropertyInBoundaries( context, entityId, boundaries, propertyIds, selectors ) if type( entityId ) ~= 'string' then error( 'type of entityId argument expected string, but was ' .. type(entityId)); end local results = {} if not propertyIds or #propertyIds == 0 then return results end for i, propertyId in ipairs( propertyIds ) do local selector if selectors ~= nil then selector = selectors[ i ] or selectors[ propertyId ] or propertyId else selector = propertyId end local fakeAllClaims = {} fakeAllClaims[ propertyId ] = mw.wikibase.getAllStatements( entityId, propertyId ) local filteredClaims = WDS.filter( fakeAllClaims, selector .. '[rank:preferred, rank:normal]' ) if filteredClaims then for _, claim in pairs( filteredClaims ) do if not boundaries then table.insert( results, claim.mainsnak ) else local startBoundaries = p.getTimeBoundariesFromQualifier( context.frame, context, claim, 'P580' ) local endBoundaries = p.getTimeBoundariesFromQualifier( context.frame, context, claim, 'P582' ) if ( startBoundaries == nil or startBoundaries[ 2 ] <= boundaries[ 1 ] ) and ( endBoundaries == nil or endBoundaries[ 1 ] >= boundaries[ 2 ] ) then table.insert( results, claim.mainsnak ) end end end end if #results > 0 then break end end return results end ---@param context table ---@param statement table ---@param qualifierId string ---@return table | nil function p.getTimeBoundariesFromQualifier( _, context, statement, qualifierId ) -- only support exact date so far, but need improvement local left, right if statement.qualifiers and statement.qualifiers[ qualifierId ] then for _, qualifier in pairs( statement.qualifiers[ qualifierId ] ) do local boundaries = context.parseTimeBoundariesFromSnak( qualifier ) if not boundaries then return nil end left = min( left, boundaries[ 1 ] ) right = max( right, boundaries[ 2 ] ) end end if not left or not right then return nil end return { left, right } end ---@param frame table ---@param context table ---@param statement table ---@param qualifierIds table<string> ---@return table | nil function p.getTimeBoundariesFromQualifiers( frame, context, statement, qualifierIds ) if not qualifierIds then qualifierIds = { 'P582', 'P580', 'P585' } end for _, qualifierId in pairs( qualifierIds ) do local result = p.getTimeBoundariesFromQualifier( frame, context, statement, qualifierId ) if result then return result end end return nil end ---@type table<string> local getLabelWithLang_DEFAULT_PROPERTIES = { 'P1813', 'P1448', 'P1705' } ---@type table<string> local getLabelWithLang_DEFAULT_SELECTORS = { 'P1813[language:' .. CONTENT_LANGUAGE_CODE .. '][!P3831,P3831:Q105690470]', 'P1448[language:' .. CONTENT_LANGUAGE_CODE .. '][!P3831,P3831:Q105690470]', 'P1705[language:' .. CONTENT_LANGUAGE_CODE .. '][!P3831,P3831:Q105690470]' } ---Функция для получения метки элемента в заданный момент времени. ---@param context table ---@param options table ---@param entityId string ---@param boundaries table ---@param propertyIds table ---@param selectors table<string> ---@return string, string Текстовая метка элемента, язык метки local function getLabelWithLang( context, options, entityId, boundaries, propertyIds, selectors ) if type( entityId ) ~= 'string' then error( 'type of entityId argument expected string, but was ' .. type( entityId ) ); end if not entityId then return nil end local langCode = CONTENT_LANGUAGE_CODE -- name from label local label if options.text and options.text ~= '' then label = options.text else if not propertyIds then propertyIds = getLabelWithLang_DEFAULT_PROPERTIES selectors = getLabelWithLang_DEFAULT_SELECTORS end -- name from properties local results = getPropertyInBoundaries( context, entityId, boundaries, propertyIds, selectors ) for _, result in pairs( results ) do if result.datavalue and result.datavalue.value then if result.datavalue.type == 'monolingualtext' and result.datavalue.value.text then label = result.datavalue.value.text langCode = result.datavalue.value.language break elseif result.datavalue.type == 'string' then label = result.datavalue.value break end end end if not label then label, langCode = mw.wikibase.getLabelWithLang( entityId ) if not langCode then return nil end end end return label, langCode end ---@param context table ---@param options table ---@return string local function formatPropertyDefault( context, options ) if not context then error( 'context not specified' ); end if not options then error( 'options not specified' ); end if not options.entityId then error( 'options.entityId missing' ); end local claims if options.property then -- TODO: Почему тут может не быть property? if options.rank then -- передать настройки ранга из конфига claims = context.selectClaims( options, options.property .. options.rank ) else claims = context.selectClaims( options, options.property ) end end if claims == nil then return '' --TODO error? end -- Обход всех заявлений утверждения и с накоплением оформленных предпочтительных -- заявлений в таблице local formattedClaims = {} for _, claim in pairs( claims ) do local formattedStatement = context.formatStatement( options, claim ) -- здесь может вернуться либо оформленный текст заявления, либо строка ошибки, либо nil if formattedStatement and formattedStatement ~= '' then if not options.plain then formattedStatement = context.wrapStatement( formattedStatement, options.property, claim.id ) end table.insert( formattedClaims, formattedStatement ) end end -- создание текстовой строки со списком оформленых заявлений из таблицы local out = mw.text.listToText( formattedClaims, options.separator, options.conjunction ) if out ~= '' then if options.before then out = options.before .. out end if options.after then out = out .. options.after end end return out end ---Create context ---@param initOptions table ---@return table | nil local function initContext( initOptions ) local context = { entityId = initOptions.entityId, entity = initOptions.entity, extractCategory = extractCategory, formatSnak = formatSnak, formatPropertyDefault = formatPropertyDefault, formatStatementDefault = formatStatementDefault, getPropertyInBoundaries = getPropertyInBoundaries, getTimeBoundariesFromQualifier = p.getTimeBoundariesFromQualifier, getTimeBoundariesFromQualifiers = p.getTimeBoundariesFromQualifiers, wrapSnak = wrapSnak, wrapStatement = wrapStatement, wrapQualifier = wrapQualifier, } context.cloneOptions = function( options ) local entity = options.entity options.entity = nil local newOptions = mw.clone( options ) options.entity = entity newOptions.entity = entity newOptions.frame = options.frame; -- На склонированном фрейме frame:expandTemplate() return newOptions end context.formatProperty = function( options ) local func = getUserFunction( options, 'property', context.formatPropertyDefault ) return func( context, options ) end context.formatStatement = function( options, statement ) return formatStatement( context, options, statement ) end context.formatSnak = function( options, snak, circumstances ) return formatSnak( context, options, snak, circumstances ) end context.formatRefs = function( options, statement ) return formatRefs( context, options, statement ) end context.parseTimeFromSnak = function( snak ) if snak and snak.datavalue and snak.datavalue.value and snak.datavalue.value.time then return tonumber( os.time( splitISO8601( tostring( snak.datavalue.value.time ) ) ) ) * 1000 end return nil end context.parseTimeBoundariesFromSnak = function( snak ) if snak and snak.datavalue and snak.datavalue.value and snak.datavalue.value.time and snak.datavalue.value.precision then return parseTimeBoundaries( snak.datavalue.value.time, snak.datavalue.value.precision ) end return nil end context.getSourcingCircumstances = function( statement ) return getSourcingCircumstances( statement ) end context.selectClaims = function( options, propertyId ) return selectClaims( context, options, propertyId ) end return context end ---Функция для оформления утверждений (statement) ---Подробнее о утверждениях см. d:Wikidata:Glossary/ru ---@param options table ---@return string Formatted wikitext. local function formatProperty( options ) -- Получение сущности по идентификатору local entity = getEntityFromId( options.entityId ) if not entity then return -- throwError( 'entity-not-found' ) end -- проверка на присутсвие у сущности заявлений (claim) -- подробнее о заявлениях см. d:Викиданные:Глоссарий if not entity.claims then return '' --TODO error? end -- improve options options.frame = g_frame options.entity = entity options.extends = function( self, newOptions ) return copyTo( newOptions, copyTo( self, {} ) ) end if options.i18n then options.i18n = copyTo( options.i18n, copyTo( getConfig( 'i18n' ), {} ) ) else options.i18n = getConfig( 'i18n' ) end local context = initContext( options ) return context.formatProperty( options ) end ---Функция для оформления одного утверждения (statement) ---@param context table ---@param options table ---@param statement table ---@return string Formatted wikitext. function formatStatement( context, options, statement ) if not statement then error( 'statement is not specified or nil' ) end if not statement.type or statement.type ~= 'statement' then throwError( 'unknown-claim-type' ) end local functionToCall = getUserFunction( options, 'claim', context.formatStatementDefault ) return functionToCall( context, options, statement ) end ---@param statement table ---@return table function getSourcingCircumstances( statement ) if not statement then error( 'statement is not specified' ) end local circumstances = {} if statement.qualifiers and statement.qualifiers.P1480 then for _, qualifier in pairs( statement.qualifiers.P1480 ) do if qualifier and qualifier.datavalue and qualifier.datavalue.type == 'wikibase-entityid' and qualifier.datavalue.value and qualifier.datavalue.value[ 'entity-type'] == 'item' then table.insert( circumstances, qualifier.datavalue.value.id ) end end end return circumstances end ---Функция для оформления одного утверждения (statement) ---@param context table Context. ---@param options table Parameters. ---@param statement table ---@return string Formatted wikitext. function formatStatementDefault( context, options, statement ) if not context then error( 'context is not specified' ) end if not options then error( 'options is not specified' ) end if not statement then error( 'statement is not specified' ) end local circumstances = context.getSourcingCircumstances( statement ) options.qualifiers = statement.qualifiers local result = context.formatSnak( options, statement.mainsnak, circumstances ) if options.qualifier and statement.qualifiers and statement.qualifiers[ options.qualifier ] then local qualifierConfig = getPropertyParams( options.qualifier, nil, {} ) if options.i18n then qualifierConfig.i18n = options.i18n end if qualifierConfig.datatype == 'time' then qualifierConfig.nolinks = true end local qualifierValues = {} for _, qualifierSnak in pairs( statement.qualifiers[ options.qualifier ] ) do local snakValue = context.formatSnak( qualifierConfig, qualifierSnak ) if snakValue and snakValue ~= '' then table.insert( qualifierValues, snakValue ) end end if result and result ~= '' and #qualifierValues then if qualifierConfig.invisible then result = result .. table.concat( qualifierValues, ', ' ) else result = result .. ' (' .. table.concat( qualifierValues, ', ' ) .. ')' end end end if result and result ~= '' and options.references then result = result .. context.formatRefs( options, statement ) end return result end ---Функция для оформления части утверждения (snak) ---Подробнее о snak см. d:Викиданные:Глоссарий ---@param context table Context. ---@param options table Parameters. ---@param snak table ---@param circumstances table ---@return string Formatted wikitext. function formatSnak( context, options, snak, circumstances ) circumstances = circumstances or {} local result if snak.snaktype == 'somevalue' then if options[ 'somevalue' ] and options[ 'somevalue' ] ~= '' then result = options[ 'somevalue' ] else result = options.i18n[ 'somevalue' ] end elseif snak.snaktype == 'novalue' then if options[ 'novalue' ] and options[ 'novalue' ] ~= '' then result = options[ 'novalue' ] else result = options.i18n[ 'novalue' ] end elseif snak.snaktype == 'value' then result = formatDatavalue( context, options, snak.datavalue, snak.datatype ) for _, item in pairs( circumstances ) do if options.i18n[ item ] then result = options.i18n[ item ] .. result end end else throwError( 'unknown-snak-type' ) end if not result or result == '' then return nil end if options.plain then return result end return context.wrapSnak( result, snak.hash ) end ---Функция для оформления объектов-значений с географическими координатами ---@param value string Raw value. ---@param options table Parameters. ---@return string Formatted string. local function formatGlobeCoordinate( value, options ) -- проверка на требование в параметрах вызова на возврат сырого значения if options[ 'subvalue' ] == 'latitude' then -- широты return value[ 'latitude' ] elseif options[ 'subvalue' ] == 'longitude' then -- долготы return value[ 'longitude' ] elseif options[ 'nocoord' ] and options[ 'nocoord' ] ~= '' then -- если передан параметр nocoord, то не выводить координаты -- обычно это делается при использовании нескольких карточек на странице return '' else -- в противном случае формируются параметры для вызова шаблона {{coord}} -- нужно дописать в документации шаблона, что он отсюда вызывается, и что -- любое изменние его парамеров должно быть согласовано с кодом тут local coordModule = require( 'Module:Coordinates' ) local globe = options.globe or '' if globe == '' and value[ 'globe' ] then local globes = require( 'Module:Wikidata/Globes' ) globe = globes[ value[ 'globe' ] ] or '' end local display = 'inline' if options.display and options.display ~= '' then display = options.display elseif ( options.property:upper() == 'P625' ) then display = 'title' end local format = options.format or '' if format == '' then format = 'dms' if value[ 'precision' ] then local precision = value[ 'precision' ] * 60 if precision >= 60 then format = 'd' elseif precision >= 1 then format = 'dm' end end end g_frame.args = { tostring( value[ 'latitude' ] ), tostring( value[ 'longitude' ] ), globe = globe, type = options.type and options.type or '', scale = options.scale and options.scale or '', display = display, format = format, } return coordModule.coord(g_frame) end end ---Функция для оформления объектов-значений с файлами с Викисклада ---@param value string Raw value. ---@param options table Parameters. ---@return string Formatted string. local function formatCommonsMedia( value, options ) local image = value local caption = '' if options[ 'caption' ] and options[ 'caption' ] ~= '' then caption = options[ 'caption' ] end if caption ~= '' then caption = wrapQualifier( caption, 'P2096', { class = 'media-caption', style = 'display:block' } ) end if not string.find( value, '[%[%]%{%}]' ) and not string.find( value, 'UNIQ%-%-imagemap' ) then -- если в value не содержится викикод или imagemap, то викифицируем имя файла -- ищем слово imagemap в строке, потому что вставляется плейсхолдер: [[phab:T28213]] image = '[[File:' .. value .. '|frameless' if options[ 'border' ] and options[ 'border' ] ~= '' then image = image .. '|border' end local size = options[ 'size' ] if size and size ~= '' then -- TODO: check localized pixel names too if not string.match( size, 'px$' ) then size = size .. 'px' end else size = fileDefaultSize end image = image .. '|' .. size if options[ 'alt' ] and options[ 'alt' ] ~= '' then image = image .. '|alt=' .. options[ 'alt' ] end if caption ~= '' then image = image .. '|' .. caption end image = image .. ']]' if caption ~= '' then image = image .. '<br>' .. caption end else image = image .. caption .. getCategoryByCode( 'media-contains-markup' ) end return image end ---Function for render math formulas ---@param value string Value. ---@param options table Parameters. ---@return string Formatted string. local function formatMath( value, options ) return options.frame:extensionTag{ name = 'math', content = value } end ---Функция для оформления внешних идентификаторов ---@param value string ---@param options table ---@return string local function formatExternalId( value, options ) local formatter = options.formatter local propertyId = options.property:upper() if not formatter or formatter == '' then local isGoodFormat = false local wbStatus, formatRegexStatements = pcall( mw.wikibase.getBestStatements, propertyId, 'P1793' ) if wbStatus and formatRegexStatements then for _, statement in pairs( formatRegexStatements ) do if statement.mainsnak.snaktype == 'value' then local pattern = mw.ustring.gsub( statement.mainsnak.datavalue.value, '\\', '%' ) pattern = mw.ustring.gsub( pattern, '{%d+,?%d*}', '+' ) if ( string.find( pattern, '|' ) or string.find( pattern, '%)%?' ) or mw.ustring.match( value, '^' .. pattern .. '$' ) ~= nil ) then isGoodFormat = true break end end end end if isGoodFormat then local formatterStatements wbStatus, formatterStatements = pcall( mw.wikibase.getBestStatements, propertyId, 'P1630' ) if wbStatus and formatterStatements then for _, statement in pairs( formatterStatements ) do if statement.mainsnak.snaktype == 'value' then formatter = statement.mainsnak.datavalue.value break end end end end end if formatter and formatter ~= '' then local encodedValue = mw.ustring.gsub( value, '%%', '%%%%' ) -- ломается, если подставить внутрь другого mw.ustring.gsub local link = mw.ustring.gsub( mw.ustring.gsub( formatter, '$1', encodedValue ), '.', { [ ' ' ] = '%20', [ '+' ] = '%2b', [ '[' ] = '%5B', [ ']' ] = '%5D' } ) local title = options.title if not title or title == '' then title = '$1' end title = mw.ustring.gsub( mw.ustring.gsub( title, '$1', encodedValue ), '.', { [ '[' ] = '(', [ ']' ] = ')' } ) return '[' .. link .. ' ' .. title .. ']' end return value end ---Функция для оформления числовых значений ---@param value table Объект-значение ---@param options table Таблица параметров ---@return string Оформленный текст local function formatQuantity( value, options ) -- диапазон значений local amount = string.gsub( value.amount, '^%+', '' ) local lang = mw.language.getContentLanguage() local langCode = lang:getCode() local function formatNum( number, sigfig ) local multiplier = '' if options.countByThousands then local powers = options.i18n.thousandPowers local pos = 1 while math.abs( number ) >= 1000 and pos < #powers do number = number / 1000 pos = pos + 1 end multiplier = powers[ pos ] if math.abs( number ) >= 100 then sigfig = sigfig or 0 elseif math.abs( number ) >= 10 then sigfig = sigfig or 1 else sigfig = sigfig or 2 end else sigfig = sigfig or 12 -- округление до 12 знаков после запятой, на 13-м возникает ошибка в точности end local iMultiplier = 10^sigfig number = math.floor( number * iMultiplier + 0.5 ) / iMultiplier return string.gsub( lang:formatNum( number ), '^-', '−' ) .. multiplier end local out = formatNum( tonumber( amount ) ) if value.upperBound then local diff = tonumber( value.upperBound ) - tonumber( amount ) if diff > 0 then -- временная провека, пока у большинства значений не будет убрано ±0 -- Пробуем понять до какого знака округлять local integer, dot, decimals, _ = value.upperBound:match( '^+?-?(%d*)(%.?)(%d*)(.*)' ) local precision if dot == '' then precision = -integer:match( '0*$' ):len() else precision = #decimals end local bound = formatNum( diff, precision ) if string.match( bound, 'E%-(%d+)' ) then -- если в экспоненциальном формате local digits = tonumber( string.match( bound, 'E%-(%d+)' ) ) - 2 bound = formatNum( diff * 10 ^ digits, precision ) bound = string.sub( bound, 0, 2 ) .. string.rep( '0', digits ) .. string.sub( bound, -string.len( bound ) + 2 ) end out = out .. ' ± ' .. bound end end if options.unit and options.unit ~= '' then if options.unit ~= '-' then out = out .. ' ' .. options.unit end elseif value.unit and string.match( value.unit, 'http://www.wikidata.org/entity/' ) then local unitEntityId = string.gsub( value.unit, 'http://www.wikidata.org/entity/', '' ) if unitEntityId ~= 'undefined' then local wbStatus, unitEntity = pcall( mw.wikibase.getEntity, unitEntityId ) if wbStatus == true and unitEntity then if unitEntity.claims.P2370 and unitEntity.claims.P2370[ 1 ].mainsnak.snaktype == 'value' and not value.upperBound and options.siConversion == true then local conversionToSiUnit = string.gsub( unitEntity.claims.P2370[ 1 ].mainsnak.datavalue.value.amount, '^%+', '' ) if math.floor( math.log10( conversionToSiUnit ) ) ~= math.log10( conversionToSiUnit ) then -- Если не степени десятки (переводить сантиметры в метры не надо!) local outValue = tonumber( amount ) * conversionToSiUnit if outValue > 0 then -- Пробуем понять до какого знака округлять local integer, dot, decimals, _ = amount:match( '^(%d*)(%.?)(%d*)(.*)' ) local precision if dot == '' then precision = -integer:match( '0*$' ):len() else precision = #decimals end local adjust = math.log10( math.abs( conversionToSiUnit ) ) + math.log10( 2 ) local minPrecision = 1 - math.floor( math.log10( outValue ) + 2e-14 ) out = formatNum( outValue, math.max( math.floor( precision + adjust ), minPrecision ) ) else out = formatNum( outValue, 0 ) end unitEntityId = string.gsub( unitEntity.claims.P2370[ 1 ].mainsnak.datavalue.value.unit, 'http://www.wikidata.org/entity/', '' ) wbStatus, unitEntity = pcall( mw.wikibase.getEntity, unitEntityId ) end end local writingSystemElementId = 'Q8209' local langElementId = 'Q7737' local label = getLabelWithLang( context, options, unitEntity.id, nil, { "P5061", "P558", "P558" }, { 'P5061[language:' .. langCode .. ']', 'P558[P282:' .. writingSystemElementId .. ', P407:' .. langElementId .. ']', 'P558[!P282][!P407]' } ) out = out .. ' ' .. label end end end return out end ---Функция для оформления URL ---@param context table ---@param options table ---@param value string local function formatUrlValue( context, options, value ) if not options.length or options.length == '' then options.length = 25 end local moduleUrl = require( 'Module:URL' ) return moduleUrl.formatUrlSingle( context, options, value ) end local DATATYPE_CACHE = {} ---Get property datatype by ID. ---@param propertyId string Property ID, e.g. 'P123'. ---@return string Property datatype, e.g. 'commonsMedia', 'time' or 'url'. local function getPropertyDatatype( propertyId ) if not propertyId or not string.match( propertyId, '^P%d+$' ) then return nil end local cached = DATATYPE_CACHE[ propertyId ] if cached ~= nil then return cached end local wbStatus, propertyEntity = pcall( mw.wikibase.getEntity, propertyId ) if wbStatus ~= true or not propertyEntity then return nil end mw.log("Loaded datatype " .. propertyEntity.datatype .. " of " .. propertyId .. ' from wikidata, consider passing datatype argument to formatProperty call or to Wikidata/config' ) DATATYPE_CACHE[ propertyId ] = propertyEntity.datatype return propertyEntity.datatype end ---@param datavalue table ---@return function local function getPlainValueFunction( datavalue, _ ) if datavalue.type == 'wikibase-entityid' then return function( _, _, value ) return getEntityIdFromValue( value ) end elseif datavalue.type == 'string' then return function( _, _, value ) return value end elseif datavalue.type == 'monolingualtext' then return function( _, _, value ) return value.text end elseif datavalue.type == 'globecoordinate' then return function( _, _, value ) return value.latitude .. ',' .. value.longitude end elseif datavalue.type == 'quantity' then return function( _, _, value ) return value.amount end elseif datavalue.type == 'time' then return function( _, _, value ) return value.time end end throwError( 'unknown-datavalue-type' ) end ---@param datavalue table ---@param datatype string ---@return function local function getDefaultValueFunction( datavalue, datatype ) -- вызов обработчиков по умолчанию для известных типов значений if datavalue.type == 'wikibase-entityid' then -- Entity ID return function( context, options, value ) return formatEntityId( context, options, getEntityIdFromValue( value ) ) end elseif datavalue.type == 'string' then -- String if datatype and datatype == 'commonsMedia' then -- Media return function( _, options, value ) return formatCommonsMedia( value, options ) end elseif datatype and datatype == 'external-id' then -- External ID return function( _, options, value ) return formatExternalId( value, options ) end elseif datatype and datatype == 'math' then -- Math formula return function( _, options, value ) return formatMath( value, options ) end elseif datatype and datatype == 'url' then -- URL return formatUrlValue end return function( _, _, value ) return value end elseif datavalue.type == 'monolingualtext' then -- моноязычный текст (строка с указанием языка) return function( _, options, value ) if options.monolingualLangTemplate == 'lang' then if value.language == CONTENT_LANGUAGE_CODE then return value.text end return options.frame:expandTemplate{ title = 'lang-' .. value.language, args = { value.text } } elseif options.monolingualLangTemplate == 'ref' then return '<span class="lang" lang="' .. value.language .. '">' .. value.text .. '</span>' .. options.frame:expandTemplate{ title = 'ref-' .. value.language } else return '<span class="lang" lang="' .. value.language .. '">' .. value.text .. '</span>' end end elseif datavalue.type == 'globecoordinate' then -- географические координаты return function( _, options, value ) return formatGlobeCoordinate( value, options ) end elseif datavalue.type == 'quantity' then return function( _, options, value ) return formatQuantity( value, options ) end elseif datavalue.type == 'time' then return function( context, options, value ) local moduleDate = require( 'Module:Wikidata/date' ) return moduleDate.formatDate( context, options, value ) end end -- во всех стальных случаях возвращаем ошибку throwError( 'unknown-datavalue-type' ) end ---Функция для оформления значений (value) ---Подробнее о значениях см. d:Wikidata:Glossary/ru ---@param context table ---@param options table ---@param datavalue table ---@param datatype string ---@return string Оформленный текст function formatDatavalue( context, options, datavalue, datatype ) if not context then error( 'context not specified' ); end if not options then error( 'options not specified' ); end if not datavalue then error( 'datavalue not specified' ); end if not datavalue.value then error( 'datavalue.value is missing' ); end -- проверка на указание специализированных обработчиков в параметрах, -- переданных при вызове if options.plain then context.formatValueDefault = getPlainValueFunction( datavalue, datatype ) else context.formatValueDefault = getDefaultValueFunction( datavalue, datatype ) end local functionToCall = getUserFunction( options, 'value', context.formatValueDefault ) return functionToCall( context, options, datavalue.value ) end local DEFAULT_BOUNDARIES = { os.time() * 1000, os.time() * 1000} ---Функция для оформления идентификатора сущности ---@param context table ---@param options table ---@param entityId string ---@return string Оформленный текст function formatEntityId( context, options, entityId ) -- получение локализованного названия local boundaries if options.qualifiers then boundaries = p.getTimeBoundariesFromQualifiers( context.frame, context, { qualifiers = options.qualifiers } ) end if not boundaries then boundaries = DEFAULT_BOUNDARIES end local label, labelLanguageCode = getLabelWithLang( context, options, entityId, boundaries ) -- определение соответствующей показываемому элементу категории local category = context.extractCategory( options, { id = entityId } ) -- получение ссылки по идентификатору local link = mw.wikibase.sitelink( entityId ) if link then -- ссылка на категорию, а не добавление страницы в неё if mw.ustring.match( link, '^' .. mw.site.namespaces[ 14 ].name .. ':' ) then link = ':' .. link end if label and not options.rawArticle then if labelLanguageCode ~= CONTENT_LANGUAGE_CODE then label = '<span lang="' .. label .. '">' .. label .. '</span>' end local a = '[[' .. link .. '|' .. label .. ']]' if CONTENT_LANGUAGE_CODE ~= labelLanguageCode and 'mul' ~= labelLanguageCode then a = a .. getCategoryByCode( 'links-to-entities-with-missing-local-language-label' ) end return a .. category else return '[[' .. link .. ']]' .. category end end if label then -- TODO: возможно, лучше просто mw.wikibase.getLabel(entityId) -- красная ссылка -- TODO: разобраться, почему не всегда есть options.frame local title = mw.title.new( label ) if title and not title.exists and options.frame then local moduleRedLink = require( 'Module:Wikidata/redLink' ) local rawLabel = mw.wikibase.getLabel(entityId) or label -- без |text= и boundaries; or label - костыль local redLink = moduleRedLink.formatRedLinkWithInfobox(rawLabel, label, entityId) if CONTENT_LANGUAGE_CODE ~= labelLanguageCode and 'mul' ~= labelLanguageCode then redLink = '<span lang="' .. labelLanguageCode .. '">' .. redLink .. '</span>' .. getCategoryByCode( 'links-to-entities-with-missing-local-language-label' ) end return redLink .. '<sup>[[:d:' .. entityId .. '|[d]]]</sup>' .. category end -- TODO: перенести до проверки на существование статьи local sup = '' if ( not options.format or options.format ~= 'text' ) and entityId ~= 'Q6581072' and entityId ~= 'Q6581097' -- TODO: переписать на format=text then sup = '<sup class="plainlinks noprint">[//www.wikidata.org/wiki/' .. entityId .. '?uselang=' .. CONTENT_LANGUAGE_CODE .. ' [d]]</sup>' end -- одноимённая статья уже существует - выводится текст и ссылка на ВД return '<span class="iw" data-title="' .. label .. '">' .. label .. sup .. '</span>' .. category end -- сообщение об отсутвии локализованного названия -- not good, but better than nothing return '[[:d:' .. entityId .. '|' .. entityId .. ']]<span style="border-bottom: 1px dotted; cursor: help; white-space: nowrap" title="В Викиданных нет русской подписи к элементу. Вы можете помочь, указав русский вариант подписи.">?</span>' .. getCategoryByCode( 'links-to-entities-with-missing-label' ) .. category end ---Функция для оформления утверждений (statement) ---Подробнее о утверждениях см. d:Wikidata:Glossary/ru ---@deprecated Use p.formatProperty() instead ---@param frame table ---@return string Строка оформленного текста, предназначенная для отображения в статье function p.formatStatements( frame ) return p.formatProperty( frame ) end ---Получение параметров, которые обычно используются для вывода свойства. ---@param propertyId string ---@param datatype string ---@param params table function getPropertyParams( propertyId, datatype, params ) local config = getConfig() -- Различные уровни настройки параметров, по убыванию приоритета local propertyParams = {} -- 1. Параметры, указанные явно при вызове if params then for key, value in pairs( params ) do if value ~= '' then propertyParams[ key ] = value end end end if toBoolean( propertyParams.plain, false ) then propertyParams.separator = propertyParams.separator or ', ' propertyParams.conjunction = propertyParams.conjunction or ', ' else -- 2. Настройки конкретного параметра if config.properties and config.properties[ propertyId ] then for key, value in pairs( config.properties[ propertyId ] ) do if propertyParams[ key ] == nil then propertyParams[ key ] = value end end end -- 3. Указанный пресет настроек if propertyParams.preset and config.presets and config.presets[ propertyParams.preset ] then for key, value in pairs( config.presets[ propertyParams.preset ] ) do if propertyParams[ key ] == nil then propertyParams[ key ] = value end end end datatype = datatype or params.datatype or propertyParams.datatype or getPropertyDatatype( propertyId ) if propertyParams.datatype == nil then propertyParams.datatype = datatype end -- 4. Настройки для типа данных if datatype and config.datatypes and config.datatypes[ datatype ] then for key, value in pairs( config.datatypes[ datatype ] ) do if propertyParams[ key ] == nil then propertyParams[ key ] = value end end end -- 5. Общие настройки для всех свойств if config.global then for key, value in pairs( config.global ) do if propertyParams[ key ] == nil then propertyParams[ key ] = value end end end end return propertyParams end ---Функция для оформления утверждений (statement) ---Подробнее о утверждениях см. d:Wikidata:Glossary/ru ---@param frame table ---@return string Строка оформленного текста, предназначенная для отображения в статье function p.formatProperty( frame ) local args = copyTo( frame.args, {} ) -- проверка на отсутствие обязательного параметра property if not args.property then throwError( 'property-param-not-provided' ) end local override local propertyId = mw.language.getContentLanguage():ucfirst( string.gsub( args.property, '([^Pp0-9].*)$', function(w) if string.sub( w, 1, 1 ) == '~' then override = w end return '' end ) ) if override then args[ override:match( '[,~]([^=]*)=' ) ] = override:match( '=(.*)' ) args.property = propertyId end -- проброс всех параметров из шаблона {wikidata} и параметра from откуда угодно local p_frame = frame while p_frame do if p_frame:getTitle() == mw.site.namespaces[ 10 ].name .. ':Wikidata' then copyTo( p_frame.args, args, true ) end if p_frame.args and p_frame.args.from and p_frame.args.from ~= '' then args.entityId = p_frame.args.from else --- args.entityId = mw.wikibase.getEntityIdForCurrentPage() end p_frame = p_frame:getParent() end args = getPropertyParams( propertyId, nil, args ) local datatype = args.datatype -- перевод итоговых значений флагов в true/false и добавление значений -- по умолчанию только в том случае, если они нигде не были указаны ранее args.plain = toBoolean( args.plain, false ) args.nocat = not args.plain and toBoolean( args.nocat, false ) args.references = not args.plain and toBoolean( args.references, true ) -- если значение передано в параметрах вызова то выводим только его if args.value and args.value ~= '' then -- специальное значение для скрытия Викиданных if args.value == '-' then return '' end local value = args.value -- опция, запрещающая оформление значения, поэтому никак не трогаем if args.plain then return value end local context = initContext( args ) -- обработчики по типу значения local wrapperExtraArgs = {} if args[ 'value-module' ] and args[ 'value-function' ] and not string.find( value, '[%[%]%{%}]' ) then local func = getUserFunction( args, 'value' ) value = func( context, args, value ) elseif datatype == 'commonsMedia' then value = formatCommonsMedia( value, args ) elseif datatype == 'external-id' and not string.find( value, '[%[%]%{%}]' ) then wrapperExtraArgs[ 'data-wikidata-external-id' ] = mw.text.killMarkers( value ) value = formatExternalId( value, args ) --elseif datatype == 'math' then -- args.frame = frame -- костыль: в formatMath нужно frame:extensionTag -- value = formatMath( value, args ) elseif datatype == 'url' then value = formatUrlValue( context, args, value ) end -- оборачиваем в тег для JS-функций if string.match( propertyId, '^P%d+$' ) then value = mw.text.trim( value ) -- временная штрафная категория для исправления табличных вставок local allowTables = getPropertyParams( propertyId, nil, {} ).allowTables if not allowTables and string.match( value, '<t[dhr][ >]' ) -- and not string.match( value, '<table[ >]' ) -- and not string.match( value, '^%{%|' ) then value = value .. getCategoryByCode( 'value-contains-table', propertyId ) else value = wrapStatement( value, propertyId, nil, wrapperExtraArgs ) end end return value end -- ability to disable loading Wikidata if args.entityId == '-' then return '' end g_frame = frame -- после проверки всех аргументов -- вызов функции оформления для свойства (набора утверждений) return formatProperty( args ) end ---Функция проверки на присутствие источника в списке нерекомендованных. ---@param snaks table ---@return boolean local function isReferenceDeprecated( snaks ) if not snaks then return false end if snaks.P248 and snaks.P248[ 1 ] and snaks.P248[ 1 ].datavalue and snaks.P248[ 1 ].datavalue.value.id then local entityId = snaks.P248[ 1 ].datavalue.value.id if getConfig( 'deprecatedSources', entityId ) then return true end elseif snaks.P1433 and snaks.P1433[ 1 ] and snaks.P1433[ 1 ].datavalue and snaks.P1433[ 1 ].datavalue.value.id then local entityId = snaks.P1433[ 1 ].datavalue.value.id if getConfig( 'deprecatedSources', entityId ) then return true end end return false end ---Функция оформления ссылок на источники (reference) ---Подробнее о ссылках на источники см. d:Wikidata:Glossary/ru --- ---Экспортируется в качестве зарезервированной точки для вызова из функций-расширения вида claim-module/claim-function через context ---Вызов из других модулей напрямую осуществляться не должен (используйте frame:expandTemplate вместе с одним из специлизированных шаблонов вывода значения свойства). ---@param context table ---@param options table ---@param statement table ---@return string Оформленные примечания для отображения в статье function formatRefs( context, options, statement ) if not context then error( 'context not specified' ); end if not options then error( 'options not specified' ); end if not options.entityId then error( 'options.entityId missing' ); end if not statement then error( 'statement not specified' ); end if not outputReferences then return '' end ---@type string[] local references = {} if statement.references then local hasNotDeprecated = false local displayCount = 0 for _, reference in pairs( statement.references ) do if not isReferenceDeprecated( reference.snaks ) then hasNotDeprecated = true end end for _, reference in pairs( statement.references ) do local display = true if hasNotDeprecated then if isReferenceDeprecated( reference.snaks ) then display = false end end if displayCount >= 2 then if options.entityId and options.property then local propertyId = mw.ustring.match( options.property, '^[Pp][0-9]+' ) -- TODO: обрабатывать не тут, а раньше local moreReferences = '<sup>[[d:' .. options.entityId .. '#' .. string.upper( propertyId ) .. '|[…]]]</sup>' table.insert( references, moreReferences ) end break end if display == true then ---@type string local refText = moduleSources.renderReference( g_frame, options.entityId, reference ) if refText and refText ~= '' then table.insert( references, refText ) displayCount = displayCount + 1 end end end end return table.concat( references ) end return p