Module:Sources
Documentation for this module may be created at Module:Sources/doc
---@alias args table ---@alias frame { args: args, extensionTag: function, newChild: ( fun( args: args ): frame ) } ---@alias source { publication: source, [string]: any } ---@alias value: string | { id: string } ---@alias snak { datatype: string, snaktype: string, datavalue: { type: string, value: value } } ---@alias snaks table<string, table<number, snak>> ---@alias statement { mainsnak: snak, rank: string, qualifiers: snaks } ---@alias statements table<string, table<number, statement>> ---@alias map { name: string, ids: string[] }[]> ---@type table local p = {} ---@type table<string, string> local NORMATIVE_DOCUMENTS = { Q20754888 = 'Закон Российской Федерации', Q20754884 = 'Закон РСФСР', Q20873831 = 'Распоряжение Президента Российской Федерации', Q20873834 = 'Указ исполняющего обязанности Президента Российской Федерации', Q2061228 = 'Указ Президента Российской Федерации', } ---@type table<string, string> local LANG_CACHE = { Q150 = 'fr', Q188 = 'de', Q1321 = 'es', Q1860 = 'en', Q652 = 'it', Q7737 = 'ru', Q8798 = 'uk', } ---@type map local PROPERTY_MAP = { { name = 'sourceId', ids = { 'P248', 'P805' } }, { name = 'lang', ids = { 'P407', 'P364' } }, { name = 'author', ids = { 'P50', 'P2093' } }, { name = 'part', ids = { 'P958', 'P1810' } }, { name = 'title', ids = { 'P1476' } }, { name = 'subtitle', ids = { 'P1680' } }, { name = 'url', ids = { 'P953', 'P1065', 'P854', 'P973', 'P2699', 'P888' } }, { name = 'editor', ids = { 'P98' } }, { name = 'translator', ids = { 'P655' } }, { name = 'publication-id', ids = { 'P1433' } }, { name = 'edition', ids = { 'P393' } }, { name = 'publisher', ids = { 'P123' } }, { name = 'place', ids = { 'P291' } }, { name = 'volume', ids = { 'P478' } }, { name = 'issue', ids = { 'P433' } }, { name = 'dateOfCreation', ids = { 'P571' } }, { name = 'dateOfPublication', ids = { 'P577' } }, { name = 'pages', ids = { 'P304' } }, { name = 'numberOfPages', ids = { 'P1104' } }, { name = 'tirage', ids = { 'P1092' } }, { name = 'isbn', ids = { 'P212', 'P957' } }, { name = 'issn', ids = { 'P236' } }, -- { name = 'accessdate', ids = { 'P813' } }, -- disable, creates duplicate references { name = 'docNumber', ids = { 'P1545' } }, { name = 'type', ids = { 'P31' } }, { name = 'arxiv', ids = { 'P818' } }, { name = 'doi', ids = { 'P356' } }, { name = 'pmid', ids = { 'P698' } }, } -- table.insert( PROPERTY_MAP.url, 'P856' ) -- only as qualifier ---@type map local PUBLICATION_PROPERTY_MAP = mw.clone( PROPERTY_MAP ) ---@type string[] local monthGen = { 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря' } ---@type string local i18nDefaultLanguage = mw.language.getContentLanguage():getCode() p.i18nDefaultLanguage = i18nDefaultLanguage ---@type string local i18nEtAlDefault = ' et al.' ---@type table<string, string> local i18nEtAl = { ru = ' и др.', uk = ' та ін.', } ---@type table<string, string> local i18nEditors = { fr = '', de = 'Hrsg.: ', es = '', en = '', it = '', ru = 'под ред. ', uk = 'за ред. ', } ---@type table<string, string> local i18nTranslators = { fr = '', de = '', es = '', en = '', it = '', ru = 'пер. ', uk = 'пер. ', } ---@type table<string, string> local i18nVolume = { de = 'Vol.', fr = 'Vol.', es = 'Vol.', en = 'Vol.', it = 'Vol.', ru = 'Т.', uk = 'Т.', } ---@type table<string, string> local i18nIssue = { en = 'Iss.', ru = 'вып.', uk = 'вип.', } ---@type table<string, string> local i18nPages = { fr = 'P.', de = 'S.', es = 'P.', en = 'P.', it = 'P.', ru = 'С.', uk = 'С.', } ---@type table<string, string> local i18nNumberOfPages = { en = 'p.', ru = 'с.', } ---@type table<string, string> local i18nTirage = { en = 'ed. size: %d', ru = '%d экз.', } ---@param args args ---@return source local function getFilledArgs( args ) ---@type source local data = {} for key, value in pairs( args ) do if mw.text.trim( value ) ~= '' then if key == 1 then key = 'sourceId' end data[ key ] = mw.text.trim( value ) end end return data end ---Returns formatted pair {Family name(s), First name(s)} ---@param fullName string ---@return table<number, string> local function tokenizeName( fullName ) local space = '%s+' -- matches single or more spacing character local name = "(%a[%a%-']*)%.?" -- matches single name, have to start with letter, can contain apostrophe and hyphen, may end with dot local surname = "(%a[%a%-']*)" -- same as name, but can't end with dot local surnamePrefixes = { 'ван', 'van', 'де', 'de' } local nm, nm2, srn, srn2, pref fullName = ' ' .. fullName .. ' ' fullName = mw.ustring.gsub( fullName, ' оглы ', ' ' ) fullName = mw.text.trim( fullName ) -- Surname, Name local pattern = '^' .. surname .. ',' .. space .. name .. '$' srn, nm = mw.ustring.match( fullName, pattern ) if srn then return { srn, mw.ustring.sub( nm, 1, 1 ) .. '.' } end -- Surname, Name prefix for _, surnamePrefix in pairs( surnamePrefixes ) do pattern = '^' .. surname .. ',' .. space .. name .. space .. '(' .. surnamePrefix .. ')' .. '$' srn, nm, pref = mw.ustring.match( fullName, pattern ) if srn then return { mw.ustring.sub( pref ) .. ' ' .. srn, mw.ustring.sub( nm, 1, 1 ) .. '.' } end end -- Surname, Name Name pattern = '^' .. surname .. ',' .. space .. name .. space .. name .. '$' srn, nm, nm2 = mw.ustring.match( fullName, pattern ) if srn then return { srn, mw.ustring.sub( nm, 1, 1 ) .. '. ' .. mw.ustring.sub( nm2, 1, 1 ) .. '.' } end -- Surname Surname, Name pattern = '^' .. surname .. space .. surname .. ',' .. space .. name .. '$' srn, srn2, nm = mw.ustring.match( fullName, pattern ) if srn then return { srn .. ' ' .. srn2, mw.ustring.sub( nm, 1, 1 ) .. '.' } end -- Name Name Surname pattern = '^' .. name .. space .. name .. space .. surname .. '$' nm, nm2, srn = mw.ustring.match( fullName, pattern ) if srn then return { srn, mw.ustring.sub( nm, 1, 1 ) .. '. ' .. mw.ustring.sub( nm2, 1, 1 ) .. '.' } end -- Name Name prefix Surname for _, surnamePrefix in pairs( surnamePrefixes ) do pattern = '^' .. name .. space .. name .. space .. '(' .. surnamePrefix .. ')' .. space .. surname .. '$' nm, nm2, pref, srn = mw.ustring.match( fullName, pattern ) if srn then return { mw.ustring.sub( pref ) .. ' ' .. srn, mw.ustring.sub( nm, 1, 1 ) .. '. ' .. mw.ustring.sub( nm2, 1, 1 ) .. '.' } end end -- Surname, Name Name prefix for _, surnamePrefix in pairs( surnamePrefixes ) do pattern = '^' .. surname .. ',' .. space .. name .. space .. name .. space .. '(' .. surnamePrefix .. ')' .. '$' srn, nm, nm2, pref = mw.ustring.match( fullName, pattern ) if srn then return { mw.ustring.sub( pref ) .. ' ' .. srn, mw.ustring.sub( nm, 1, 1 ) .. '. ' .. mw.ustring.sub( nm2, 1, 1 ) .. '.' } end end -- Name{1,4} Surname for k = 1, 4 do pattern = '^' .. string.rep( name .. space, k ) .. surname .. '$' ---@type string[] local matched = { mw.ustring.match( fullName, pattern ) } if #matched ~= 0 then for j = 1, k do matched[ j ] = mw.ustring.sub( matched[ j ], 1, 1 ) end return { matched[ k + 1 ], table.concat( matched, '. ', 1, k ) .. '.' } end end -- Surname Name{1,4} for k = 1, 4 do pattern = '^' .. surname .. string.rep( space .. name, k ) .. '$' ---@type string[] local matched = { mw.ustring.match( fullName, pattern ) } if #matched ~= 0 then for j = 2, k + 1 do matched[ j ] = mw.ustring.sub( matched[ j ], 1, 1 ) end return { matched[ 1 ], table.concat( matched, '. ', 2, k + 1 ) .. '.' } end end return { fullName } end ---@param fullName string | nil ---@return string | nil local function personNameToAuthorName( fullName ) if not fullName then return nil end local tokenized = tokenizeName( fullName ) if #tokenized == 1 then return tokenized[ 1 ] end return tokenized[ 1 ] .. ' ' .. tokenized[ 2 ] end ---@param fullName string | nil ---@return string | nil local function personNameToResponsibleName( fullName ) if not fullName then return nil end local tokenized = tokenizeName( fullName ) if #tokenized == 1 then return tokenized[ 1 ] end return tokenized[ 2 ] .. ' ' .. tokenized[ 1 ] end ---@alias options { separator: string, conjunction: string, format: ( fun( data: string ): string ), nolinks: boolean, preferids: boolean, short: boolean } ---@type options local options_commas = { separator = ', ', conjunction = ', ', format = function( data ) return data end, nolinks = false, preferids = false, short = false, } ---@type options local options_commas_short = mw.clone( options_commas ) options_commas_short.short = true ---@type options local options_commas_it_short = mw.clone( options_commas_short ) options_commas_it_short.format = function( data ) return "''" .. data .. "''" end ---@type options local options_commas_nolinks = mw.clone( options_commas ) options_commas_nolinks.nolinks = true ---@type options local options_citetypes = { separator = ' ', conjunction = ' ', format = function( data ) return 'citetype_' .. data end, nolinks = true , preferids = true, short = false, } ---@type options local options_commas_authors = mw.clone( options_commas ) options_commas_authors.format = personNameToAuthorName ---@type options local options_commas_responsible = mw.clone( options_commas ) options_commas_responsible.format = personNameToResponsibleName ---@type options local options_ids = { separator = '; ', conjunction = '; ', format = function( id ) return id end, nolinks = true, preferids = false, short = false, } ---@type options local options_arxiv = mw.clone( options_ids ) options_arxiv.format = function( id ) return '[https://arxiv.org/abs/' .. id .. ' arXiv:' .. id .. ']' end ---@type options local options_doi = mw.clone( options_ids ) options_doi.format = function( doi ) return '[https://dx.doi.org/' .. doi .. ' doi:' .. doi .. ']' end ---@type options local options_issn = mw.clone( options_ids ) options_issn.format = function( issn ) return '[https://www.worldcat.org/issn/' .. issn .. ' ' .. issn .. ']' end ---@type options local options_pmid = mw.clone( options_ids ) options_pmid.format = function( pmid ) return '[https://www.ncbi.nlm.nih.gov/pubmed/?term=' .. pmid .. ' PMID:' .. pmid .. ']' end ---@param str string | nil ---@return boolean local function isEmpty( str ) return not str or #str == 0 end ---@param allQualifiers snaks ---@param qualifierPropertyId string ---@return string | nil local function getSingleStringQualifierValue( allQualifiers, qualifierPropertyId ) if not allQualifiers or not allQualifiers[ qualifierPropertyId ] then return nil end ---@type table<number, snak> local propertyQualifiers = allQualifiers[ qualifierPropertyId ] for _, qualifier in pairs( propertyQualifiers ) do if ( qualifier and qualifier.datatype == 'string' and qualifier.datavalue and qualifier.datavalue.type == 'string' and qualifier.datavalue.value ~= '' ) then return qualifier.datavalue.value end end return nil end ---@param data table ---@param resultProperty string ---@return void local function appendImpl_toTable( data, resultProperty ) if not data[ resultProperty ] then data[ resultProperty ] = {} elseif ( type( data[ resultProperty ] ) == 'string' or ( type( data[ resultProperty ] ) == 'table' and type( data[ resultProperty ].id ) == 'string' ) ) then data[ resultProperty ] = { data[ resultProperty ] } end end ---@param datavalue table ---@param qualifiers snaks ---@param data table ---@param propertyName string ---@param options table local function appendImpl( datavalue, qualifiers, data, propertyName, options ) data[ propertyName ] = data[ propertyName ] or {} if propertyName == 'issn' then table.insert( data[ propertyName ], datavalue.value ) elseif propertyName == 'url' or datavalue.type == 'url' then local value = datavalue.value if options.format then value = options.format( value ) end appendImpl_toTable( data, propertyName ) table.insert( data[ propertyName ], value ) elseif datavalue.type == 'string' then local value = getSingleStringQualifierValue( qualifiers, 'P1932' ) if not value then value = getSingleStringQualifierValue( qualifiers, 'P1810' ) end if not value then value = datavalue.value if options.format then value = options.format( value ) end end appendImpl_toTable(data, propertyName) local pos = getSingleStringQualifierValue( qualifiers, 'P1545' ) if pos then table.insert( data[ propertyName ], tonumber(pos), value ) else table.insert( data[ propertyName ], value ) end elseif datavalue.type == 'monolingualtext' then local value = datavalue.value.text if options.format then value = options.format( value ) end appendImpl_toTable( data, propertyName ) table.insert( data[ propertyName ], value ) elseif datavalue.type == 'quantity' then local value = datavalue.value.amount if ( mw.ustring.sub( value , 1, 1 ) == '+' ) then value = mw.ustring.sub( value , 2 ) end if options.format then value = options.format( value ) end appendImpl_toTable( data, propertyName ) table.insert( data[ propertyName ], value ) elseif datavalue.type == 'wikibase-entityid' then local pos = getSingleStringQualifierValue( qualifiers, 'P1545' ) local value = datavalue.value appendImpl_toTable(data, propertyName) local label = getSingleStringQualifierValue( qualifiers, 'P1932' ) if not label then label = getSingleStringQualifierValue( qualifiers, 'P1810' ) end local toInsert = { id = value.id, label = label } if pos and tonumber( pos ) then table.insert( data[ propertyName ], tonumber( pos ), toInsert ) else table.insert( data[ propertyName ], toInsert ) end elseif datavalue.type == 'time' then local value = datavalue.value if options.format then value = options.format( value ) end appendImpl_toTable( data, propertyName ) table.insert( data[ propertyName ], tostring( value.time ) ) end end ---@param entityId string ---@param propertyId string ---@return table<number, statement> local function getAllStatements( entityId, propertyId ) ---@type boolean, table<number, statement> local wdStatus, statements = pcall( mw.wikibase.getAllStatements, entityId, propertyId ) if wdStatus and statements then return statements end return {} end ---@param entityId string ---@param propertyId string ---@return table<number, statement> local function getBestStatements( entityId, propertyId ) ---@type boolean, table<number, statement> local wdStatus, statements = pcall( mw.wikibase.getBestStatements, entityId, propertyId ) if wdStatus and statements then return statements end return {} end ---@param entityId string ---@param projectToCheck string? ---@return string | nil local function getSitelink( entityId, projectToCheck ) ---@type boolean, string local wbStatus, sitelink if projectToCheck then wbStatus, sitelink = pcall( mw.wikibase.getSitelink, entityId, projectToCheck ) else wbStatus, sitelink = pcall( mw.wikibase.getSitelink, entityId ) end if not wbStatus then return nil end return sitelink end ---@param args any[] ---@return any | nil local function coalesce( args ) for _, arg in pairs( args ) do if not isEmpty( arg ) then return arg end end return nil end ---@param value any ---@return string | nil local function getSingle( value ) if type( value ) == 'string' then return tostring( value ) elseif type( value ) == 'table' then if value.id then return tostring( value.id ) end for _, tableValue in pairs( value ) do return getSingle( tableValue ) end end return nil end ---@param langEntityId string ---@return string | nil local function getLangCode( langEntityId ) if not langEntityId then return nil end langEntityId = getSingle( langEntityId ) if not string.match( langEntityId, '^Q%d+$' ) then return langEntityId end local cached = LANG_CACHE[ langEntityId ] if cached then if cached == '' then return nil end return cached end local claims = getBestStatements( langEntityId, 'P424' ) for _, claim in pairs( claims ) do if claim and claim.mainsnak and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value then LANG_CACHE[ langEntityId ] = claim.mainsnak.datavalue.value return claim.mainsnak.datavalue.value end end LANG_CACHE[ langEntityId ] = '' return nil end ---@param entityId string ---@param propertyId string ---@param data source ---@param propertyName string ---@param options table? ---@return void local function appendEntitySnaks( entityId, propertyId, data, propertyName, options ) options = options or {} -- do not populate twice if data[ propertyName ] and ( propertyName ~= 'author' or data[ propertyId ] ) then return end local statements = getBestStatements( entityId, propertyId ) if propertyName == 'author' then data[ propertyId ] = true end local lang = getLangCode( data.lang ) or i18nDefaultLanguage if propertyId == 'P1680' then -- if there is a default language for _, statement in pairs( statements ) do if statement and statement.mainsnak and statement.mainsnak.datavalue and statement.mainsnak.datavalue.value and statement.mainsnak.datavalue.value.language == lang then --found default language string appendImpl( statement.mainsnak.datavalue, statement.qualifiers, data, propertyName, options ) return end end end for _, statement in pairs( statements ) do if statement and statement.mainsnak and statement.mainsnak.datavalue then appendImpl( statement.mainsnak.datavalue, statement.qualifiers or {}, data, propertyName, options ) if propertyName == 'publication-id' and statement.qualifiers then data[ 'publication-qualifiers' ] = statement.qualifiers end end end end ---@param claims table<number, statement> ---@param qualifierPropertyId string ---@param result table ---@param resultPropertyId string ---@param options table ---@return void local function appendQualifiers( claims, qualifierPropertyId, result, resultPropertyId, options ) -- do not populate twice if not claims or result[ resultPropertyId ] then return end for _, claim in pairs( claims ) do if claim.qualifiers and claim.qualifiers[ qualifierPropertyId ] then ---@type table<number, snak> local propertyQualifiers = claim.qualifiers[ qualifierPropertyId ] for _, qualifier in pairs( propertyQualifiers ) do if qualifier and qualifier.datavalue then appendImpl( qualifier.datavalue, nil, result, resultPropertyId, options ) end end end end end ---@param entityId string ---@param propertyId string ---@param value any ---@return table<number, statement> local function findClaimsByValue( entityId, propertyId, value ) local result = {} local claims = getAllStatements( entityId, propertyId ) for _, claim in pairs( claims ) do if ( claim.mainsnak and claim.mainsnak.datavalue ) then local datavalue = claim.mainsnak.datavalue if ( datavalue.type == "string" and datavalue.value == value ) or ( datavalue.type == "wikibase-entityid" and datavalue.value[ "entity-type" ] == "item" and tostring( datavalue.value.id ) == value ) then table.insert( result, claim ) end end end return result end ---@param entityId string ---@param typeEntityId string ---@return boolean local function isInstanceOf( entityId, typeEntityId ) return findClaimsByValue( entityId, 'P31', typeEntityId )[ 1 ] ~= nil end ---@param entityId string ---@param typeEntityIds string[] ---@return string ---@todo Rewrite local function getFirstType( entityId, typeEntityIds ) for _, typeEntityId in pairs( typeEntityIds ) do if isInstanceOf( entityId, typeEntityId ) then return typeEntityId end end return nil end ---@param snaks snaks ---@param data source ---@param map map ---@return void local function populateDataFromSnaks( snaks, data, map ) for _, row in ipairs( map ) do local parameterName, propertyIds = row.name, row.ids for _, propertyId in pairs( propertyIds ) do if not data[ parameterName ] and snaks[ propertyId ] then local options = {} if propertyId == 'P888' then options = { format = function( id ) return 'http://www.jstor.org/stable/' .. id end } end for _, snak in pairs( snaks[ propertyId ] ) do if snak and snak.datavalue then appendImpl( snak.datavalue, {}, data, parameterName, options ) end end end end end end ---@param entityId string | nil ---@param data source ---@param map map ---@return void local function populateDataFromEntity( entityId, data, map ) if not data.title then if not isEmpty( entityId ) then local optionsAsLinks = { format = function( text ) return { id = entityId, label = text } end } appendEntitySnaks( entityId, 'P1476', data, 'title', optionsAsLinks ) else appendEntitySnaks( entityId, 'P1476', data, 'title', {} ) end appendEntitySnaks( entityId, 'P1680', data, 'subtitle', {} ) end local bookSeriesStatements = getBestStatements( entityId, 'P361' ) for _, statement in pairs( bookSeriesStatements ) do if statement and statement.mainsnak and statement.mainsnak.datavalue and statement.mainsnak.datavalue.value and statement.mainsnak.datavalue.value.id then local possibleBookSeriesEntityId = statement.mainsnak.datavalue.value.id if isInstanceOf( possibleBookSeriesEntityId, 'Q277759' ) then appendImpl_toTable( data, 'bookSeries' ) table.insert( data.bookSeries, { id = possibleBookSeriesEntityId } ) appendQualifiers( { statement }, 'P478', data, 'bookSeriesVolume', {} ) appendQualifiers( { statement }, 'P433', data, 'bookSeriesIssue', {} ) end end end for _, row in ipairs( map ) do local parameterName, propertyIds = row.name, row.ids for _, propertyId in pairs( propertyIds ) do local options = {} if propertyId == 'P888' then options = { format = function( id ) return 'http://www.jstor.org/stable/' .. id end } end appendEntitySnaks( entityId, propertyId, data, parameterName, options ) end end end ---@param data source ---@return void local function expandPublication( data ) if not data[ 'publication-id' ] then return end local publicationId = getSingle( data[ 'publication-id' ] ) data.publication = {} for key, value in pairs( data ) do if not string.match( key, '^publication-' ) then data.publication[ key ] = value end end data.publication.sourceId = publicationId data.publication.title = data[ 'publication-title' ] data.publication.subtitle = data[ 'publication-subtitle' ] if data[ 'publication-qualifiers' ] then populateDataFromSnaks( data[ 'publication-qualifiers' ], data.publication, PUBLICATION_PROPERTY_MAP ) end populateDataFromEntity( publicationId, data.publication, PUBLICATION_PROPERTY_MAP ) if type( data.publication.title ) == 'table' and data.publication.title[ 1 ] then data.publication.title = data.publication.title[ 1 ] end if type( data.publication.subtitle ) == 'table' and data.publication.subtitle[ 1 ] then data.publication.subtitle = data.publication.subtitle[ 1 ] end for key, value in pairs( data.publication ) do if key ~= 'sourceId' and key ~= 'title' and key ~= 'subtitle' and key ~= 'url' and not data[ key ] then data[ key ] = value end end end ---@param data source ---@return void local function expandBookSeries( data ) local bookSeries = data.bookSeries if not bookSeries then return end -- use only first one if type( bookSeries ) == 'table' and bookSeries[ 1 ] and bookSeries[ 1 ].id then data.bookSeries = bookSeries[ 1 ] bookSeries = data.bookSeries end if not bookSeries or not bookSeries.id then return end appendEntitySnaks( bookSeries.id, 'P123', data, 'publisher', {} ) appendEntitySnaks( bookSeries.id, 'P291', data, 'place', {} ) appendEntitySnaks( bookSeries.id, 'P236', data, 'issn', {} ) end ---@param entityId string ---@return string | nil local function getNormativeTitle( entityId ) local possibleTypeIds = {} for typeId, _ in pairs( NORMATIVE_DOCUMENTS ) do table.insert( possibleTypeIds, typeId ) end local foundTypeId = getFirstType( entityId, possibleTypeIds ) if foundTypeId then return NORMATIVE_DOCUMENTS[ foundTypeId ] end return nil end ---@param urls table<number, string> | string ---@param text string ---@return string local function wrapInUrl( urls, text ) local url = getSingle( urls ) if string.sub( url, 1, 1 ) == ':' then return '[[' .. url .. '|' .. text .. ']]' else return '[' .. url .. ' ' .. text .. ']' end end ---@param entityId string ---@param lang string ---@return string local function getElementLink( entityId, lang ) local sitelink = getSitelink( entityId, nil ) if sitelink then return ':' .. sitelink end if lang ~= 'mul' then -- link to entity in source language sitelink = getSitelink( entityId, lang .. 'wiki' ) if sitelink then return ':' .. lang .. ':' .. sitelink end end return ':d:' .. entityId end ---@param entityId string ---@param lang string ---@return string local function getLabel( entityId, lang ) local wbStatus, label = pcall( mw.wikibase.getLabelByLang, entityId, lang ) if not wbStatus then return '' end if label and label ~= '' then return label end wbStatus, label = pcall( mw.wikibase.getLabel, entityId ) if not wbStatus then return '' end return label or '' end ---@param lang string ---@param entityId string ---@param customTitle string ---@param options table local function renderLink( lang, entityId, customTitle, options ) if not entityId then error( 'entityId is not specified' ) end if type( entityId ) ~= 'string' then error( 'entityId is not string, but ' .. type( entityId ) ) end if type( customTitle or '' ) ~= 'string' then error( 'customTitle is not string, but ' .. type( customTitle ) ) end local title = customTitle -- ISO 4 if isEmpty( title ) then local propertyStatements = getBestStatements( entityId, 'P1160' ) for _, claim in pairs( propertyStatements ) do if ( claim and claim.mainsnak and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value and claim.mainsnak.datavalue.value.language == lang ) then title = claim.mainsnak.datavalue.value.text -- mw.log( 'Got title of ' .. entityId .. ' from ISO 4 claim: «' .. title .. '»' ) break end end end -- official name P1448 -- short name P1813 if isEmpty( title ) and options.short then local propertyStatements = getBestStatements( entityId, 'P1813' ) for _, claim in pairs( propertyStatements ) do if ( claim and claim.mainsnak and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value and claim.mainsnak.datavalue.value.language == lang ) then title = claim.mainsnak.datavalue.value.text -- mw.log( 'Got title of ' .. entityId .. ' from short name claim: «' .. title .. '» (' .. lang .. ')' ) break end end end -- person name P1559 -- labels if isEmpty( title ) then title = getLabel( entityId, lang ) -- mw.log( 'Got title of ' .. entityId .. ' from label: «' .. title .. '» (' .. lang .. ')' ) end local actualText = title or '\'\'(untranslated)\'\'' local link = getElementLink( entityId, lang ) return wrapInUrl( link, actualText ) end ---@param lang string ---@param value value ---@param options options ---@return string local function asString( lang, value, options ) if type( value ) == 'string' then return options.format( value ) end if type( value ) ~= 'table' then return options.format( '(unknown type)' ) end if value.id then -- this is link if type( value.label or '' ) ~= 'string' then mw.logObject( value, 'error value' ) error( 'label of table value is not string but ' .. type( value.label ) ) end local title if options.preferids then title = value.id elseif options.nolinks then title = value.label or getLabel( value.id, lang ) else title = renderLink( lang, value.id, value.label, options ) end if title == '' then title = "''(untranslated title)''" end return options.format( title ) end local resultList = {} for _, tableValue in pairs( value ) do table.insert( resultList, asString( lang, tableValue, options ) ) end return mw.text.listToText( resultList, options.separator, options.conjunction ) end ---@param entityId string ---@param data source ---@return source local function populateSourceDataImpl( entityId, data, map ) local wsLink = getSitelink( entityId, 'ruwikisource' ) if wsLink and not mw.ustring.gmatch( wsLink, 'Категория:' ) then data.url = ":ru:s:" .. wsLink end populateDataFromEntity( entityId, data, map ) local normativeTitle = getNormativeTitle( entityId ) if normativeTitle then local y, m, d = mw.ustring.match( getSingle( data.dateOfCreation ) , "(%-?%d+)%-(%d+)%-(%d+)T" ) y, m, d = tonumber( y ),tonumber( m ), tonumber( d ) local title = asString( 'ru', data.title, options_commas_nolinks ) local docNumber = getSingle( data.docNumber ) data.title = { normativeTitle .. " от " .. tostring( d ) .. " " .. monthGen[ m ] .. " " .. tostring( y ) .. " г." .. ( docNumber and ( " № " .. docNumber ) or '' ) .. ' «' .. title.. '»' } end if not data.title then local lang = getLangCode( data.lang ) or i18nDefaultLanguage local label = getLabel( entityId, lang ) if label ~= '' then data.title = { label } end end return data end ---@param entityId string ---@param propertyId string ---@param data source ---@return void local function expandSpecialsQualifiers( entityId, propertyId, data ) local statements = getBestStatements( entityId, propertyId ) for _, statement in pairs( statements ) do populateDataFromSnaks( statement.qualifiers or {}, data, PROPERTY_MAP ) end end ---Expand special types of references when additional data could be found in OTHER entity properties ---@param data source ---@return void local function expandSpecials( data ) if not data.entityId then return end if data.sourceId == 'Q36578' then -- Gemeinsame Normdatei -- specified by P227 appendEntitySnaks( data.entityId, 'P227', data, 'part', { format = function(gnd ) return 'Record #' .. gnd; end } ) appendEntitySnaks( data.entityId, 'P227', data, 'url', { format = function(gnd ) return 'http://d-nb.info/gnd/' .. gnd .. '/'; end } ) data.year = '2012—2016' expandSpecialsQualifiers( data.entityId, 'P227', data ) elseif data.sourceId == 'Q15222191' then -- BNF -- specified by P268 appendEntitySnaks( data.entityId, 'P268', data, 'part', { format = function(id ) return 'Record #' .. id; end } ) appendEntitySnaks( data.entityId, 'P268', data, 'url', { format = function(id ) return 'http://catalogue.bnf.fr/ark:/12148/cb' .. id; end } ) expandSpecialsQualifiers( data.entityId, 'P268', data ) elseif data.sourceId == 'Q54919' then -- VIAF -- specified by P214 appendEntitySnaks( data.entityId, 'P214', data, 'part', { format = function(id ) return 'Record #' .. id; end } ) appendEntitySnaks( data.entityId, 'P214', data, 'url', { format = function(id ) return 'https://viaf.org/viaf/' .. id; end } ) expandSpecialsQualifiers( data.entityId, 'P214', data ) else -- generic property search for _, sourceClaim in pairs( getBestStatements( data.sourceId, 'P1687' ) ) do if sourceClaim.mainsnak.snaktype == 'value' then local sourcePropertyId = sourceClaim.mainsnak.datavalue.value.id for _, sourcePropertyClaim in pairs( getBestStatements( sourcePropertyId, 'P1630' ) ) do if sourcePropertyClaim.mainsnak.snaktype == 'value' then appendEntitySnaks( data.entityId, sourcePropertyId, data, 'url', { format = function( id ) return mw.ustring.gsub( mw.ustring.gsub( sourcePropertyClaim.mainsnak.datavalue.value, '$1', id ), ' ', '%%20' ) end } ) expandSpecialsQualifiers( data.entityId, sourcePropertyId, data ) break end end end end end -- do we have appropriate record in P1433 ? local claims = findClaimsByValue( currentEntityId, 'P1343', data.sourceId ) if claims and #claims ~= 0 then for _, claim in pairs( claims ) do populateDataFromSnaks( claim.qualifiers, data, PROPERTY_MAP ) populateDataFromEntity( data.sourceId, data, PROPERTY_MAP ) end end end ---@param text string ---@param tip string ---@return string local function toTextWithTip( text, tip ) return '<span title="' .. tip .. '" style="border-bottom: 1px dotted; cursor: help; white-space: nowrap">' .. text .. '</span>' end ---@param lang string ---@param placeId string ---@return string local function getPlaceName( placeId, lang ) -- ГОСТ Р 7.0.12—2011 if lang == 'ru' then if placeId == 'Q649' then return toTextWithTip( 'М.', 'Москва' ); end if placeId == 'Q656' then return toTextWithTip( 'СПб.', 'Санкт-Петербург' ); end if placeId == 'Q891' then return toTextWithTip( 'Н. Новгород', 'Нижний Новгород' ); end if placeId == 'Q908' then return toTextWithTip( 'Ростов н/Д.', 'Ростов-на-Дону' ); end end return nil end ---@param data source ---@param lang string ---@return void local function preprocessPlace( data, lang ) if not data.place then return end ---@type table<number, string> local newPlace = {} for index, place in pairs( data.place ) do if place.id then local newPlaceStr = getPlaceName( place.id, lang ) if newPlaceStr then newPlace[ index ] = newPlaceStr else newPlace[ index ] = getLabel( place.id, lang ) end else newPlace[ index ] = place end end data.place = newPlace end ---@param entityId string ---@param lang string ---@param providedLabel string | nil ---@param options options ---@return string local function getPersonNameAsLabel( entityId, lang, providedLabel, options ) -- would custom label provided we don't need to check entity at all if not isEmpty( providedLabel ) then return options.format( providedLabel ) end if lang == 'mul' then lang = i18nDefaultLanguage end ---@type string | nil local personName = getLabel( entityId, lang ) if isEmpty( personName ) then return '\'\'(not translated to ' .. lang .. ')\'\'' end if not isInstanceOf( entityId, 'Q5' ) then return personName end return options.format( personName ) end ---@param entityId string ---@param lang string ---@param customLabel string | nil ---@param options options ---@return string local function getPersonNameAsWikitext( entityId, lang, customLabel, options ) local personName = getPersonNameAsLabel( entityId, lang, customLabel, options ) local link = getElementLink( entityId, lang ) return wrapInUrl( link, personName ) end ---@param value value ---@param lang string ---@param options options ---@return string local function getPeopleAsWikitext( value, lang, options ) if type( value ) == 'string' then return options.format( value ) elseif type( value ) == 'table' then if value.id then -- this is link if options.preferids then return tostring( value.id ) else if options.nolinks then return getPersonNameAsLabel( value.id, lang, value.label, options ) else return getPersonNameAsWikitext( value.id, lang, value.label, options ) end end end local maxAuthors = 10 -- need some restrictions, as some publications have enormous amount of authors (e.g. 115 authors of Q68951544) local resultList = {} for _, tableValue in pairs( value ) do local nextWikitext = getPeopleAsWikitext( tableValue, lang, options ) if not isEmpty( nextWikitext ) then table.insert( resultList, nextWikitext ) if #resultList == maxAuthors + 1 then -- keep one more to indicate that there are too many break end end end local resultWikitext = '' for i, wikitext in pairs( resultList ) do if i == maxAuthors + 1 then resultWikitext = resultWikitext .. ( i18nEtAl[ lang ] or i18nEtAlDefault ) break end if i ~= 1 then resultWikitext = resultWikitext .. ', ' end resultWikitext = resultWikitext .. wikitext end return resultWikitext end return '' -- options.format( '(unknown type)' ) end ---@param lang string ---@param data source ---@return string local function generateAuthorLinks( lang, data ) local result = '' if data.author then result = getPeopleAsWikitext( data.author, lang, options_commas_authors ) result = '<i class="wef_low_priority_links">' .. result .. '</i> ' end return result end ---@param lang string ---@param data source ---@param conjunction string ---@param propertyName string ---@param urlPropertyName string? ---@return string local function appendProperty( lang, data, conjunction, propertyName, urlPropertyName ) if not data[ propertyName ] then return '' end local out if urlPropertyName and data[ urlPropertyName ] then out = wrapInUrl( data[ urlPropertyName ], asString( lang, data[ propertyName ], options_commas_nolinks ) ) else out = asString( lang, data[ propertyName ], options_commas ) end if not out or out == '' then return '' end return conjunction .. out end ---@param lang string ---@param data source ---@return string local function appendTitle( lang, data ) local conjunction = '' local result = '' if data.part then result = result .. appendProperty( lang, data, '', 'part', 'parturl' ) conjunction = ' // ' end return result .. appendProperty( lang, data, conjunction, 'title', 'url' ) end ---@param lang string ---@return string local function appendLanguage( lang ) if lang == i18nDefaultLanguage then return '' end ---@type { getRefHtml: ( fun( lang: string ): string ), list_ref: ( fun( frame: frame ): string ) } local langs = require( 'Module:Languages' ) return langs.list_ref( p.currentFrame:newChild{ args = { lang } } ) end ---@param lang string ---@param data source ---@return string local function appendSubtitle( lang, data ) return appendProperty( lang, data, ': ', 'subtitle', nil ) end ---@param lang string ---@param data source ---@return string local function appendOriginalTitle( lang, data ) return appendProperty( lang, data, ' = ', 'originaltitle', nil ) end ---@param lang string ---@param data source ---@return string local function appendPublication( lang, data ) if not data.publication then return '' end local result = ' // ' .. asString( lang, data.publication.title, options_commas_it_short ) if data.publication.subtitle and data.publication.subtitle ~= '' then result = result .. ': ' .. asString( lang, data.publication.subtitle, options_commas_it_short ) end return result end ---@param lang string ---@param data source ---@return string local function appendEditor( lang, data ) if not data.editor and not data.translator then return '' end local result = ' / ' if data.editor then local prefix = i18nEditors[ lang ] or i18nEditors[ i18nDefaultLanguage ] result = result .. prefix .. getPeopleAsWikitext( data.editor, lang, options_commas_responsible ) if data.translator then result = result .. ', ' end end if data.translator then local prefix = i18nTranslators[ lang ] or i18nTranslators[ i18nDefaultLanguage ] result = result .. prefix .. getPeopleAsWikitext( data.translator, lang, options_commas_responsible ) end return result end ---@param lang string ---@param data source local function appendEdition( lang, data ) return appendProperty( lang, data, ' — ', 'edition', nil ) end ---@param lang string ---@param data source ---@return string local function appendPublicationData( lang, data ) if not data.place and not data.publisher and not data.year then return '' end local result = ' — ' if data.place then result = result .. asString( lang, data.place, options_commas_short ) if data.publisher or data.year then result = result .. ': ' end end if data.publisher then result = result .. asString( lang, data.publisher, options_commas_short ) if data.year then result = result .. ', ' end end if data.year then result = result .. asString( lang, data.year, options_commas ) end result = result .. '.' return result end ---@param lang string ---@param data source ---@return string local function appendVolumeAndIssue( lang, data ) if not data.volume and not data.issue then return '' end local result = ' — ' local letter_vol = i18nVolume[ lang ] or i18nVolume[ i18nDefaultLanguage ] local letter_iss = i18nIssue[ lang ] or i18nIssue[ i18nDefaultLanguage ] if data.volume then result = result .. appendProperty( lang, data, letter_vol .. ' ', 'volume', nil ) result = result ..appendProperty( lang, data, ', ' .. letter_iss .. ' ', 'issue', nil ) else result = result .. appendProperty( lang, data, letter_iss .. ' ', 'issue', nil ) end result = result .. '.' return result end ---@param lang string ---@param data source ---@return string local function appendPages( lang, data ) if not data.pages then return '' end local letter = i18nPages[ lang ] or i18nPages[ i18nDefaultLanguage ] local strPages = asString( lang, data.pages, options_commas ) strPages = mw.ustring.gsub( strPages, '[-—]', '—' ) return ' — ' .. letter .. ' ' .. strPages .. '.' end ---@param lang string ---@param data source ---@return string local function appendNumberOfPages( lang, data ) if not data.numberOfPages then return '' end local letter = i18nNumberOfPages[ lang ] or i18nNumberOfPages[ i18nDefaultLanguage ] return appendProperty( lang, data, ' — ', 'numberOfPages', nil ) .. ' ' .. letter end ---@param lang string ---@param data source ---@return string local function appendBookSeries( lang, data ) if not data.bookSeries then return '' end local result = appendProperty( lang, data, ' — (', 'bookSeries', nil ) if data.bookSeriesVolume or data.bookSeriesIssue then result = result .. '; ' local letter_vol = i18nVolume[ lang ] or i18nVolume[ i18nDefaultLanguage ] local letter_iss = i18nIssue[ lang ] or i18nIssue[ i18nDefaultLanguage ] if data.bookSeriesVolume then result = result .. appendProperty( lang, data, letter_vol .. ' ', 'bookSeriesVolume', nil ) result = result .. appendProperty( lang, data, ', ' .. letter_iss .. ' ', 'bookSeriesIssue', nil ) else result = result .. appendProperty( lang, data, letter_iss .. ' ', 'bookSeriesIssue', nil ) end end result = result .. ')' return result end ---@param lang string ---@param data source ---@return string local function appendTirage( lang, data ) if not data.tirage then return '' end local tirageTemplate = i18nTirage[ lang ] or i18nTirage[ i18nDefaultLanguage ] ---@type options local optionsTirage = { separator = '; ', conjunction = '; ', format = function( _data ) return tostring( mw.ustring.format( tirageTemplate, _data ) ) end, short = false, nolinks = false, preferids = false, } return ' — ' .. asString( lang, data.tirage, optionsTirage ) end ---@param lang string ---@param value string | nil ---@param options options ---@param prefix string? ---@return string local function appendIdentifier( lang, value, options, prefix ) if not value then return '' end return ' — ' .. ( prefix or '' ) .. asString( lang, value, options ) end ---@param result string ---@param lang string ---@param data source ---@return string local function wrapSourceId( result, lang, data ) if not data.sourceId then return result end local citeType = data.type and asString( lang, data.type, options_citetypes ) or 'citetype_unknown' return '<span class="wikidata_cite ' .. citeType .. '" data-entity-id="' .. data.sourceId .. '">' .. result .. '</span>' end ---@param data source ---@return string local function appendAccessDate( data ) if not data.accessdate then return '' end local date = getSingle( data.accessdate ) local pattern = "(%-?%d+)%-(%d+)%-(%d+)T" local y, m, d = mw.ustring.match( date, pattern ) y, m, d = tonumber( y ), tonumber( m ), tonumber( d ) local date_str = ( d > 0 and ' ' .. tostring( d ) or '' ) .. ( m > 0 and ' ' .. monthGen[ m ] or '' ) .. ( y > 0 and ' ' .. tostring( y ) or '' ) return " <small>Проверено" .. date_str .. ".</small>" end ---@param data source ---@param lang string ---@return void local function populateUrl( data, lang ) if data.sourceId and not data.url then local sitelink = getSitelink( data.sourceId, lang .. 'wikisource' ) if sitelink then data.url = ':' .. lang .. ':s:' .. sitelink end end end ---@param data source ---@return void local function populateYear( data ) if not data.year and data.dateOfPublication then local date = getSingle( data.dateOfPublication ) data.year = mw.ustring.sub( date, 2, 5 ) end if not data.year and data.dateOfCreation then local date = getSingle( data.dateOfCreation ) data.year = mw.ustring.sub( date, 2, 5 ) end end ---@param data source ---@return void local function populateTitle( data ) data.title = data.title or getSingle( data.url ) end ---@param data source ---@return string local function renderSource( data ) local lang = getLangCode( data.lang ) or i18nDefaultLanguage preprocessPlace( data, lang ) populateUrl( data, lang ) populateTitle( data ) if not data.title then return '' end populateYear( data ) local result = generateAuthorLinks( lang, data ) result = result .. appendTitle( lang, data ) result = result .. appendLanguage( lang ) result = result .. appendSubtitle( lang, data ) result = result .. appendOriginalTitle( lang, data ) result = result .. appendPublication( lang, data ) result = result .. '<span class="wef_low_priority_links">' result = result .. appendEditor( lang, data ) -- Might take current editor instead of actual. Use with caution result = result .. appendEdition( lang, data ) result = result .. appendPublicationData( lang, data ) result = result .. appendVolumeAndIssue( lang, data ) result = result .. appendPages( lang, data ) result = result .. appendNumberOfPages( lang, data ) result = result .. appendBookSeries( lang, data ) result = result .. appendTirage( lang, data ) result = result .. appendIdentifier( lang, data.isbn, options_commas, 'ISBN ' ) result = result .. appendIdentifier( lang, data.issn, options_issn, 'ISSN ' ) result = result .. appendIdentifier( lang, data.doi, options_doi, nil ) result = result .. appendIdentifier( lang, data.pmid, options_pmid, nil ) result = result .. appendIdentifier( lang, data.arxiv, options_arxiv, nil ) result = result .. appendAccessDate( data ) result = result .. '</span>' return wrapSourceId( result, lang, data ) end ---@param data source Данные в простом формате, согласованном с модулями формирования библиографического описания ---@param snaks snaks ---@return string | nil local function renderReferenceImpl( data, snaks ) -- не показывать источники с "импортировано из" if snaks.P143 then return nil end -- забрать данные из reference populateDataFromSnaks( snaks or {}, data, PROPERTY_MAP ) data.sourceId = getSingle( data.sourceId ) populateDataFromEntity( data.sourceId, data, PROPERTY_MAP ) expandSpecials( data ) populateSourceDataImpl( data.sourceId, data, PROPERTY_MAP ) expandPublication( data ) expandBookSeries( data ) if next( data ) == nil then return nil end local rendered = renderSource( data ) if mw.ustring.len( rendered ) == 0 then return nil end if data.ref then local anchorValue = 'CITEREF' .. data.ref .. ( coalesce( { data[ 'ref-year' ], data.year } ) or '' ) rendered = '<span class="citation" id="' .. mw.uri.anchorEncode( anchorValue ) .. '">' .. rendered .. '</span>' end return rendered end ---@param frame frame ---@param currentEntityId string | { id: string } ---@param reference table{ snaks: snaks } ---@return string | nil function p.renderSource( frame, currentEntityId, reference ) reference = reference or { snaks = {} } p.currentFrame = frame local data = getFilledArgs( frame.args or {} ) populateDataFromSnaks( reference.snaks, data, PROPERTY_MAP ) data.sourceId = getSingle( data.sourceId ) if type( currentEntityId ) == 'string' then data.entityId = currentEntityId elseif type( currentEntityId ) == 'table' and currentEntityId.id then data.entityId = currentEntityId.id end ---@type string local rendered = renderReferenceImpl( data, reference.snaks or {} ) if not rendered then return '' end return rendered end ---@param frame frame ---@param currentEntityId string ---@param reference table ---@return string function p.renderReference( frame, currentEntityId, reference ) local rendered = p.renderSource( frame, currentEntityId, reference ) if not rendered or rendered == '' then return '' end -- Про выбор алгоритма хеширования см. [[Модуль:Hash]]. Знак подчёркивания в начале позволяет -- исключить ошибку, когда имя сноски — чисто числовое значение, каковыми иногда бывают хеши. return frame:extensionTag( 'ref', rendered, { name = '_' .. mw.hash.hashValue( 'fnv164', rendered ) } ) .. '[[Category:Википедия:Статьи с источниками из Викиданных]]' end ---@param frame frame ---@return string | nil function p.testPersonNameToAuthorName( frame ) return personNameToAuthorName( frame.args[ 1 ] ) end ---@param frame frame ---@return string | nil function p.testPersonNameToResponsibleName( frame ) return personNameToResponsibleName( frame.args[ 1 ] ) end return p