Модуль:Sources — Википедия

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

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

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

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

6 тестов из 33 провалено.

test_personNameToAuthorName
Тест Ожидаемое значение Фактическое значение
✔ {{#invoke:Sources | testPersonNameToAuthorName | Ломоносов, Михаил Васильевич }} Ломоносов М. В. Ломоносов М. В.
✔ {{#invoke:Sources | testPersonNameToAuthorName | Ломоносов, Михаил }} Ломоносов М. Ломоносов М.
✔ {{#invoke:Sources | testPersonNameToAuthorName | Михаил Васильевич Ломоносов }} Ломоносов М. В. Ломоносов М. В.
✔ {{#invoke:Sources | testPersonNameToAuthorName | Михаил Ломоносов }} Ломоносов М. Ломоносов М.
✔ {{#invoke:Sources | testPersonNameToAuthorName | М. В. Ломоносов }} Ломоносов М. В. Ломоносов М. В.
✔ {{#invoke:Sources | testPersonNameToAuthorName | М. Ломоносов }} Ломоносов М. Ломоносов М.
✔ {{#invoke:Sources | testPersonNameToAuthorName | Ломоносов М. В. }} Ломоносов М. В. Ломоносов М. В.
✔ {{#invoke:Sources | testPersonNameToAuthorName | Ломоносов М. }} Ломоносов М. Ломоносов М.
✔ {{#invoke:Sources | testPersonNameToAuthorName | Топчибашев, Мустафа Агабек оглы }} Топчибашев М. А. Топчибашев М. А.
✔ {{#invoke:Sources | testPersonNameToAuthorName | Гельмонт, Ян Баптиста ван }} ван Гельмонт Я. Б. ван Гельмонт Я. Б.
✔ {{#invoke:Sources | testPersonNameToAuthorName | Гельмонт, Ян ван }} ван Гельмонт Я. ван Гельмонт Я.
✔ {{#invoke:Sources | testPersonNameToAuthorName | Jan Baptista van Helmont }} van Helmont J. B. van Helmont J. B.
N {{#invoke:Sources | testPersonNameToAuthorName | Jan van Helmont }} van Helmont J. Helmont J. v.
test_personNameToResponsibleName
Тест Ожидаемое значение Фактическое значение
✔ {{#invoke:Sources | testPersonNameToResponsibleName | Ломоносов, Михаил Васильевич }} М. В. Ломоносов М. В. Ломоносов
✔ {{#invoke:Sources | testPersonNameToResponsibleName | Ломоносов, Михаил }} М. Ломоносов М. Ломоносов
✔ {{#invoke:Sources | testPersonNameToResponsibleName | Михаил Васильевич Ломоносов }} М. В. Ломоносов М. В. Ломоносов
✔ {{#invoke:Sources | testPersonNameToResponsibleName | Михаил Ломоносов }} М. Ломоносов М. Ломоносов
✔ {{#invoke:Sources | testPersonNameToResponsibleName | М. В. Ломоносов }} М. В. Ломоносов М. В. Ломоносов
✔ {{#invoke:Sources | testPersonNameToResponsibleName | М. Ломоносов }} М. Ломоносов М. Ломоносов
✔ {{#invoke:Sources | testPersonNameToResponsibleName | Ломоносов М. В. }} М. В. Ломоносов М. В. Ломоносов
✔ {{#invoke:Sources | testPersonNameToResponsibleName | Ломоносов М. }} М. Ломоносов М. Ломоносов
✔ {{#invoke:Sources | testPersonNameToResponsibleName | Топчибашев, Мустафа Агабек оглы }} М. А. Топчибашев М. А. Топчибашев
✔ {{#invoke:Sources | testPersonNameToResponsibleName | Гельмонт, Ян Баптиста ван }} Я. Б. ван Гельмонт Я. Б. ван Гельмонт
✔ {{#invoke:Sources | testPersonNameToResponsibleName | Гельмонт, Ян ван }} Я. ван Гельмонт Я. ван Гельмонт
✔ {{#invoke:Sources | testPersonNameToResponsibleName | Jan Baptista van Helmont }} J. B. van Helmont J. B. van Helmont
N {{#invoke:Sources | testPersonNameToResponsibleName | Jan van Helmont }} J. van Helmont J. v. Helmont
test_renderSource
Тест Ожидаемое значение Фактическое значение
N {{#invoke:Sources | renderSource | Q20750516}} президент Российской Федерации Указ Президента Российской Федерации от 15 января 1992 г. № 23 «О Генеральном директоре Агентства федеральной безопасности Российской Федерации и Министре внутренних дел Российской Федерации» // Собрание законодательства Российской Федерации — 1992. президент России Указ Президента Российской Федерации от 15 января 1992 г. № 23 «О Генеральном директоре Агентства федеральной безопасности Российской Федерации и Министре внутренних дел Российской Федерации» // Собрание законодательства Российской Федерации — 1992.
N {{#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-4doi:10.1007/B97182 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-4doi:10.1007/B97182
N {{#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-4doi:10.1007/978-3-540-24676-3_33 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-4doi:10.1007/978-3-540-24676-3_33
N {{#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 Eichenauer J., Lehn J. A non-linear congruential pseudo random number generator (англ.) // Statistical PapersSpringer Berlin Heidelberg, Springer Science+Business Media, 1986. — Vol. 27, Iss. 1. — P. 315—326. — ISSN 0932-5026; 1613-9798doi:10.1007/BF02932576
✔ {{#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 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
✔ {{#invoke:Sources | renderSource | Q27450585}} Введение в криптографию / под ред. В. В. ЯщенкоМ.: МЦНМО, 2000. — 271 с. — ISBN 978-5-900916-26-2 Введение в криптографию / под ред. В. В. ЯщенкоМ.: МЦНМО, 2000. — 271 с. — ISBN 978-5-900916-26-2
test_templates
Тест Ожидаемое значение Фактическое значение
✔ {{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 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


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

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

Свойство Комментарий
автор (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;
---@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 not currentEntityId then     	data.entityId = mw.wikibase.getEntityIdForCurrentPage()     elseif 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