м (1 версия импортирована)
м (1 версия импортирована)
 
(не показана 1 промежуточная версия 1 участника)
Строка 1: Строка 1:
---@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 = {}
local p = {}


local utils = require('Module:Sources/utils')
---@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()
local i18nDefaultLanguage = mw.language.getContentLanguage():getCode()
p.i18nDefaultLanguage = i18nDefaultLanguage


---@type string
local i18nEtAlDefault = ' et al.'
local i18nEtAlDefault = ' et al.'
---@type table<string, string>
local i18nEtAl = {
local i18nEtAl = {
ru = ' и др.',
    ru = ' и др.',
uk = ' та ін.',
    uk = ' та ін.',
}
}


---@type table<string, string>
local i18nEditors = {
local i18nEditors = {
fr = '',
    fr = '',
de = 'Hrsg.: ',
    de = 'Hrsg.: ',
es = '',
    es = '',
en = '',
    en = '',
it = '',
    it = '',
ru = 'под ред. ',
    ru = 'под ред. ',
uk = 'за ред. ',
    uk = 'за ред. ',
}
}


---@type table<string, string>
local i18nTranslators = {
local i18nTranslators = {
fr = '',
    fr = '',
de = '',
    de = '',
es = '',
    es = '',
en = '',
    en = '',
it = '',
    it = '',
ru = 'пер. ',
    ru = 'пер. ',
uk = 'пер. ',
    uk = 'пер. ',
}
}


---@type table<string, string>
local i18nVolume = {
local i18nVolume = {
     de  = 'Vol.',
     de  = 'Vol.',
fr = 'Vol.',
    fr = 'Vol.',
es = 'Vol.',
    es = 'Vol.',
en = 'Vol.',
    en = 'Vol.',
it = 'Vol.',
    it = 'Vol.',
ru = 'Т.',
    ru = 'Т.',
uk = 'Т.',
    uk = 'Т.',
}
}


---@type table<string, string>
local i18nIssue = {
local i18nIssue = {
en = 'Iss.',
    en = 'Iss.',
ru = 'вып.',
    ru = 'вып.',
uk = 'вип.',
    uk = 'вип.',
}
}


---@type table<string, string>
local i18nPages = {
local i18nPages = {
fr = 'P.',
    fr = 'P.',
de = 'S.',
    de = 'S.',
es = 'P.',
    es = 'P.',
en = 'P.',
    en = 'P.',
it = 'P.',
    it = 'P.',
ru = 'С.',
    ru = 'С.',
uk = 'С.',
    uk = 'С.',
}
}


---@type table<string, string>
local i18nNumberOfPages = {
local i18nNumberOfPages = {
en = 'p.',
    en = 'p.',
ru = 'с.',
    ru = 'с.',
}
}


---@type table<string, string>
local i18nTirage = {
local i18nTirage = {
en = 'ed. size: %d',
    en = 'ed. size: %d',
ru = '%d экз.',
    ru = '%d экз.',
}
}


---@param args args
---@return source
local function getFilledArgs( args )
    ---@type source
    local data = {}


local monthg = {'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'}
    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


local PREFIX_CITEREF = "CITEREF_";
    return data
end


-- Returns formatted pair {Family name(s), First name(s)}
---Returns formatted pair {Family name(s), First name(s)}
---@param fullName string
---@return table<number, string>
local function tokenizeName( fullName )
local function tokenizeName( fullName )
local start = '^%s*' -- matches beginning of the string + arbitrary number of spaces
    local space = '%s+' -- matches single or more spacing character
local finish = '%s*$' -- matches end of the string + arbitrary number of spaces
    local name = "(%a[%a%-']*)%.?" -- matches single name, have to start with letter, can contain apostrophe and hyphen, may end with dot
local comma = '\,%s+' -- matches comma + single or more spacing character
    local surname = "(%a[%a%-']*)" -- same as name, but can't end with dot
local space = '%s+' -- matches single or more spacing character
    local surnamePrefixes = { 'ван', 'van', 'де', 'de' }
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 nm, nm2, srn, srn2, pref
local f, i = mw.ustring.match(fullName, start .. surname .. comma .. name .. finish)
if f then
mw.log('tokenizeName: «' .. fullName .. '»: have «Fa, Im» match')
return {f, mw.ustring.sub( i, 1, 1 ) .. '.'}
end
local f, i, o = mw.ustring.match(fullName, start .. surname .. comma .. name .. space .. name .. finish)
if f then
mw.log( 'tokenizeName: «' .. fullName .. '»: have «Fa, Im Ot» match')
return {f, mw.ustring.sub( i, 1, 1 ) .. '.&nbsp;'
.. mw.ustring.sub( o, 1, 1 ) .. '.'}
end


local f1, f2, i = mw.ustring.match(fullName, start .. surname .. space .. surname .. comma .. name .. finish)
    fullName = ' ' .. fullName .. ' '
if f1 then
    fullName = mw.ustring.gsub( fullName, ' оглы ', ' ' )
mw.log('tokenizeName: «' .. fullName .. '»: have «Fa Fa, Im» match')
    fullName = mw.text.trim( fullName )
return {f1 .. '&nbsp;' .. f2, mw.ustring.sub( i, 1, 1 ) .. '.'}
end
local i, o, f = mw.ustring.match(fullName, start .. name .. space .. name .. space .. 'оглы' .. space .. surname .. finish)
if f then
mw.log('tokenizeName: «' .. fullName .. '»: have «Im Ot оглы Fa» match')
return {f, mw.ustring.sub(i, 1, 1) .. '.&nbsp;' .. mw.ustring.sub(o, 1, 1) .. '.'}
end


local i1, i2, f = mw.ustring.match(fullName, start .. name .. space .. name .. space .. 'de' .. space .. surname .. finish)
    -- Surname, Name
if f then
    local pattern = '^' .. surname .. ',' .. space .. name .. '$'
mw.log('tokenizeName: «' .. fullName .. '»: have «Im Im de Fa» match')
    srn, nm = mw.ustring.match( fullName, pattern )
return {f, mw.ustring.sub( i1, 1, 1 ) .. '.&nbsp;' .. mw.ustring.sub( i2, 1, 1 ) .. '.'}
    if srn then
end
        return {
            srn,
-- Try matching k names + surname
            mw.ustring.sub( nm, 1, 1 ) .. '.'
for k = 1, 4 do
        }
pattern = start .. string.rep(name .. space, k) .. surname .. finish
    end
matched = {mw.ustring.match(fullName, pattern)}
 
if #matched ~= 0 then
    -- Surname, Name prefix
mw.log('tokenizeName: «' .. fullName .. '»: have «Im (x' .. k .. ') Fa» match')
    for _, surnamePrefix in pairs( surnamePrefixes ) do
for i = 1, k do
        pattern = '^' .. surname .. ',' .. space .. name .. space .. '(' .. surnamePrefix .. ')' .. '$'
matched[i] = mw.ustring.sub(matched[i], 1, 1)
        srn, nm, pref = mw.ustring.match( fullName, pattern )
end
        if srn then
return {matched[k + 1], table.concat(matched, '.&nbsp;', 1, k) .. '.'}
            return {
end
                mw.ustring.sub( pref ) .. ' ' .. srn,
end
                mw.ustring.sub( nm, 1, 1 ) .. '.' }
        end
mw.log('Unmatched any pattern: «' .. fullName .. '»')
    end
return {fullName}
 
    -- 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 ) .. '.&nbsp;' .. 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 .. '&nbsp;' .. 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 ) .. '.&nbsp;' .. 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 ) .. '.&nbsp;' .. 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 ) .. '.&nbsp;' .. 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, '.&nbsp;', 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, '.&nbsp;', 2, k + 1 ) .. '.'
            }
        end
    end
 
    return { fullName }
end
end


---@param fullName string | nil
---@return string | nil
local function personNameToAuthorName( fullName )
local function personNameToAuthorName( fullName )
if not fullName then return fullName end
    if not fullName then
local tokenized = tokenizeName(fullName)
        return nil
if #tokenized == 1 then
    end
return tokenized[1]
 
else
    local tokenized = tokenizeName( fullName )
return tokenized[1] .. '&nbsp;' .. tokenized[2]
    if #tokenized == 1 then
end
        return tokenized[ 1 ]
    end
 
    return tokenized[ 1 ] .. '&nbsp;' .. tokenized[ 2 ]
end
end


---@param fullName string | nil
---@return string | nil
local function personNameToResponsibleName( fullName )
local function personNameToResponsibleName( fullName )
if not fullName then return fullName end
    if not fullName then
local tokenized = tokenizeName(fullName)
        return nil
if #tokenized == 1 then
    end
return tokenized[1]
 
else
    local tokenized = tokenizeName( fullName )
return tokenized[2] .. '&nbsp;' .. tokenized[1]
    if #tokenized == 1 then
end
        return tokenized[ 1 ]
    end
 
    return tokenized[ 2 ] .. '&nbsp;' .. tokenized[ 1 ]
end
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


local options_commas = { separator = ', ', conjunction = ', ', format = function( src ) return src end, nolinks = false, preferids = false };
---@type options
local options_commas_short = { separator = ', ', conjunction = ', ', format = function( src ) return src end, nolinks = false, preferids = false, short = true };
local options_commas_it_short = mw.clone( options_commas_short )
local options_commas_nolinks = { separator = ', ', conjunction = ', ', format = function( src ) return src end, nolinks = true, preferids = false };
options_commas_it_short.format = function( data ) return "''" .. data .. "''" end
local options_commas_it = { separator = ', ', conjunction = ', ', format = function( src ) return "''" .. src .. "''" end, nolinks = false, preferids = false };
local options_commas_it_short = { separator = ', ', conjunction = ', ', format = function( src ) return "''" .. src .. "''" end, nolinks = false, preferids = false, short = true };
local options_commas_it_nolinks = { separator = ', ', conjunction = ', ', format = function( src ) return "''" .. src .. "''" end, nolinks = true , preferids = false };
local options_citetypes = { separator = ' ', conjunction = ' ', format = function( src ) return 'citetype_' .. src end, nolinks = true , preferids = true };


local options_commas_authors = { separator = ', ', conjunction = ', ', format = personNameToAuthorName, nolinks = false, preferids = false };
---@type options
local options_commas_responsible = { separator = ', ', conjunction = ', ', format = personNameToResponsibleName, nolinks = false, preferids = false };
local options_commas_nolinks = mw.clone( options_commas )
options_commas_nolinks.nolinks = true


local options_arxiv = { separator = '; ', conjunction = '; ', format = function( id ) return '[http://arxiv.org/abs/' .. id .. ' arXiv:' .. id .. ']' end, nolinks = true, preferids = false };
---@type options
local options_doi = { separator = '; ', conjunction = '; ', format = function( doi ) return '[http://dx.doi.org/' .. doi .. ' doi:' .. doi .. ']' end, nolinks = true, preferids = false };
local options_citetypes = {
local options_issn = { separator = '; ', conjunction = '; ', format = function( issn ) return '[https://www.worldcat.org/issn/' .. issn .. ' ' .. issn .. ']' end, nolinks = true, preferids = false };
    separator = ' ',
local options_pmid = { separator = '; ', conjunction = '; ', format = function( pmid ) return '[https://www.ncbi.nlm.nih.gov/pubmed/?term=' .. pmid .. ' PMID:' .. pmid .. ']' end, nolinks = true, preferids = false };
    conjunction = ' ',
    format = function( data ) return 'citetype_' .. data end,
    nolinks = true ,
    preferids = true,
    short = false,
}


local function getPersonNameAsLabel( context, entityId, providedLabel, options )
---@type options
-- would custom label provided we don't need to check entity at all
local options_commas_authors = mw.clone( options_commas )
if ( not utils.isEmpty( providedLabel ) ) then
options_commas_authors.format = personNameToAuthorName
mw.log( 'Custom label provided for ' .. entityId );
return options.format( providedLabel );
end


local entity = utils.getEntity( context, entityId );
---@type options
if ( not entity ) then return '\'\'(entity ' .. entityId .. ' is missing)\'\'' end;
local options_commas_responsible = mw.clone( options_commas )
options_commas_responsible.format = personNameToResponsibleName


local lang = context.lang
---@type options
if lang == 'mul' then
local options_ids = {
lang = i18nDefaultLanguage
    separator = '; ',
end
    conjunction = '; ',
local personName = nil;
    format = function( id ) return id end,
-- support only labels so far
    nolinks = true,
if ( entity.labels[ lang ] ) then
    preferids = false,
personName = entity.labels[ lang ].value;
    short = false,
mw.log('Got person name of ' .. entityId .. ' from label: «' .. personName .. '»' )
}
end


if ( not utils.isInstanceOf( entity, 'Q5' ) ) then
---@type options
mw.log( 'Entity ' .. entityId .. ' is not a person' );
local options_arxiv = mw.clone( options_ids )
return personName;
options_arxiv.format = function( id ) return '[https://arxiv.org/abs/' .. id .. ' arXiv:' .. id .. ']' end
end


if ( utils.isEmpty( personName ) ) then
---@type options
return '\'\'(not translated to ' .. lang .. ')\'\'';
local options_doi = mw.clone( options_ids )
else
options_doi.format = function( doi ) return '[https://dx.doi.org/' .. doi .. ' doi:' .. doi .. ']' end
return options.format( personName );
 
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
end


local function getPersonNameAsWikitext( context, entityId, customLabel, options )
---@param allQualifiers snaks
local personName = getPersonNameAsLabel( context, entityId, customLabel, options);
---@param qualifierPropertyId string
if ( personName == nil ) then
---@return string | nil
return nil;
local function getSingleStringQualifierValue( allQualifiers, qualifierPropertyId )
end
    if not allQualifiers or not allQualifiers[ qualifierPropertyId ] then
        return nil
    end


local link = utils.getElementLink( context, entityId, nil );
    ---@type table<number, snak>
return utils.wrapInUrl( link, personName );
    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
end


local function getPeopleAsWikitext( context, value, options )
---@param data table
if type( value ) == 'string' then
---@param resultProperty string
return options.format( value )
---@return void
elseif type( value ) == 'table' then
local function appendImpl_toTable( data, resultProperty )
if value.id then
    if not data[ resultProperty ] then
-- this is link
        data[ resultProperty ] = {}
if options.preferids then
    elseif ( type( data[ resultProperty ] ) == 'string' or ( type( data[ resultProperty ] ) == 'table' and type( data[ resultProperty ].id ) == 'string' ) ) then
return value.id
        data[ resultProperty ] = { data[ resultProperty ] }
else
    end
if options.nolinks then
end
return getPersonNameAsLabel( context, value.id, value.label, options )
else
return getPersonNameAsWikitext( context, value.id, 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 i, tableValue in pairs( value ) do
local nextWikitext = getPeopleAsWikitext( context, tableValue, options )
if not utils.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 = ''
---@param datavalue table
for i, wikitext in pairs( resultList ) do
---@param qualifiers snaks
if i == maxAuthors + 1 then
---@param data table
resultWikitext = resultWikitext .. ( i18nEtAl[ context.lang ] or i18nEtAlDefault )
---@param propertyName string
break;
---@param options table
end
local function appendImpl( datavalue, qualifiers, data, propertyName, options )
if i ~= 1 then
    data[ propertyName ] = data[ propertyName ] or {}
resultWikitext = resultWikitext .. ', '
    if propertyName == 'issn' then
end
        table.insert( data[ propertyName ], datavalue.value )
resultWikitext = resultWikitext .. wikitext
    elseif propertyName == 'url' or datavalue.type == 'url' then
end
        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


return resultWikitext
        if not value then
end
            value = datavalue.value
            if options.format then
                value = options.format( value )
            end
        end


return options.format( '(unknown type)' )
        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
end


local function generateAuthorLinks(context, src)
---@param entityId string
local result = ''
---@param propertyId string
if src.author then
---@return table<number, statement>
result = getPeopleAsWikitext( context, src.author, options_commas_authors )
local function getAllStatements( entityId, propertyId )
result = '<i class="wef_low_priority_links">' .. result .. '</i> '
    ---@type boolean, table<number, statement>
end
    local wdStatus, statements = pcall( mw.wikibase.getAllStatements, entityId, propertyId )
return result
    if wdStatus and statements then
        return statements
    end
 
    return {}
end
end


local function appendProperty(result, context, src, conjunctor, property, url)
---@param entityId string
if src[property] then
---@param propertyId string
if url and src[url] then
---@return table<number, statement>
result = result .. conjunctor .. utils.wrapInUrl( src[url], utils.toString( context, src[property], options_commas_nolinks ) )
local function getBestStatements( entityId, propertyId )
else
    ---@type boolean, table<number, statement>
result = result .. conjunctor .. utils.toString( context, src[property], options_commas )
    local wdStatus, statements = pcall( mw.wikibase.getBestStatements, entityId, propertyId )
end
    if wdStatus and statements then
end
        return statements
return result
    end
 
    return {}
end
end


local function appendTitle(result, context, src)
---@param entityId string
conjunctor = ''
---@param projectToCheck string?
if src.part then
---@return string | nil
result = appendProperty(result, context, src, '', 'part', 'parturl')
local function getSitelink( entityId, projectToCheck )
conjunctor = ' // '
    ---@type boolean, string
end
    local wbStatus, sitelink
result = appendProperty(result, context, src, conjunctor, 'title', 'url')
 
return result
    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
end


local function appendLanguage(result, context, src)
---@param args any[]
if context.lang ~= i18nDefaultLanguage then
---@return any | nil
local langs = require('Module:Languages')
local function coalesce( args )
result = result .. langs.list_ref(p.currentFrame:newChild{ args = {context.lang} })
    for _, arg in pairs( args ) do
end
        if not isEmpty( arg ) then return arg end
return result
    end
    return nil
end
end


local function appendSubtitle(result, context, src)
---@param value any
return appendProperty(result, context, src, ': ', 'subtitle')
---@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
end


local function appendOriginalTitle(result, context, src)
---@param langEntityId string
return appendProperty(result, context, src, ' = ', 'originaltitle')
---@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
end


local function appendPublication(result, context, src)
---@param entityId string
if src.publication then
---@param propertyId string
if type( src.publication.title or '') ~= 'string' then
---@param data source
error('type of src.publication.title is not string but ' .. type( src.publication.title ) )
---@param propertyName string
end
---@param options table?
---@return void
result = result .. ' // ' .. utils.toString( context, src.publication, options_commas_it_short )
local function appendEntitySnaks( entityId, propertyId, data, propertyName, options )
if src.publication.subtitle then
    options = options or {}
result = result .. ': ' .. utils.toString( context, src.publication.subtitle, options_commas_it_short )
 
end
    -- do not populate twice
end
    if data[ propertyName ] and ( propertyName ~= 'author' or data[ propertyId ] ) then
return result
        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
end


local function appendEditor(result, context, src)
---@param claims table<number, statement>
if src.editor or src.translator then
---@param qualifierPropertyId string
result = result .. ' / '
---@param result table
if src.editor then
---@param resultPropertyId string
local prefix = i18nEditors[ context.lang ] or i18nEditors[ i18nDefaultLanguage ]
---@param options table
result = result .. prefix .. getPeopleAsWikitext( context, src.editor, options_commas_responsible )
---@return void
if src.translator then
local function appendQualifiers( claims, qualifierPropertyId, result, resultPropertyId, options )
result = result .. ', '
    -- do not populate twice
end
    if not claims or result[ resultPropertyId ] then
end
        return
if src.translator then
    end
local prefix = i18nTranslators[ context.lang ] or i18nTranslators[ i18nDefaultLanguage ]
 
result = result .. prefix .. getPeopleAsWikitext( context, src.translator, options_commas_responsible )
    for _, claim in pairs( claims ) do
end
        if claim.qualifiers and claim.qualifiers[ qualifierPropertyId ] then
end
            ---@type table<number, snak>
return result
            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
end


local function appendEdition(result, context, src)
---@param entityId string
return appendProperty(result, context, src, ' — ', 'edition')
---@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
end


local function appendPublicationData(result, context, src)
---@param entityId string
if src.place or src.publisher or src.year then
---@param typeEntityId string
result = result .. ' — '
---@return boolean
if src.place then
local function isInstanceOf( entityId, typeEntityId )
result = result .. utils.toString( context, src.place, options_commas_short )
    return findClaimsByValue( entityId, 'P31', typeEntityId )[ 1 ] ~= nil
if src.publisher or src.year then
result = result .. ': '
end
end
if src.publisher then
result = result .. utils.toString( context, src.publisher, options_commas_short )
if src.year then
result = result .. ', '
end
end
if src.year then
result = result .. utils.toString( context, src.year, options_commas )
end
result = result .. '.';
end
return result
end
end


local function appendVolumeAndIssue(result, context, src)
---@param entityId string
if src.volume or src.issue then
---@param typeEntityIds string[]
result = result .. ' — '
---@return string
local letter_vol = i18nVolume[ context.lang ] or i18nVolume[ i18nDefaultLanguage ]
---@todo Rewrite
local letter_iss = i18nIssue[ context.lang ] or i18nIssue[ i18nDefaultLanguage ]
local function getFirstType( entityId, typeEntityIds )
if src.volume then
    for _, typeEntityId in pairs( typeEntityIds ) do
result = appendProperty(result, context, src, letter_vol .. '&nbsp;', 'volume')
        if isInstanceOf( entityId, typeEntityId ) then
result = appendProperty(result, context, src, ', ' .. letter_iss .. '&nbsp;', 'issue')
            return typeEntityId
else
        end
result = appendProperty(result, context, src, letter_iss .. '&nbsp;', 'issue')
    end
end
 
result = result .. '.'
    return nil
end
return result
end
end


local function appendPages(result, context, src)
---@param snaks snaks
if src.pages then
---@param data source
local letter = i18nPages[ context.lang ] or i18nPages[ i18nDefaultLanguage ]
---@param map map
local strPages = utils.toString( context, src.pages, options_commas )
---@return void
strPages = mw.ustring.gsub( strPages, '[-—]', '—' );
local function populateDataFromSnaks( snaks, data, map )
result = result .. ' — ' .. letter .. '&nbsp;' .. strPages .. '.'
    for _, row in ipairs( map ) do
end
        local parameterName, propertyIds = row.name, row.ids
return result
        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
end


local function appendNumberOfPages(result, context, src)
---@param entityId string | nil
if src.numberOfPages then
---@param data source
local letter = i18nNumberOfPages[ context.lang ] or i18nNumberOfPages[ i18nDefaultLanguage ]
---@param map map
result = appendProperty(result, context, src, ' ', 'numberOfPages') .. '&nbsp;' .. letter
---@return void
end
local function populateDataFromEntity( entityId, data, map )
return result
    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
end


local function appendBookSeries(result, context, src)
---@param data source
if src.bookSeries then
---@return void
result = appendProperty(result, context, src, ' — (', 'bookSeries')
local function expandPublication( data )
if src.bookSeriesVolume or src.bookSeriesIssue then
    if not data[ 'publication-id' ] then
result = result .. '; '
        return
local letter_vol = i18nVolume[ context.lang ] or i18nVolume[ i18nDefaultLanguage ]
    end
local letter_iss = i18nIssue[ context.lang ] or i18nIssue[ i18nDefaultLanguage ]
 
if ( src.bookSeriesVolume ) then
    local publicationId = getSingle( data[ 'publication-id' ] )
result = appendProperty(result, context, src, letter_vol .. '&nbsp;', 'bookSeriesVolume')
    data.publication = {}
result = appendProperty(result, context, src, ', ' .. letter_iss .. '&nbsp;', 'bookSeriesIssue')
    for key, value in pairs( data ) do
else
        if not string.match( key, '^publication-' ) then
result = appendProperty(result, context, src, letter_iss .. '&nbsp;', 'bookSeriesIssue')
            data.publication[ key ] = value
end
        end
end
    end
result = result .. ')'
    data.publication.sourceId = publicationId
end
    data.publication.title = data[ 'publication-title' ]
return result
    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
end


local function appendTirage(result, context, src)
---@param data source
if src.tirage then
---@return void
local tirageTemplate = i18nTirage[ context.lang ] or i18nTirage[ i18nDefaultLanguage ]
local function expandBookSeries( data )
result = result .. ' ' .. utils.toString( context, src.tirage, { separator = '; ', conjunction = '; ', format = function( data ) return mw.ustring.format(tirageTemplate, data) end } )
    local bookSeries = data.bookSeries
end
    if not bookSeries then
return result
        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
end


local function appendIdentifiers(result, context, src)
---@param entityId string
if src.isbn  then result = result .. ' — ISBN ' .. utils.toString( context, src.isbn, options_commas ) end
---@return string | nil
if src.issn  then result = result .. ' — ISSN ' .. utils.toString( context, src.issn, options_issn ) end
local function getNormativeTitle( entityId )
if src.doi  then result = result .. ' — ' .. utils.toString( context, src.doi, options_doi ) end
    local possibleTypeIds = {}
if src.pmid  then result = result .. ' — ' .. utils.toString( context, src.pmid, options_pmid ) end
    for typeId, _ in pairs( NORMATIVE_DOCUMENTS ) do
if src.arxiv then result = result .. ' — ' .. utils.toString( context, src.arxiv, options_arxiv ) end
        table.insert( possibleTypeIds, typeId )
return result
    end
 
    local foundTypeId = getFirstType( entityId, possibleTypeIds )
    if foundTypeId then
        return NORMATIVE_DOCUMENTS[ foundTypeId ]
    end
 
    return nil
end
end


local function appendSourceId(result, context, src)
---@param urls table<number, string> | string
if src.sourceId then
---@param text string
local citetype = src.type and utils.toString(context, src.type, options_citetypes) or 'citetype_unknown'
---@return string
result = '<span class="wikidata_cite ' .. citetype .. '" data-entity-id="' .. utils.getSingle(src.sourceId) .. '">' .. result .. '</span>'
local function wrapInUrl( urls, text )
end
    local url = getSingle( urls )
return result
 
    if string.sub( url, 1, 1 ) == ':' then
        return '[[' .. url .. '|' .. text .. ']]'
    else
        return '[' .. url .. ' ' .. text .. ']'
    end
end
end


local function appendAccessDate(result, context, src)
---@param entityId string
if src.accessdate then
---@param lang string
local date = utils.getSingle(src.accessdate)
---@return string
local pattern = "(%-?%d+)%-(%d+)%-(%d+)T";
local function getElementLink( entityId, lang )
local y, m, d = mw.ustring.match(date, pattern)
    local sitelink = getSitelink( entityId, nil )
y, m, d = tonumber(y), tonumber(m), tonumber(d)
    if sitelink then
local date_str = (d > 0 and ' ' .. tostring(d) or '')
        return ':' .. sitelink
  .. (m > 0 and ' ' .. monthg[m] or '')
    end
  .. (y > 0 and ' ' .. tostring(y) or '')
 
result = result .. " <small>Проверено" .. date_str .. ".</small>"
    if lang ~= 'mul' then
end
        -- link to entity in source language
return result
        sitelink = getSitelink( entityId, lang .. 'wiki' )
        if sitelink then
            return ':' .. lang .. ':' .. sitelink
        end
    end
 
    return ':d:' .. entityId
end
end


local function populateUrl(context, src)
---@param entityId string
if src.sourceId and not src.url then
---@param lang string
local entity = utils.getEntity(context, src.sourceId)
---@return string
if entity.sitelinks and entity.sitelinks[context.lang .. 'wikisource'] then
local function getLabel( entityId, lang )
src.url = ':' .. context.lang .. ':s:' .. entity.sitelinks[context.lang .. 'wikisource'].title
    local wbStatus, label = pcall( mw.wikibase.getLabelByLang, entityId, lang )
end
    if not wbStatus then
end
        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
end


local function populateYear(src)
---@param entityId string
if not src.year and src.dateOfPublication then
---@param data source
local date = utils.getSingle(src.dateOfPublication)
---@return source
src.year = mw.ustring.sub(date, 2, 5)
local function populateSourceDataImpl( entityId, data, map )
end
    local wsLink = getSitelink( entityId, 'ruwikisource' )
if not src.year and src.dateOfCreation then
    if wsLink and not mw.ustring.gmatch( wsLink, 'Категория:' ) then
local date = utils.getSingle(src.dateOfCreation)
        data.url = ":ru:s:" .. wsLink
src.year = mw.ustring.sub(date, 2, 5)
    end
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 ..
                    " от&nbsp;" .. tostring( d ) .. "&nbsp;" .. monthGen[ m ]  .. " " .. tostring( y ) .. "&nbsp;г." ..
                    ( docNumber and ( " №&nbsp;" .. 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
end


local function populateTitle(src)
---@param entityId string
src.title = src.title or utils.getSingle(src.url) or '\'\'(unspecified title)\'\''
---@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
end


local function renderSource(context, src)
---Expand special types of references when additional data could be found in OTHER entity properties
options_commas_authors.format = personNameToAuthorName
---@param data source
options_commas_responsible.format = personNameToResponsibleName
---@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 )


context.lang = utils.getLangCode(utils.getSingle(src.lang)) or i18nDefaultLanguage
    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 )


utils.preprocessPlaces(src, context.lang)
    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 )


populateUrl(context, src)
    else
populateTitle(src)
        -- generic property search
populateYear(src)
        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


local result = generateAuthorLinks(context, src)
    -- do we have appropriate record in P1433 ?
result = appendTitle(result, context, src)
    local claims = findClaimsByValue( currentEntityId, 'P1343', data.sourceId )
result = appendLanguage(result, context, src)
    if claims and #claims ~= 0 then
result = appendSubtitle(result, context, src)
        for _, claim in pairs( claims ) do
result = appendOriginalTitle(result, context, src)
            populateDataFromSnaks( claim.qualifiers, data, PROPERTY_MAP )
result = appendPublication(result, context, src)
            populateDataFromEntity( data.sourceId, data, PROPERTY_MAP )
        end
result = result .. '<span class="wef_low_priority_links">'
    end
result = appendEditor(result, context, src) -- Might take current editor instead of actual. Use with caution
end
result = appendEdition(result, context, src)
result = appendPublicationData(result, context, src)
result = appendVolumeAndIssue(result, context, src)
result = appendPages(result, context, src)
result = appendNumberOfPages(result, context, src)
result = appendBookSeries(result, context, src)
result = appendTirage(result, context, src)
result = appendIdentifiers(result, context, src)
result = appendSourceId(result, context, src)
result = appendAccessDate(result, context, src)
result = result .. '</span>'


return result
---@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
end


local function renderReferenceImpl(currentEntity, reference, refAnchor, refAnchorYear)
---@param lang string
if not reference.snaks then
---@param placeId string
return nil
---@return string
end
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
local context = {
---@param lang string
cache = {}
---@return void
}
local function preprocessPlace( data, lang )
    if not data.place then
        return
    end


-- данные в простом формате, согласованном с модулями формирования библиографического описания
    ---@type table<number, string>
local data = {}
    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 .. '&nbsp;', 'volume', nil )
        result = result ..appendProperty( lang, data, ', ' .. letter_iss .. '&nbsp;', 'issue', nil )
    else
        result = result .. appendProperty( lang, data, letter_iss .. '&nbsp;', '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 .. '&nbsp;' .. 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 ) .. '&nbsp;' .. 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 .. '&nbsp;', 'bookSeriesVolume', nil )
            result = result .. appendProperty( lang, data, ', ' .. letter_iss .. '&nbsp;', 'bookSeriesIssue', nil )
        else
            result = result .. appendProperty( lang, data, letter_iss .. '&nbsp;', '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
     -- забрать данные из reference
     utils.populateDataFromClaims(context, nil, reference.snaks, data)
     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


utils.expandSpecials(context, currentEntity, reference, data)
    local rendered = renderSource( data )
    if mw.ustring.len( rendered ) == 0 then
        return nil
    end


local sourceEntity = nil
    if data.ref then
if data.sourceId then
        local anchorValue = 'CITEREF' .. data.ref .. ( coalesce( { data[ 'ref-year' ], data.year } ) or '' )
sourceEntity = utils.getEntity(context, data.sourceId)
        rendered = '<span class="citation" id="' .. mw.uri.anchorEncode( anchorValue ) .. '">' .. rendered .. '</span>'
if sourceEntity then
    end
utils.populateSourceDataImpl(context, sourceEntity, data)
end
end


if data.publication then
    return rendered
utils.expandPublication(context, sourceEntity, data)
end
end


utils.expandBookSeries(context, data)
---@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


if next(data) == nil then
    local data = getFilledArgs( frame.args or {} )
return nil
    populateDataFromSnaks( reference.snaks, data, PROPERTY_MAP )
end
    data.sourceId = getSingle( data.sourceId )


local rendered = renderSource(context, data)
    if type( currentEntityId ) == 'string' then
if mw.ustring.len(rendered) == 0 then
        data.entityId = currentEntityId
return nil
    elseif type( currentEntityId ) == 'table' and currentEntityId.id then
end
        data.entityId = currentEntityId.id
    end


if refAnchor then
    ---@type string
local anchorValue = 'CITEREF' .. refAnchor .. (utils.coalesce(refAnchorYear, data.year) or '')
    local rendered = renderReferenceImpl( data, reference.snaks or {} )
rendered = '<span class="citation" id="' .. mw.uri.anchorEncode(anchorValue) .. '">' .. rendered .. '</span>'
    if not rendered then
end
        return ''
    end


return rendered
    return rendered
end
end


local function artificialSnaks(args)
local snaks = {}
if args[1] then
entityId = mw.text.trim(args[1])
snaks.P248 = {utils.toWikibaseEntityIdSnak("P248", entityId)}
snaks.P805 = {utils.toWikibaseEntityIdSnak("P805", entityId)}
end
utils.copyArgsToSnaks(args, snaks)
return mw.wikibase.getEntity(), {snaks = snaks}
end


function p.renderReference(frame, currentEntity, reference)
---@param frame frame
p.currentFrame = 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


-- template call
    -- Про выбор алгоритма хеширования см. [[Модуль:Hash]]. Знак подчёркивания в начале позволяет
if frame and not currentEntity and not reference then
    -- исключить ошибку, когда имя сноски — чисто числовое значение, каковыми иногда бывают хеши.
currentEntity, reference = artificialSnaks(frame.args)
    return frame:extensionTag( 'ref', rendered, { name = '_' .. mw.hash.hashValue( 'fnv164', rendered ) } ) .. '[[Category:Википедия:Статьи с источниками из Викиданных]]'
end
end


local rendered = renderReferenceImpl(currentEntity, reference)
---@param frame frame
if not rendered then
---@return string | nil
return ''
function p.testPersonNameToAuthorName( frame )
end
    return personNameToAuthorName( frame.args[ 1 ] )
-- Про выбор алгоритма хеширования см. [[Модуль:Hash]]. Знак подчёркивания в начале позволяет
-- исключить ошибку, когда имя сноски — чисто числовое значение, каковыми иногда бывают хеши.
return frame:extensionTag('ref', rendered, {name = '_' .. mw.hash.hashValue('fnv164', rendered)}) .. '[[Category:Википедия:Статьи с источниками из Викиданных]]'
end
end


function p.renderSource(frame)
---@param frame frame
p.currentFrame = frame
---@return string | nil
currentEntity, reference = artificialSnaks(frame.args)
function p.testPersonNameToResponsibleName( frame )
return renderReferenceImpl(currentEntity, reference, frame.args['ref'], frame.args['ref-year'])
    return personNameToResponsibleName( frame.args[ 1 ] )
end
end


return p;
return p

Текущая версия от 18:34, 21 июня 2023

Принцип работы модуляПравить

Данный модуль генерирует текст, используемый в сносках, ссылающихся на элемент викиданных.

Тесты [ править ]Править

9 тестов провалено.

test_personNameToAuthorName
Тест Ожидаемое значение Фактическое значение
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToAuthorName | Ломоносов, Михаил Васильевич }} Ломоносов М. В. Ломоносов М. В.
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToAuthorName | Ломоносов, Михаил }} Ломоносов М. Ломоносов М.
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToAuthorName | Михаил Васильевич Ломоносов }} Ломоносов М. В. Ломоносов М. В.
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToAuthorName | Михаил Ломоносов }} Ломоносов М. Ломоносов М.
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToAuthorName | М. В. Ломоносов }} Ломоносов М. В. Ломоносов М. В.
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToAuthorName | М. Ломоносов }} Ломоносов М. Ломоносов М.
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToAuthorName | Ломоносов М. В. }} Ломоносов М. В. Ломоносов М. В.
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToAuthorName | Ломоносов М. }} Ломоносов М. Ломоносов М.
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToAuthorName | Топчибашев, Мустафа Агабек оглы }} Топчибашев М. А. Топчибашев М. А.
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToAuthorName | Гельмонт, Ян Баптиста ван }} ван Гельмонт Я. Б. ван Гельмонт Я. Б.
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToAuthorName | Гельмонт, Ян ван }} ван Гельмонт Я. ван Гельмонт Я.
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToAuthorName | Jan Baptista van Helmont }} van Helmont J. B. van Helmont J. B.
Файл:X mark.svgN {{#invoke:Sources | testPersonNameToAuthorName | Jan van Helmont }} van Helmont J. Helmont J. v.
test_personNameToResponsibleName
Тест Ожидаемое значение Фактическое значение
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToResponsibleName | Ломоносов, Михаил Васильевич }} М. В. Ломоносов М. В. Ломоносов
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToResponsibleName | Ломоносов, Михаил }} М. Ломоносов М. Ломоносов
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToResponsibleName | Михаил Васильевич Ломоносов }} М. В. Ломоносов М. В. Ломоносов
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToResponsibleName | Михаил Ломоносов }} М. Ломоносов М. Ломоносов
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToResponsibleName | М. В. Ломоносов }} М. В. Ломоносов М. В. Ломоносов
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToResponsibleName | М. Ломоносов }} М. Ломоносов М. Ломоносов
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToResponsibleName | Ломоносов М. В. }} М. В. Ломоносов М. В. Ломоносов
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToResponsibleName | Ломоносов М. }} М. Ломоносов М. Ломоносов
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToResponsibleName | Топчибашев, Мустафа Агабек оглы }} М. А. Топчибашев М. А. Топчибашев
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToResponsibleName | Гельмонт, Ян Баптиста ван }} Я. Б. ван Гельмонт Я. Б. ван Гельмонт
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToResponsibleName | Гельмонт, Ян ван }} Я. ван Гельмонт Я. ван Гельмонт
Файл:Yes check.svg {{#invoke:Sources | testPersonNameToResponsibleName | Jan Baptista van Helmont }} J. B. van Helmont J. B. van Helmont
Файл:X mark.svgN {{#invoke:Sources | testPersonNameToResponsibleName | Jan van Helmont }} J. van Helmont J. v. Helmont
test_renderSource
Тест Ожидаемое значение Фактическое значение
Файл:X mark.svgN {{#invoke:Sources | renderSource | Q20750516}} президент Российской Федерации Указ Президента Российской Федерации от 15 января 1992 г. № 23 «О Генеральном директоре Агентства федеральной безопасности Российской Федерации и Министре внутренних дел Российской Федерации» // Собрание законодательства Российской Федерации — 1992. Ошибка Lua на строке 530: attempt to index field 'wikibase' (a nil value).
Файл:X mark.svgN {{#invoke:Sources | renderSource | Q21683979}} Advances in Cryptology — EUROCRYPT 2004 (англ.): International Conference on the Theory and Applications of Cryptographic Techniques, Interlaken, Switzerland, May 2-6, 2004. Proceedings / C. Cachin, J. L. CamenischSpringer, Berlin, Heidelberg, 2004. — 630 p. — ISBN 978-3-540-21935-4 — doi:10.1007/B97182 Ошибка Lua на строке 530: attempt to index field 'wikibase' (a nil value).
Файл:X mark.svgN {{#invoke:Sources | renderSource | Q21683981}} Nguyen P. Can We Trust Cryptographic Software? Cryptographic Flaws in GNU Privacy Guard v1.2.3 (англ.) // Advances in Cryptology — EUROCRYPT 2004: International Conference on the Theory and Applications of Cryptographic Techniques, Interlaken, Switzerland, May 2-6, 2004. Proceedings / C. Cachin, J. L. CamenischSpringer, Berlin, Heidelberg, 2004. — P. 555—570. — 630 p. — ISBN 978-3-540-21935-4 — doi:10.1007/978-3-540-24676-3_33 Ошибка Lua на строке 530: attempt to index field 'wikibase' (a nil value).
Файл:X mark.svgN {{#invoke:Sources | renderSource | Q21725400}} Eichenauer J., Lehn J. A non-linear congruential pseudo random number generator (англ.) // Statistische HefteSpringer Berlin Heidelberg, Springer Science+Business Media, 1986. — Vol. 27, Iss. 1. — P. 315—326. — ISSN 0932-5026; 1613-9798doi:10.1007/BF02932576 Ошибка Lua на строке 530: attempt to index field 'wikibase' (a nil value).
Файл:X mark.svgN {{#invoke:Sources | renderSource | Q21725116}} Menezes A. J., Oorschot P. v., Vanstone S. A. Handbook of Applied Cryptography (англ.)CRC Press, 1996. — 816 p. — (Discrete Mathematics and Its Applications) — ISBN 978-0-8493-8523-0 Ошибка Lua на строке 530: attempt to index field 'wikibase' (a nil value).
Файл:X mark.svgN {{#invoke:Sources | renderSource | Q27450585}} Введение в криптографию / под ред. В. В. ЯщенкоМ.: МЦНМО, 2000. — 271 с. — ISBN 978-5-900916-26-2 Ошибка Lua на строке 530: attempt to index field 'wikibase' (a nil value).
test_templates
Тест Ожидаемое значение Фактическое значение
Файл:X mark.svgN {{source | Q21725116}} Menezes A. J., Oorschot P. v., Vanstone S. A. Handbook of Applied Cryptography (англ.)CRC Press, 1996. — 816 p. — (Discrete Mathematics and Its Applications) — ISBN 978-0-8493-8523-0 Ошибка Lua на строке 530: attempt to index field 'wikibase' (a nil value).


Служебные подмодулиПравить

Используемые параметры ВикиданныхПравить

Свойство Комментарий
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value). используется для указания названия статьи в энциклопедии
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value). если требуется переопределить название из метки элемента
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).
Ошибка Lua в Модуль:WD на строке 450: attempt to index field 'wikibase' (a nil value).

ФункцииПравить

ВнешниеПравить

Внешние функции принимают объекты типа фрейм и предназначены для вызова из других модулей или через функцию парсера <syntaxhighlight lang="text" class="" id="" style="" inline="1">{{#invoke:}}</syntaxhighlight>.

Прямое обращение к функциям модулей в статьях крайне нежелательно! Используйте для этих целей подходящие шаблоны.

p.renderSource(frame)Править

Выдаёт вики-текст ссылки на заданный источник для подстановки внутрь сноски или списка литературы. См. шаблоны {{source}} и {{ВД-Источник}}, использующие данную функцию. Поддерживает следующие аргументы:

  • <syntaxhighlight lang="text" class="" id="" style="" inline="1">frame.args[1]</syntaxhighlight> — анонимный аргумент, задающий идентификатор объекта на викиданных, по которому нужно сгенерировать ссылку. Например, Q20750516.
  • <syntaxhighlight lang="text" class="" id="" style="" inline="1">frame.args['ref']</syntaxhighlight> — задаёт метку ref, которую в дальнейшем можно будет использовать в шаблонах типа {{sfn}}.
  • <syntaxhighlight lang="text" class="" id="" style="" inline="1">frame.args['ref-year']</syntaxhighlight> — задаёт метку ref-year, которая используется аналогично метке ref.
  • <syntaxhighlight lang="text" class="" id="" style="" inline="1">frame.args['part']</syntaxhighlight> — дополнительный аргумент, позволяющий уточнить часть источника, на которую идёт ссылка (например, главу в книге).
  • <syntaxhighlight lang="text" class="" id="" style="" inline="1">frame.args['parturl']</syntaxhighlight> — ссылка, которую следует поставить на часть, описанную предыдущим аргументом.
  • <syntaxhighlight lang="text" class="" id="" style="" inline="1">frame.args['pages']</syntaxhighlight> — конкретные страницы в источнике, на которые ведётся ссылка.
  • <syntaxhighlight lang="text" class="" id="" style="" inline="1">frame.args['url']</syntaxhighlight> — позволяет явно указать, какую ссылку нужно будет проставить на источник.
  • <syntaxhighlight lang="text" class="" id="" style="" inline="1">frame.args['volume']</syntaxhighlight> — позволяет явно указать том источника, на который идёт ссылка.
  • <syntaxhighlight lang="text" class="" id="" style="" inline="1">frame.args['issue']</syntaxhighlight> — позволяет явно указать выпуск источника, на который идёт ссылка.

Пробрасывание большей части аргументов происходит в utils.copyArgsToSnaks. Сам переданный фрейм сохраняется в p.currentFrame для дальнейшего использования, а на основе переданных аргументов функцией artificialSnaks создаются искусственные снеки, которые ссылаются на источник, указанный в <syntaxhighlight lang="text" class="" id="" style="" inline="1">frame.args[1]</syntaxhighlight>, через свойства P248 (stated in) и P805 (statement is subject of). Затем данные передаются в renderReferenceImpl для дальнейшей обработки.

p.renderReference(frame, currentEntity, reference)Править

Выдаёт вики-текст готовой сноски на заданный источник. Поддерживает те же аргументы, что и p.renderSource, кроме ref и ref-year. См. шаблоны {{source-ref}} и {{ВД-Сноска}}, использующие данную функцию. Также используется в Модуль:Wikidata для отображения ссылок, указанных возле утверждений на викиданных. Если currentEntity и reference отсутствуют, создаются искусственные снеки с помощью функции artificialSnaks, после чего они передаются в renderReferenceImpl. Если вики-текст для сноски был успешно сгенерирован, он оборачивается в тэг <syntaxhighlight lang="text" class="" id="" style="" inline="1"><ref></syntaxhighlight> с помощью frame: extensionTag, при этом имя для сноски генерируется путём хеширования её вики-текста через mw.hash.hashValue. Статьи, с такими сносками помещаются в Категория:Википедия:Статьи с источниками из Викиданных.

ВнутренниеПравить

tokenizeName(fullName)Править

Преобразует полное имя в пару {фамилии через пробел, инициалы имён через пробел}. Реализована в виде разбора случаев, которые можно встретить на викиданных:

  1. Фамилия, Имя
  2. Фамилия, Имя Имя
  3. Фамилия Фамилия, Имя
  4. Имя Имя оглы Фамилия
  5. Имя Имя де Фамилия
  6. Имя … Имя Фамилия (хотя бы одно и не более четырёх единичных имён)

Здесь имя, в отличие от фамилии, может являться инициалом. Если ни один из форматов выше не выполнен, возвращает полное имя без изменений.

personNameToAuthorName(fullName)Править

Преобразует полное имя в формат Фамилия И. О. с помощью tokenizeName.

personNameToResponsibleName(fullName)Править

Преобразует полное имя в формат И. О. Фамилия с помощью tokenizeName.

getPeopleAsWikitext(context, value, options)Править

Преобразует список имён value в викитекст в соответствии со списком опций options. В опциях должны быть проставлены следующие поля:

  1. separator — разделитель в списке;
  2. conjunction — разделитель перед последним элементом списка;
  3. format — функция, преобразующая имена к некоторому нормализованному виду (например, personNameToAuthorName);
  4. nolinks — логическое значение, должно быть истинным если проставление ссылок нежелательно;
  5. preferids — логическое значение, должно быть истинным если нужно вернуть id с викиданных, а не имена.

Если в списке больше maxAuthors (на текущий момент 10) людей, заменяет остальных на и др. или его аналоги (если в контексте указан язык, то используется i18nEtAl[context.lang], иначе используется i18nEtAlDefault).

appendProperty(result, context, src, conjunctor, property, url)Править

Приписывает src[property] к result, разделяя их строкой, записанной в conjunctor. Если возможно, оформляет его ссылкой на src[url].

generateAuthorLinks(context, src)Править

Возвращает список авторов src.author, оформленный через getPeopleAsWikitext и обрамлённый в <syntaxhighlight lang="text" class="" id="" style="" inline="1"><i class="wef_low_priority_links"></i></syntaxhighlight>.

appendTitle(result, context, src)Править

Дописывает к result строку <syntaxhighlight lang="text" class="" id="" style="" inline="1">src.part // src.title</syntaxhighlight> либо только <syntaxhighlight lang="text" class="" id="" style="" inline="1">src.title</syntaxhighlight> если src.part не указан. Если возможно, обрамляет src.part (или src.title если src.part не указан) в src.url.

appendLanguage(result, context, src)Править

Если context.lang отличается от i18nDefaultLanguage (в нашем разделе русский), то указание об этом приписывается к result с помощью Модуль:Languages в формате {{ref-lang}}.

appendSubtitle(result, context, src)Править

Дописывает к result строку <syntaxhighlight lang="text" class="" id="" style="" inline="1">: src.subtitle</syntaxhighlight> если src.subtitle определён.

appendOriginalTitle(result, context, src)Править

Дописывает к result строку <syntaxhighlight lang="text" class="" id="" style="" inline="1"> = src.originaltitle</syntaxhighlight> если src.originaltitle определён.

appendPublication(result, context, src)Править

Дописывает к result строку <syntaxhighlight lang="text" class="" id="" style="" inline="1"> // src.publication: src.publication.subtitle</syntaxhighlight> если определён src.publication.subtitle, либо <syntaxhighlight lang="text" class="" id="" style="" inline="1"> // src.publication</syntaxhighlight> если определён только src.publication.

appendEditor(result, context, src)Править

Дописывает к result строку <syntaxhighlight lang="text" class="" id="" style="" inline="1"> / prefix src.editor</syntaxhighlight> если определён src.editor, где prefix определяется по context.lang (по умолчанию, <syntaxhighlight lang="text" class="" id="" style="" inline="1">под ред.</syntaxhighlight>).

appendEdition(result, context, src)Править

Дописывает к result строку <syntaxhighlight lang="text" class="" id="" style="" inline="1"> — src.edition</syntaxhighlight> если src.edition определён.

appendPublicationData(result, context, src)Править

Добавляет к result строку вида <syntaxhighlight lang="text" class="" id="" style="" inline="1"> — src.place: src.publisher, src.year.</syntaxhighlight> если хотя бы один из указанных параметров определён. Неуказанная часть опускается вместе с соответствующей пунктуацией. В частности, двоеточие ставится только если указано src.place и хотя бы что-то из src.publisher и src.year, запятая ставится только если указаны и src.publisher, и src.year. Тире и точка ставятся если указан хотя бы один из параметров.

appendVolumeAndIssue(result, context, src)Править

Добавляет к result строку виду <syntaxhighlight lang="text" class="" id="" style="" inline="1"> — letter_vol src.volume, letter_iss src.issue.</syntaxhighlight> если хотя бы один из указанных параметров определён. Запятая ставится если указаны оба параметра. letter_vol и letter_iss определяются по context.lang (например, <syntaxhighlight lang="text" class="" id="" style="" inline="1">Т.</syntaxhighlight> и <syntaxhighlight lang="text" class="" id="" style="" inline="1">вып.</syntaxhighlight> для русских текстов, <syntaxhighlight lang="text" class="" id="" style="" inline="1">Vol.</syntaxhighlight> и <syntaxhighlight lang="text" class="" id="" style="" inline="1">Iss.</syntaxhighlight> для английских).

appendPages(result, context, src)Править

Добавляет к result строку вида <syntaxhighlight lang="text" class="" id="" style="" inline="1"> — letter src.pages.</syntaxhighlight> если src.pages определён, при этом в качестве разделителя в src.pages, если это диапозон страниц, используется символ «—», а letter определяется исходя из context.lang (например, <syntaxhighlight lang="text" class="" id="" style="" inline="1">P.</syntaxhighlight> для английского и <syntaxhighlight lang="text" class="" id="" style="" inline="1">С.</syntaxhighlight> для русского).

appendNumberOfPages(result, context, src)Править

Добавляет к result строку вида <syntaxhighlight lang="text" class="" id="" style="" inline="1"> — src.numberOfPages letter</syntaxhighlight> если src.numberOfPages определён. При этом letter определяется из context.lang (<syntaxhighlight lang="text" class="" id="" style="" inline="1">p.</syntaxhighlight> для английского и <syntaxhighlight lang="text" class="" id="" style="" inline="1">с.</syntaxhighlight> для русского).

appendBookSeries(result, context, src)Править

Добавляет к result строку вида <syntaxhighlight lang="text" class="" id="" style="" inline="1"> — (src.bookSeries; letter_vol src.bookSeriesVolume, letter_iss src.bookSeriesIssue)</syntaxhighlight> если src.bookSeries определено. Точка с запятой ставится только если определено src.bookSeriesVolume или src.bookSeriesIssue, запятая ставится если определены оба параметра. letter_vol и letter_iss определяются из context.lang, аналогично тому, как это делается в appendVolumeAndIssue.

appendBookSeries(result, context, src)Править

Добавляет к result информацию из src.tirage если тот определён. Формат определяется из context.lang, для английского это <syntaxhighlight lang="text" class="" id="" style="" inline="1"> — ed. size: src.tirage</syntaxhighlight>, а для русского <syntaxhighlight lang="text" class="" id="" style="" inline="1"> — src.tirage экз.</syntaxhighlight>.

appendIdentifiers(result, context, src)Править

Добавляет к result идентификаторы ISBN, ISSN, DOI, PMID и arXiv если те определены. Идентификаторы приписываются через тире, более точный формат определён в таблицах options_commas, options_issn, options_doi, options_pmid и options_arxiv.

appendSourceId(result, context, src)Править

Оборачивает result в <syntaxhighlight lang="text" class="" id="" style="" inline="1"><span class="wikidata_cite citetype" data-entity-id="src.sourceId"></span></syntaxhighlight>, где citetyle это src.type если это поле определено и citetype_unknown в противном случае.

appendAccessDate(result, context, src)Править

Добавляет к result строку виду <syntaxhighlight lang="text" class="" id="" style="" inline="1">Проверено dd month yyyy.</syntaxhighlight>, где dd, month и yyyy берутся из src.accessdate если данное поле определено.

populateUrl(context, src)Править

Если src.url не определено, но src.sourceId известен, пытается присвоить в src.url ссылку на викитеку.

populateYear(src)Править

Если src.year не определён, пытается заполнить его из src.dateOfPublication и src.dateOfCreation.

populateTitle(src)Править

Если src.title не определён, пытается присвоить ему src.url, если и это не получается, то присваивает <syntaxhighlight lang="text" class="" id="" style="" inline="1">(unspecified title)</syntaxhighlight>.

renderSource(context, src)Править

Внутренняя функция, генерирующая текст, который будет отображаться в сноске. Действует следующим образом:

  1. Записывает src.lang в context.lang (или i18nDefaultLanguage если src.lang записать не получилось).
  2. Вызывает populateUrl, populateTitle и populateYear.
  3. Заводит переменную result, изначально равную generateAuthorLinks(context, src).
  4. .Последовательно применяет к result функции appendTitle—appendAccessDate, при этом блок appendEditor—appendAccessDate дополнительно обрамляется в <syntaxhighlight lang="text" class="" id="" style="" inline="1"><span class="wef_low_priority_links"></span></syntaxhighlight>

---@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 ) .. '.&nbsp;' .. 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 .. '&nbsp;' .. 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 ) .. '.&nbsp;' .. 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 ) .. '.&nbsp;' .. 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 ) .. '.&nbsp;' .. 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, '.&nbsp;', 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, '.&nbsp;', 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 ] .. '&nbsp;' .. 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 ] .. '&nbsp;' .. 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 ..
                    " от&nbsp;" .. tostring( d ) .. "&nbsp;" .. monthGen[ m ]  .. " " .. tostring( y ) .. "&nbsp;г." ..
                    ( docNumber and ( " №&nbsp;" .. 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 .. '&nbsp;', 'volume', nil )
        result = result ..appendProperty( lang, data, ', ' .. letter_iss .. '&nbsp;', 'issue', nil )
    else
        result = result .. appendProperty( lang, data, letter_iss .. '&nbsp;', '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 .. '&nbsp;' .. 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 ) .. '&nbsp;' .. 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 .. '&nbsp;', 'bookSeriesVolume', nil )
            result = result .. appendProperty( lang, data, ', ' .. letter_iss .. '&nbsp;', 'bookSeriesIssue', nil )
        else
            result = result .. appendProperty( lang, data, letter_iss .. '&nbsp;', '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