Документация

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

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

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

Ошибка Lua в package.lua на строке 80: module 'Module:UnitTests' not found.


Служебные подмодули Тупатыны

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

Свойство Комментарий
автор (P50)
имя автора (строка) (P2093)
язык произведения или названия (P407)
язык оригинала фильма или телешоу (P364)
раздел, стих или параграф (P958) используется для указания названия статьи в энциклопедии
название (P1476) если требуется переопределить название из метки элемента
подзаголовок (P1680)
доступен по URL (P953)
архивный URL (P1065)
URL-ссылка на источник (P854)
опубликовано в (P1433)
номер издания (P393)
издатель (P123)
место публикации (P291)
редактор (P98)
страницы (P304)
количество страниц (P1104)
том (P478)
выпуск (P433)
дата основания / создания / возникновения (P571)
дата публикации (P577)
ISBN-13 (P212)
ISBN-10 (P957)
ISSN (P236)
порядковый номер (P1545)
код arXiv (P818)
JSTOR (P888)

Функции Тупатыны

Внешние Тупатыны

Внешние функции принимают объекты типа фрейм и предназначены для вызова из других модулей или через функцию парсера {{#invoke:}}.

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

p.renderSource(frame) Тупатыны

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

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

Пробрасывание большей части аргументов происходит в utils.copyArgsToSnaks. Сам переданный фрейм сохраняется в p.currentFrame для дальнейшего использования, а на основе переданных аргументов функцией artificialSnaks создаются искусственные снеки, которые ссылаются на источник, указанный в frame.args[1], через свойства 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. Если вики-текст для сноски был успешно сгенерирован, он оборачивается в тэг <ref> с помощью 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 и обрамлённый в <i class="wef_low_priority_links"></i>.

appendTitle(result, context, src) Тупатыны

Дописывает к result строку src.part // src.title либо только src.title если 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 строку : src.subtitle если src.subtitle определён.

appendOriginalTitle(result, context, src) Тупатыны

Дописывает к result строку  = src.originaltitle если src.originaltitle определён.

appendPublication(result, context, src) Тупатыны

Дописывает к result строку  // src.publication: src.publication.subtitle если определён src.publication.subtitle, либо  // src.publication если определён только src.publication.

appendEditor(result, context, src) Тупатыны

Дописывает к result строку  / prefix src.editor если определён src.editor, где prefix определяется по context.lang (по умолчанию, под ред.).

appendEdition(result, context, src) Тупатыны

Дописывает к result строку  — src.edition если src.edition определён.

appendPublicationData(result, context, src) Тупатыны

Добавляет к result строку вида  — src.place: src.publisher, src.year. если хотя бы один из указанных параметров определён. Неуказанная часть опускается вместе с соответствующей пунктуацией. В частности, двоеточие ставится только если указано src.place и хотя бы что-то из src.publisher и src.year, запятая ставится только если указаны и src.publisher, и src.year. Тире и точка ставятся если указан хотя бы один из параметров.

appendVolumeAndIssue(result, context, src) Тупатыны

Добавляет к result строку виду  — letter_vol src.volume, letter_iss src.issue. если хотя бы один из указанных параметров определён. Запятая ставится если указаны оба параметра. letter_vol и letter_iss определяются по context.lang (например, Т. и вып. для русских текстов, Vol. и Iss. для английских).

appendPages(result, context, src) Тупатыны

Добавляет к result строку вида  — letter src.pages. если src.pages определён, при этом в качестве разделителя в src.pages, если это диапозон страниц, используется символ «—», а letter определяется исходя из context.lang (например, P. для английского и С. для русского).

appendNumberOfPages(result, context, src) Тупатыны

Добавляет к result строку вида  — src.numberOfPages letter если src.numberOfPages определён. При этом letter определяется из context.lang (p. для английского и с. для русского).

appendBookSeries(result, context, src) Тупатыны

Добавляет к result строку вида  — (src.bookSeries; letter_vol src.bookSeriesVolume, letter_iss src.bookSeriesIssue) если src.bookSeries определено. Точка с запятой ставится только если определено src.bookSeriesVolume или src.bookSeriesIssue, запятая ставится если определены оба параметра. letter_vol и letter_iss определяются из context.lang, аналогично тому, как это делается в appendVolumeAndIssue.

appendBookSeries(result, context, src) Тупатыны

Добавляет к result информацию из src.tirage если тот определён. Формат определяется из context.lang, для английского это  — ed. size: src.tirage, а для русского  — src.tirage экз..

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 в <span class="wikidata_cite citetype" data-entity-id="src.sourceId"></span>, где citetyle это src.type если это поле определено и citetype_unknown в противном случае.

appendAccessDate(result, context, src) Тупатыны

Добавляет к result строку виду <small>Проверено dd month yyyy.</small>, где 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, если и это не получается, то присваивает ''(unspecified title)''.

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 дополнительно обрамляется в &lt;span class="wef_low_priority_links"&gt;&lt;/span&gt;

artificialSnaks(args) Тупатыны

Создаёт искусственные снеки, которые ссылаются на источник с идентификатором frame.args[1] через свойства P248 (stated in) и P805 (statement is subject of), а также пробрасывает в них аргументы (том, выпуск и т. д.).

---@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