Модуль:WDFormat (Bk;rl,&WDFormat)
Модуль предназначен для форматирования набора данных, полученных из Викиданных посредством модуля WDBackend или заданных вручную. Форматирование может осуществляться в произвольной форме, как в строчку, так и в табличном виде.
Конечное представление информации задаётся профилем, который описывает, какими тегами оформлять данные, группы полей и отдельные поля, а также какие преобразования над данными необходимо сделать.
Использование
[править код]Модуль является библиотечным и предназначен для использования в других модулях. Данный модуль не предназначен для использования в статьях или других шаблонах напрямую через вызов #invoke
.
Требуемое форматирование описывается профилем, представляющим собой таблицу Lua. Профиль описывает представление отдельных полей, то есть то, как они должны отображаться. Недостающий функционал реализуется указанием в профиле собственных функций, принимающих определённый набор аргументов и возвращающих определённый результат.
Для форматирования профиля в совокупности с передаваемым набором данных необходимо использовать функцию format()
. В качестве результата возвращается отформатированный викитекст.
Для примера использования см. модуль Модуль:CiteGost.
Формат профиля в общем виде
[править код]{
-- Корневой тег:
tag = {
name = 'Имя тега',
classes = { 'class1', 'class2', ... },
attr = { атрибут1='значение1', атрибут2='значение2', ... },
tag = Вложенный тег,
},
-- Группы:
-- Начало 1-й группы:
{
-- Тег группы:
tag = {
name = 'Имя тега',
classes = { 'class1', 'class2', ... },
attr = { атрибут1='значение1', атрибут2='значение2', ... },
css = { свойство1='значение1', свойство2='значение2', ... },
tag = Вложенный тег
},
ensureEnds = 'Символ/текст, которым должно заканчиваться предыдущее поле',
delimiter = 'Разделитель, добавляемый по отношению к предыдущему полю',
childEnsureEnds = 'Символ/текст, которым должны заканчиваться поля группы',
childDelimiter = 'Разделитель полей в группе',
-- Дочерние элементы текущей группы по факту продолжают предыдущие элементы (влияет на повышение первой буквы до заглавной):
passthrough = true,
prefix = 'Текст до начала группы',
-- Поля группы (подгруппы):
-- 1-е поле группы:
{
-- Тег поля:
tag = {
name = 'Имя тега',
classes = { 'class1', 'class2', ... },
attr = { атрибут1='значение1', атрибут2='значение2', ... },
tag = Вложенный тег
css = { свойство1='значение1', свойство2='значение2', ... },
},
ensureEnds = 'Символ/текст, которым должен заканчиваться предшествующий текст',
delimiter = 'Разделитель, отделяющий текущий элемент от предыдущего',
-- Функция, разрешающая отображение поля:
cond = функция,
prefix = 'Текст до поля',
field = 'Название поля',
-- Менять ли первую букву поля на заглавную
capitalize = true/false
urlMaskProp = 'P-идентификатор свойства, отвечающего за маску ссылки',
-- Функции, через которые поле будет отформатировано:
format = { функция1, функция2, ... },
suffix = 'Текст после поля',
},
-- Второе поле:
{
value = 'Отображаемое значение',
},
-- Остальные группы:
...
suffix = 'Текст в конце группы',
},
-- Остальные группы:
...
ensureEnds = 'Окончение форматированных данных, например, точка.'
}
Функции форматирования полей
[править код]Форматирование поля задаётся через команду format
с указанием функции форматирования, принимающей определённый набор аргументов. Доступны следующие встроенные функции:
numericalRanges
— форматирование диапазона чисел (корректирует знак диапазона);dash
— оформление тире в тексте;unit
— получение единицы измерения у элемента Викиданных;abbr
— получить сокращённое обозначение (есть ограничения) у элемента Викиданных;short
— получить короткое название у элемента Викиданных;abbrWithHint
— получить сокращённое обозначение (есть ограничения) у элемента Викиданных с расшифровкой в подсказке;date
— форматирование даты;quantity
— форматирование количества с указанной в нём единицей измерения;entity
— получить значение идентификатора элемента Викиданных;wikilink
— оформить поле Викиссылкой, если это возможно;wikisource
— оформить поле ссылкой на Викитеку, если в элементе Викиданных указана соответствующая статья;link
— оформить поле внешней ссылкой, если это возможно;wikidata
— добавление к полю надстрочной ссылки на элемент Викиданных, если хотя бы в одном языковом разделе есть статья по теме;wikidataLink
— оформить поле ссылкой на элемент Викиданных, который с полем связан.forceWikidataLink
— оформить поле ссылкой на элемент Викиданных, который с полем связан. Формирует ссылку, даже если ранее по тексту ссылка на такой элемент была дана.
Внесение изменений
[править код]При исправлении ошибки, пожалуйста, сначала добавьте тест, который будет проваливаться из-за обнаруженной ошибки, и только затем вносите исправление. При внесении исправления проверьте, чтобы все тесты проходили. Вносить исправление можно только, если оно не ломает другие тесты.
Добавление нового функционала рекомендуется делать у себя в песочнице, скопировав в неё модуль. В правке копирования необходимо указать тот факт, что делается копирование, и сделать ссылку на оригинальный модуль в виде викитекста. При добавлении нового функционала сначала желательно добавить тест на этот функционал, затем добавить сам функционал, убедившись, что все тесты при этом проходят.
Тесты
[править код]Все тесты пройдены.
Название | Ожидается | Фактически | |
---|---|---|---|
test_format_array | |||
test_format_arrayCapitalize | |||
test_format_arrayForceCapitalize | |||
test_format_capitalize | |||
test_format_conflicts | |||
test_format_conflicts_recursive | |||
test_format_date | |||
test_format_delimiter_function | |||
test_format_depends | |||
test_format_depends_fieldPath | |||
test_format_depends_recursive | |||
test_format_ensureEndsAndDelimiter | |||
test_format_fieldPath | |||
test_format_group_childDelimiter_and_childEnsureEnds | |||
test_format_group_passthrough | |||
test_format_group_prefix_with_delimiter | |||
test_format_group_prefix_with_delimiter_inside_another_group | |||
test_format_groups_delimiter | |||
test_format_innerTags | |||
test_format_isStatic_hidden | |||
test_format_isStatic_hidden_in_array | |||
test_format_isStatic_hidden_in_array_in_additional_group | |||
test_format_isStatic_visible | |||
test_format_link | |||
test_format_linkOrder | |||
test_format_nested_array | |||
test_format_oneField | |||
test_format_person | |||
test_format_person_multipleNames | |||
test_format_prefixAndSuffix | |||
test_format_recurseGroups | |||
test_format_rootTag | |||
test_format_squareBrackets | |||
test_format_tableTag | |||
test_format_wikilink | |||
test_format_wikisource |
В качестве интеграционных тестов использованы тесты модуля, который работает на основе текущего модуля.
Книги (9/12) | ||||
---|---|---|---|---|
Тест | Ожидается | Фактически | Разница | |
❌ | Книга с указанной серией{{Источник информации|Q115189432
|заглавие тома = Животные
|ref = Красная книга РФ, т. «Животные»
}}
| Красная книга Российской Федерации. Т. «Животные». — 2-е издание. — М. : ФГБУ «ВНИИ Экология», 2021. — 1128 с. — (Красная книга Российской Федерации). — ISBN 978-5-6047425-0-1. — WD Q115189432. | Красная книга Российской Федерации. Т. «Животные». — 2-ое издание. — М. : ФГБУ «ВНИИ Экология», 2021. — 1128 с. — (Красная книга Российской Федерации). — ISBN 978-5-6047425-0-1. — WD Q115189432. | < 2- > 2-ое |
❌ | Том через qid с ручным заданием частного заглавия{{Источник информации|Q115926218
|заглавие тома = Full text
}}
| Lefever Ernest W. United Nations peacekeeping in the Congo: 1960-1964 : an analysis of political, executive and military control : [англ.]. In 4 vols. Vol. 2. Full text / Ernest W. Lefever, Wynfred Joshua. — Washington : Brookings Institution, 1966, 30 June. — 454 p. — OCLC 489825. — WD Q115926218. | Lefever Ernest W. United Nations peacekeeping in the Congo: 1960-1964 : an analysis of political, executive and military control : [англ.]. In 4 vols. Vol. 2. Full text / Ernest W. Lefever, Wynfred Joshua. — Washington : Brookings Institution, 1966, 30 June. — 454 p. — OCLC 489825. — WD Q115926218. | < [https:// > [https://search.worldcat.org/title/489825 < [https://www.worldcat.org/ > [https://search.worldcat.org/title/489825 < [https://www.worldcat.org/ocl > [https://search.worldcat.org/title/489825 |
❌ | Ручное задание информации с серией{{Источник информации|Q118220282|авторы=Ulla C. Kopp|заглавие=Neural Control of Renal Function|ссылка=https://www.ncbi.nlm.nih.gov/books/NBK57113/|оригинал=Neural Control of Renal Function|место=San Rafael|издатель=Morgan & Claypool Life Sciences|дата=2011-08-20|серия=Integrated Systems Physiology: from Molecule to Function to Disease|pmid=21850765|язык=en|qid=Q121112788|офлайн=1}}
| Kopp Ulla C. Neural Control of Renal Function : [англ.]. — San Rafael : Morgan & Claypool Life Sciences, 2011, 20 August. — (Integrated Systems Physiology: from Molecule to Function to Disease). — PMID 21850765. — WD Q121112788. | Kopp Ulla C. Neural Control of Renal Function : [англ.]. — 2011, 20 August. — (Integrated Systems Physiology: from Molecule to Function to Disease). — doi:10.4199/c00034ed1v01y201106isp022. — PMID 21850765. — WD Q121112788. | < > 2011, <span class="nowrap">20 August</span>. — (Integrated Systems Physiology: from Molecule to Function to Disease). — [[Цифровой идентификатор объекта|doi]]:[https://doi.org/10.4199/C00034ED1V01Y201106ISP022 10.4199/c00034ed1v01y201106isp022]. |
Медиа (1/2) | ||||
---|---|---|---|---|
Тест | Ожидается | Фактически | Разница | |
❌ | Видеозапись на youtube{{Источник информации
|заглавие = Деятельность ВОЗ по научно-техническому анализу и прогнозированию в области здравоохранения
|qid издателя = Q7817
|id = 1m_4Y6pHmUw
|qid издания = Q866
|язык = ru
|дата = 2022-07-08
|дата обращения = 2022-11-22
|ref = ВОЗ
}}
| Деятельность ВОЗ по научно-техническому анализу и прогнозированию в области здравоохранения [видеозапись] // YouTube. — ВОЗ, 2022, 8 июля. — Дата обращения: 22 ноября 2022. | Деятельность ВОЗ по научно-техническому анализу и прогнозированию в области здравоохранения [видеозапись] // YouTube. — ВОЗ, 2022, 8 июля. — Дата обращения: 22 ноября 2022. | < [https://www.youtube.com/ > [https://www.youtube.com/results?&search_query=1m_4Y6pHmUw&sp=CAM%253D < [https://www.youtube.com/wa > [https://www.youtube.com/results?&search_query=1m_4Y6pHmUw&sp=CAM%253D < [https://www.youtube.com/watch > [https://www.youtube.com/results?&search_query=1m_4Y6pHmUw&sp=CAM%253D < [https://www.youtube.com/watch?v=1m_4Y6pHmUw > [https://www.youtube.com/results?&search_query=1m_4Y6pHmUw&sp=CAM%253D |
Язык (0/1) | ||||
---|---|---|---|---|
Тест | Ожидается | Фактически | Разница | |
❌ | На двух языках{{Источник информации|Q101483459}}
| Whitlam J.[d] Harper Collins Portuguese dictionary : English-Portuguese, Portuguese-English : [англ., португ.] / J. Whitlam, Vitória Davies[d], M. Harland[d]. — Glasgow : HarperCollins UK[d], 1991. — OCLC 370343862. — WD Q101483459. | Whitlam J.[d] Harper Collins Portuguese dictionary : English-Portuguese, Portuguese-English : [англ., португ.] / J. Whitlam, Vitória Davies[d], M. Harland[d]. — Glasgow : HarperCollins UK[d], 1991. — OCLC 370343862. — WD Q101483459. | < [https:// > [https://search.worldcat.org/title/370343862 < [https://www.worldcat.org/ > [https://search.worldcat.org/title/370343862 < [https://www.worldcat.org/ocl > [https://search.worldcat.org/title/370343862 |
План разработки
[править код]План работ:
|
См. также
[править код]require('strict')
local p = {}
local NS_MODULE = 828 --: https://www.mediawiki.org/wiki/Extension_default_namespaces
local moduleNamespace = mw.site.namespaces[NS_MODULE].name
local wikidata = require(moduleNamespace .. ':WDCommon')
local function dump(obj, level)
if type(obj) ~= 'table' then
if type(obj) == 'string' then
return "'" .. obj .. "'"
else
return tostring(obj)
end
end
if not level then
level = 0
end
local indent = string.rep(' ', level)
local s = '{'
local isFirst = true
for i, v in pairs(obj) do
if not isFirst then
s = s .. ','
end
s = s .. '\n'
local currIndent = string.rep(' ', level + 4)
s = s .. currIndent
if type(i) == 'string' then
s = s .. i .. ' = '
end
s = s .. dump(v, level + 4)
isFirst = false
end
s = s .. '\n' .. indent .. '}'
return s
end
local Formatter = {
profile = nil,
processField = {},
}
-- Transforms tag table hierarchy to mediawiki html container representation.
-- Returns deepest child.
function Formatter:tagToContainer(tag, parentContainer, source)
if not tag or not tag.name then
return parentContainer
end
local container = parentContainer
while tag do
if container then
container = container:tag(tag.name)
else
container = mw.html.create(tag.name)
end
if tag.classes then
for _, currClass in ipairs(tag.classes) do
container:addClass(currClass)
end
end
if tag.attr then
for attr, value in pairs(tag.attr) do
if type(value) == 'function' then
value = value(source)
end
container:attr(attr, value)
end
end
if tag.css then
for key, value in pairs(tag.css) do
container:css(key, value)
end
end
tag = tag.tag
end
return container
end
function Formatter:new(profile, source, langCode, params)
local obj = {}
setmetatable(obj, self)
self.__index = self
self.profile = profile
self.source = source
self.lang = langCode
self.params = params
self.state = {
empty = true,
linkedEntities = {},
}
self.lastAddedText = ''
self.container = mw.html.create()
assert(self.profile.tag ~= nil, 'Not root container found. Use tag profile field.')
return obj
end
function Formatter.processField.numericalRanges(source, processedData, result)
local defaultLangObj = mw.getContentLanguage()
local defaultLang = defaultLangObj:getCode()
local rangeSign
if defaultLang == 'ru' then
rangeSign = '—'
elseif defaultLang == 'ko' then
rangeSign = '~'
else
rangeSign = '–'
end
result.wikitext = mw.ustring.gsub(result.wikitext, '-', rangeSign)
end
function Formatter.processField.squareBrackets(source, processedData, result)
result.text = '[' .. result.text .. ']'
result.wikitext = '[' .. result.wikitext .. ']'
end
function Formatter.processField.nowiki(source, processedData, result)
result.wikitext = mw.text.nowiki(result.wikitext)
end
function Formatter.processField.dash(source, processedData, result)
local defaultLangObj = mw.getContentLanguage()
local defaultLang = defaultLangObj:getCode()
local dashSign
if defaultLang == 'ru' then
dashSign = ' — '
end
result.wikitext = mw.ustring.gsub(result.wikitext, ' %- ', dashSign)
result.text = mw.ustring.gsub(result.text, ' %- ', dashSign)
end
function Formatter.processField.unit(source, processedData, result, state)
local entity = processedData.fieldTable.entity
if not entity then
return
end
local unit = wikidata.unit(entity, processedData.langCode)
if (unit and state.groupEmpty and processedData.capitalize) or processedData.forceCapitalize then
unit = mw.ustring.gsub(unit, '^%l', mw.ustring.upper)
end
result.text = unit
result.wikitext = unit
end
function Formatter.processField.abbr(source, processedData, result, state)
local entity = processedData.fieldTable.entity
if not entity or processedData.fieldTable.exact then
return
end
local abbr, _, ok = wikidata.abbrBiblio(entity, processedData.langCode)
if (abbr and state.groupEmpty and processedData.capitalize) or processedData.forceCapitalize then
abbr = mw.ustring.gsub(abbr, '^%l', mw.ustring.upper)
end
if abbr and ok then
abbr = mw.ustring.gsub(abbr, ' ', ' ')
end
result.text = abbr
result.wikitext = abbr
end
function Formatter.processField.short(source, processedData, result, state)
local entity = processedData.fieldTable.entity
if not entity or processedData.fieldTable.exact then
return
end
local short = wikidata.short(entity, processedData.langCode)
if (short and state.groupEmpty and processedData.capitalize) or processedData.forceCapitalize then
short = mw.ustring.gsub(short, '^%l', mw.ustring.upper)
end
if not short then
return
end
result.text = short
result.wikitext = short
end
function Formatter.processField.abbrWithHint(source, processedData, result, state)
local entity = processedData.fieldTable.entity
if not entity or processedData.fieldTable.exact then
return
end
local abbr, _, ok = wikidata.abbrBiblio(entity, processedData.langCode)
if (abbr and state.groupEmpty and processedData.capitalize) or processedData.forceCapitalize then
abbr = mw.ustring.gsub(abbr, '^%l', mw.ustring.upper)
end
if abbr and ok then
abbr = mw.ustring.gsub(abbr, ' ', ' ')
end
if result.text ~= abbr then
result.wikitext = '<abbr title="' .. result.wikitext .. '">' .. abbr .. '</abbr>'
result.text = abbr
end
end
function Formatter.processField.date(source, processedData, result)
if type(processedData.fieldTable.value) ~= 'table' then
return
end
local langObj = mw.getLanguage(processedData.langCode)
result.text = langObj:formatDate('j xg Y', processedData.fieldTable.value.timestamp)
result.wikitext = result.text
end
function Formatter.processField.dateISO(source, processedData, result)
if type(processedData.fieldTable.value) ~= 'table' then
return
end
local langObj = mw.getLanguage(processedData.langCode)
result.text = langObj:formatDate('Y-m-d', processedData.fieldTable.value.timestamp)
result.wikitext = result.text
end
function Formatter.processField.uriScheme(source, processedData, result)
local entity = processedData.fieldTable.entity
if not entity then
return
end
result.text = wikidata.uriScheme(entity)
result.wikitext = result.text
end
function Formatter.processField.quantity(source, processedData, result)
local fieldTable = processedData.fieldTable
if not fieldTable.unitEntity then
return
end
local unit = wikidata.unit(fieldTable.unitEntity, processedData.langCode)
if unit then
result.text = result.text .. ' ' .. unit
result.wikitext = result.wikitext .. ' ' .. unit
end
end
local function nameFromFieldTable(nameTable)
local value = nameTable.value
local ok = true
if not value then
local label = mw.wikibase.getLabel(nameTable.entity)
if label then
value = mw.wikibase.getLabel(nameTable.entity) .. '<sup>[[d:' .. nameTable.entity .. '|?]]</sup>'
else
value = '?' .. '<sup>[[d:' .. nameTable.entity .. '|?]]</sup>'
end
ok = false
end
return value, ok
end
local function nameToInitial(nameTable)
local value = nameTable.value
local ok
if not value then
local label = mw.wikibase.getLabel(nameTable.entity)
if label then
value = label .. '<sup>[[d:' .. nameTable.entity .. '|?]]</sup>'
else
value = '?' .. '<sup>[[d:' .. nameTable.entity .. '|?]]</sup>'
end
ok = false
else
value = mw.ustring.sub(value, 1, 1) .. '.'
ok = true
end
return value, ok
end
local function namesToInitial(nameTables)
local value, ok
if table.getn(nameTables) == 0 then
value, ok = nameToInitial(nameTables)
else
value = ''
ok = true
for i, nameTable in ipairs(nameTables) do
if i > 1 then
value = value .. ' '
end
local currValue, currOk = nameToInitial(nameTable)
value = value .. currValue
ok = ok and currOk
end
end
return value, ok
end
function Formatter.processField.person(source, processedData, result)
-- currently supports only names with givenName and pathronym/mathronym,
-- middle names are not supported if they are a part of givenName
local fieldTable = processedData.fieldTable
local name
if fieldTable.exact then
name = result.text
if name:match(',') then
name = name:gsub('([^,]+), *(.+)', '%2 %1')
result.text = name
result.wikitext = name
end
return
end
if fieldTable.components and fieldTable.components.familyName and fieldTable.components.givenName then
local ok
name, ok = namesToInitial(fieldTable.components.givenName)
if fieldTable.components.ancestorName then
local ancestorName, ancestorNameOk = namesToInitial(fieldTable.components.ancestorName)
name = name .. ' ' .. ancestorName
ok = ok and ancestorNameOk
end
local familyName, familyNameOk = nameFromFieldTable(fieldTable.components.familyName)
name = name .. ' ' .. familyName
ok = ok and familyNameOk
if not ok then
result.linked = true
end
else
local entity = processedData.fieldTable.entity
if entity then
name = wikidata.abbr(entity, processedData.langCode)
else
name = processedData.value
end
if not name then
name = mw.wikibase.getLabel(entity) .. '<sup>[[d:' .. entity .. '|?]]</sup>'
end
end
result.text = name
result.wikitext = result.text
end
function Formatter.processField.personReversed(source, processedData, result)
-- currently supports only names with givenName and pathronym/mathronym,
-- middle names are not supported if they are a part of givenName
local fieldTable = processedData.fieldTable
local name
if fieldTable.exact then
name = result.text
if not name:match(',') then
local initials, familyName = mw.ustring.match(name, '^(.+%.) ([^%. ]+)$')
if not initials or not familyName then
return
end
name = familyName .. ', ' .. initials
result.text = name
result.wikitext = name
end
return
end
if fieldTable.components and fieldTable.components.familyName and fieldTable.components.givenName then
local ok
local familyName, ok = nameFromFieldTable(fieldTable.components.familyName)
local givenName, givenNameOk = namesToInitial(fieldTable.components.givenName)
ok = ok and givenNameOk
name = familyName .. ', ' .. givenName
if fieldTable.components.ancestorName then
local ancestorName, ancestorNameOk = namesToInitial(fieldTable.components.ancestorName)
ok = ok and ancestorNameOk
name = name .. ' ' .. ancestorName
end
if not ok then
result.linked = true
end
else
local entity = processedData.fieldTable.entity
if entity then
name = wikidata.abbr(entity, processedData.langCode)
else
return
end
if not name then
name = mw.wikibase.getLabel(entity) .. '<sup>[[d:' .. entity .. '|?]]</sup>'
end
end
result.text = name
result.wikitext = result.text
end
function Formatter.processField.personReversedNoComma(source, processedData, result)
-- currently supports only names with givenName and pathronym/mathronym,
-- middle names are not supported if they are a part of givenName
local fieldTable = processedData.fieldTable
local name
if fieldTable.exact then
name = result.text
if name:match(',') then
name = name:gsub('([^,]+), *(.+)', '%2 %1')
result.text = name
result.wikitext = name
else
local initials, familyName = mw.ustring.match(name, '^(.+%.) ([^%. ]+)$')
if not initials or not familyName then
return
end
name = familyName .. ' ' .. initials
result.text = name
result.wikitext = name
end
return
end
if fieldTable.exact then
name = result.text
if name:match(',') then
name = name:gsub('([^,]+), *(.+)', '%2 %1')
result.text = name
result.wikitext = name
end
return
end
if fieldTable.components and fieldTable.components.familyName and fieldTable.components.givenName then
local ok
local familyName, ok = nameFromFieldTable(fieldTable.components.familyName)
local givenName, givenNameOk = namesToInitial(fieldTable.components.givenName)
ok = ok and givenNameOk
name = familyName .. ' ' .. givenName
if fieldTable.components.ancestorName then
local ancestorName, ancestorNameOk = namesToInitial(fieldTable.components.ancestorName)
ok = ok and ancestorNameOk
name = name .. ' ' .. ancestorName
end
if not ok then
result.linked = true
end
else
local entity = processedData.fieldTable.entity
if entity then
name = wikidata.abbr(entity, processedData.langCode)
else
return
end
if not name then
name = mw.wikibase.getLabel(entity) .. '<sup>[[d:' .. entity .. '|?]]</sup>'
end
end
result.text = name
result.wikitext = result.text
end
function Formatter.processField.lowercase(source, processedData, result)
result.text = mw.ustring.lower(result.text)
result.wikitext = mw.ustring.lower(result.wikitext)
end
function Formatter.processField.wikisource(source, processedData, result)
if result.linked then
return
end
local entity = processedData.fieldTable.entity
if not entity then
return
end
local title = mw.wikibase.getSitelink(entity, processedData.langCode .. 'wikisource')
if not title then
return
end
result.wikitext = '[[s:' .. title .. '|' .. result.text .. ']]'
result.linked = true
end
function Formatter.processField.wikiversity(source, processedData, result)
if result.linked then
return
end
local entity = processedData.fieldTable.entity
if not entity then
return
end
local title = mw.wikibase.getSitelink(entity, processedData.langCode .. 'wikiversity')
if not title then
return
end
result.wikitext = '[[s:' .. title .. '|' .. result.text .. ']]'
result.linked = true
end
function Formatter.processField.wikibooks(source, processedData, result)
if result.linked then
return
end
local entity = processedData.fieldTable.entity
if not entity then
return
end
local title = mw.wikibase.getSitelink(entity, processedData.langCode .. 'wikibooks')
if not title then
return
end
result.wikitext = '[[s:' .. title .. '|' .. result.text .. ']]'
result.linked = true
end
function Formatter.processField.link(source, processedData, result)
if result.linked or not processedData.url then
return
end
result.wikitext = '[' .. processedData.url .. ' ' .. result.wikitext .. ']'
result.linked = true
end
function Formatter.processField.safeLink(source, processedData, result)
if result.linked or not processedData.url then
return
end
if mw.ustring.match(result.wikitext, '%[') then
return
end
result.wikitext = '[' .. processedData.url .. ' ' .. result.wikitext .. ']'
result.linked = true
end
function Formatter.processField.wikilink(source, processedData, result)
if result.linked then
return
end
if processedData.wikilink then
result.wikitext = '[[' .. processedData.wikilink .. '|' .. result.wikitext .. ']]'
return
end
local entity = processedData.fieldTable.entity
if not entity then
return
end
if result.state.linkedEntities[entity] then
return
end
local wikitext
wikitext, entity = wikidata.base.wikilink(entity, result.wikitext)
if result.state.linkedEntities[entity] or not entity then
return
end
result.wikitext = wikitext
result.state.linkedEntities[entity] = true
result.linked = true
end
function Formatter.processField.resolveWikilink(source, processedData, result)
if result.linked then
return
end
if processedData.wikilink then
result.wikitext = '[[' .. processedData.wikilink .. '|' .. result.wikitext .. ']]'
return
end
local entity = processedData.fieldTable.entity
if not entity then
return
end
if result.state.linkedEntities[entity] then
return
end
local wikitext
wikitext, entity = wikidata.base.wikilink(entity, result.wikitext)
if not entity then
entity = wikidata.base.resolveParent(processedData.fieldTable.entity)
if not entity then
return
end
wikitext, entity = wikidata.base.wikilink(entity, result.wikitext)
end
if not entity or result.state.linkedEntities[entity] then
return
end
result.wikitext = wikitext
result.state.linkedEntities[entity] = true
result.linked = true
end
function Formatter.processField.valuableWikidata(source, processedData, result)
if result.linked then
return
end
local entity = processedData.fieldTable.entity
if not entity or result.state.linkedEntities[entity] then
return
end
local entityObj = mw.wikibase.getEntity(entity)
if entityObj.sitelinks then
result.wikitext = result.wikitext .. '<sup>[[d:' .. entity .. '|[d]]]</sup>'
result.state.linkedEntities[entity] = true
result.linked = true
end
end
function Formatter.processField.wikidata(source, processedData, result)
if result.linked then
return
end
local entity = processedData.fieldTable.entity
if not entity or result.state.linkedEntities[entity] then
return
end
result.wikitext = result.wikitext .. '<sup class="noprint">[[d:' .. entity .. '|[d]]]</sup>'
result.state.linkedEntities[entity] = true
result.linked = true
end
function Formatter.processField.forceWikidata(source, processedData, result)
local entity = processedData.fieldTable.entity
if not entity then
return
end
result.wikitext = result.wikitext .. '<sup>[[d:' .. entity .. '|[d]]]</sup>'
result.state.linkedEntities[entity] = true
result.linked = true
end
function Formatter.processField.wikidataLink(source, processedData, result)
if result.linked then
return
end
local entity = processedData.fieldTable.entity
if not entity then
return
end
result.wikitext = '[[d:' .. entity .. '|' .. result.wikitext .. ']]'
result.linked = true
end
function Formatter.processField.forceWikidataLink(source, processedData, result)
local entity = processedData.fieldTable.entity
if not entity then
return
end
result.wikitext = '[[d:' .. entity .. '|' .. result.wikitext .. ']]'
result.linked = true
end
function Formatter.processField.entity(source, processedData, result)
local entity = processedData.fieldTable.entity
if not entity then
return
end
result.text = entity
result.wikitext = entity
end
local function fieldTableByPath(source, path)
if type(path) == 'table' then
local currField = nil
local currFieldComponents = source
for _, currFieldName in ipairs(path) do
if type(currFieldName) == 'string' then
if not currFieldComponents then
return nil
end
currField = currFieldComponents[currFieldName]
else
currField = currField[currFieldName]
end
if not currField then
return nil
end
currFieldComponents = currField.components
end
return currField
end
return source[path]
end
local function fieldValueByPath(source, path)
local t = fieldTableByPath(source, path)
if not t then
return nil
end
if type(path) == 'table' and path.sub then
if not t.value then
return nil
end
return t.value[path.sub]
end
return t.value
end
local function urlFromMaskByPart(part, source, fieldValue, langCode)
if part.urlMaskProp then
local urlMask = wikidata.urlMask(part.urlMaskProp, langCode)
if urlMask then
return urlMask:gsub('%$1', fieldValue)
end
elseif part.urlField then
local urlTable = source[part.urlField]
if urlTable then
if type(urlTable) == 'table' then
return urlTable.value
else
return urlTable
end
end
end
return nil
end
local function wikilinkFromMaskByPart(part, source, fieldValue)
if part.wikilinkMask then
return part.wikilinkMask:gsub('%$1', fieldValue)
end
return nil
end
function Formatter:forceLangByPart(part, source)
local langCode
if not part.forceLang then
langCode = self.lang
end
if langCode then
return langCode
elseif part.forceLang == 'fallback' then
langCode = 'en'
elseif part.forceLang == 'default' or not part.forceLang then
local defaultLangObj = mw.getContentLanguage()
local defaultLang = defaultLangObj:getCode()
langCode = defaultLang
else
langCode = part.forceLang
end
return langCode
end
-- Format field with all neccessary data supplied by arguments.
function Formatter:formatField(source, part, fieldTable, fieldValue, state)
local processedData = {
field = part.field,
wikilink = wikilinkFromMaskByPart(part, source, fieldValue),
langCode = self:forceLangByPart(part, source),
fieldTable = fieldTable,
capitalize = (part.capitalize or (part.capitalize == nil)) and (state.index == 1),
forceCapitalize = (part.capitalize == true and state.index == 1),
}
processedData.url = urlFromMaskByPart(part, source, fieldValue, processedData.langCode)
local value = fieldValue
local linked = false
if fieldTable.entity and (processedData.langCode ~= self.lang or not value) then
if processedData.langCode then
value = mw.wikibase.getLabelByLang(fieldTable.entity, processedData.langCode)
if not value then
value = mw.wikibase.getLabel(fieldTable.entity)
if value then
value = value .. '<sup>[[d:' .. fieldTable.entity .. '|?]]</sup>'
linked = true
end
end
else
value = mw.wikibase.getLabel(fieldTable.entity)
end
end
if value and type(value) == 'string' then
if (state.groupEmpty and processedData.capitalize) or processedData.forceCapitalize then
value = mw.ustring.gsub(value, '^%l', mw.ustring.upper)
end
end
if type(value) == 'table' then
value = dump(value)
elseif value == nil then
value = '<b><s>(nil)</s></b>'
end
processedData.value = value
local result = {
wikitext = value,
text = value,
state = state,
linked = linked,
}
if not part.format then
state.groupEmpty = false
return result
end
for _, formatFunc in ipairs(part.format) do
formatFunc(source, processedData, result, state)
end
state.groupEmpty = false
return result
end
-- Used if fieldTable contains array.
function Formatter:formatFieldAsArray(source, part, fieldTable, state)
local text = ''
local wikitext = ''
local count = table.getn(fieldTable)
local cutCount = 0
if part.limits and count > part.limits.max then
cutCount = count - part.limits.cutTo
count = part.limits.cutTo
end
local delimiter = part.itemsDelimiter
if not delimiter then
delimiter = ', '
end
for i=1, count do
state.index = i
local currFieldTable = fieldTable[i]
local currResult = self:formatField(source, part, currFieldTable, currFieldTable.value, state)
wikitext = wikitext .. currResult.wikitext
text = text .. currResult.text
if i < count then
text = text .. delimiter
wikitext = wikitext .. delimiter
end
end
if cutCount > 0 then
local cutText = ''
for i=count + 1, count + cutCount do
state.index = i
local currFieldTable = fieldTable[i]
local currResult = self:formatField(source, part, currFieldTable, currFieldTable.value, state)
cutText = cutText .. currResult.text
if i < count + cutCount then
cutText = cutText .. delimiter
end
end
local processedData = {
field = part.field,
langCode = self:forceLangByPart(part, source),
fieldTable = fieldTable,
}
local othersText, othersWikitext = part.limits.replaceBy(source, processedData, cutText)
text = text .. othersText
wikitext = wikitext .. othersWikitext
end
return text, wikitext
end
local function fieldTableAndValueByPart(source, part)
if part.entity or part.value then
return { entity = part.entity, value = part.value }, part.value
else
return fieldTableByPath(source, part.field), fieldValueByPath(source, part.field)
end
end
-- Formats the field and returns resulting text.
function Formatter:formatFieldComponents(source, part, state)
local fieldTable, fieldValue = fieldTableAndValueByPart(source, part)
if type(fieldTable) ~= 'table' then
error('Field ' .. dump(part.field) .. ' is not a table. Its type is ' .. type(fieldTable))
end
local text
local wikitext
if table.getn(fieldTable) > 0 then
text, wikitext = self:formatFieldAsArray(source, part, fieldTable, state)
else
state.index = 1
local result = self:formatField(source, part, fieldTable, fieldValue, state)
text = result.text
wikitext = result.wikitext
end
return { text = text, wikitext = wikitext }
end
-- Format single field by its data.
function Formatter:commonFormatField(part, parentContainer, source)
local result = self:formatFieldComponents(source, part, self.state)
parentContainer:wikitext(result.wikitext)
self.lastAddedText = result.text
self.state.empty = false
end
function Formatter:ensureEndsWith(endsText, parentContainer)
if self.lastAddedText and endsText then
local len = mw.ustring.len(endsText)
if mw.ustring.sub(self.lastAddedText, -len) ~= endsText then
parentContainer:wikitext(endsText)
end
end
end
function Formatter:ensureEndsAndAddDelimiter(group, part, parentContainer, groupEmpty, source)
if self.state.empty or self.state.delimiterAdded then
return
end
local delimiter = part.delimiter
local endsText = part.ensureEnds
if (delimiter == nil or groupEmpty) and not part.forceDelimiter then
if group.childDelimiter or group.childEnsureEnds then
delimiter = group.childDelimiter
endsText = group.childEnsureEnds
end
end
if not delimiter and not endsText then
return
end
self:ensureEndsWith(endsText, parentContainer)
if type(delimiter) == 'function' then
delimiter = delimiter(source)
end
parentContainer:wikitext(delimiter)
self.state.delimiterAdded = true
end
-- Checks whether the field is set in the source.
local function fieldExists(source, path)
local t = fieldTableByPath(source, path)
if not t then
return false
end
if type(path) == 'table' and path.sub then
if not t.value then
return false
end
return (t.value[path.sub] ~= nil)
end
return true
end
local fieldConflicts, fieldDepends
fieldConflicts = function(conflicts, source)
if not conflicts then
return false
end
if type(conflicts) == 'table' and not conflicts.isPath then
for _, currField in ipairs(conflicts) do
if type(currField) == 'string' or currField.isPath then
if fieldExists(source, currField) then
return true
end
else
if fieldDepends(currField, source) then
return true
end
end
end
else
return fieldExists(source, conflicts)
end
return false
end
fieldDepends = function(depends, source)
if not depends then
return true
end
if type(depends) == 'table' and not depends.isPath then
for _, currField in ipairs(depends) do
if type(currField) == 'string' or currField.isPath then
if not fieldExists(source, currField) then
return false
end
else
if not fieldConflicts(currField, source) then
return false
end
end
end
else
return fieldExists(source, depends)
end
return true
end
-- Look-a-head for groups by data row that can be displayed within
-- specified group. For the single field returns true if it can be displayed.
function Formatter:groupRowIsAvailable(group, childGroups, localSource)
if table.getn(childGroups) > 0 then
local found = false
for _, subGroup in ipairs(childGroups) do
if not subGroup.isStatic then
if self:groupIsAvailable(subGroup, localSource) then
found = true
break
end
end
end
if group.cond and not group.cond(localSource, self.params) then
return false
end
return found
end
if not fieldExists(localSource, group.field) and not group.entity and group.value == nil then
return false
end
-- TODO: тест на удаление этого условия
if group.cond and not group.cond(localSource, self.params) then
return false
end
return true
end
-- Look-a-head for groups and data that can be displayed within specified group.
-- For the single field returns true if it can be displayed.
function Formatter:groupIsAvailable(group, source)
if fieldConflicts(group.conflicts, source) then
return false
end
if not fieldDepends(group.depends, source) then
return false
end
local childGroups = group.groups or group
local hasChildGroups = (table.getn(childGroups) > 0)
local localSource
if hasChildGroups and group.field then
localSource = source[group.field]
else
localSource = source
end
if not localSource then
error(tostring(group.field))
end
if table.getn(localSource) > 0 then
local found = false
if childGroups then
for _, row in ipairs(localSource) do
if not group.cond or group.cond(row, self.params) then
for _, subGroup in ipairs(childGroups) do
if not subGroup.isStatic then
if self:groupIsAvailable(subGroup, row) then
found = true
break
end
end
end
end
if found then
break
end
end
else
if not group.entity and group.value == nil then
for _, row in ipairs(localSource) do
if fieldExists(row, group.field) then
found = true
break
end
end
end
end
return found
end
return self:groupRowIsAvailable(group, childGroups, source)
end
-- Formats the group and puts it into the parentContainer.
function Formatter:formatGroup(parentGroup, group, parentContainer, source)
if not self:groupIsAvailable(group, source) then
return
end
local state = self.state
local groupEmpty = state.groupEmpty
local childGroups = group.groups or group
local hasChildGroups = (table.getn(childGroups) > 0)
if hasChildGroups and not group.passthrough then
state.groupEmpty = true
end
local localSource
if group.field and hasChildGroups then
localSource = source[group.field]
else
localSource = source
end
local prefix = group.prefix
if parentContainer then
self:ensureEndsAndAddDelimiter(parentGroup, group, parentContainer, state.groupEmpty, localSource)
end
if prefix and parentContainer then
parentContainer:wikitext(prefix)
self.lastAddedText = self.lastAddedText .. prefix
end
local groupContainer
if hasChildGroups then
if table.getn(localSource) > 0 then
groupContainer = parentContainer
for _, row in ipairs(localSource) do
if not group.cond or group.cond(row, self.params) then
if self:groupRowIsAvailable(group, childGroups, row) then
local currContainer = self:tagToContainer(group.tag, groupContainer, source) or parentContainer
for _, part in ipairs(childGroups) do
self:formatGroup(group, part, currContainer, row)
end
end
end
end
else
groupContainer = self:tagToContainer(group.tag, parentContainer, source) or parentContainer
for _, part in ipairs(childGroups) do
self:formatGroup(group, part, groupContainer, localSource)
end
end
else
groupContainer = self:tagToContainer(group.tag, parentContainer, source) or parentContainer
self:commonFormatField(group, groupContainer, localSource)
self.state.delimiterAdded = false
end
if not state.groupEmpty then
local suffix = group.suffix
if suffix and parentContainer then
parentContainer:wikitext(suffix)
self.lastAddedText = self.lastAddedText .. suffix
end
end
return groupContainer
end
function Formatter:format()
local rootContainer = self:formatGroup(nil, self.profile, self.container, self.source)
self:ensureEndsWith(self.profile.ensureEnds, rootContainer)
end
function Formatter:getAsText()
return tostring(self.container:allDone())
end
function p.format(profile, source, langCode, params)
-- Temporary solution for backward compatibility
if type(langCode) == 'table' then
params = langCode
langCode = 'ru'
end
if not langCode and source.langCode then
langCode = source.langCode.value
end
local f = Formatter:new(profile, source, langCode, params)
f:format()
return f:getAsText()
end
p.f = Formatter.processField
return p