Модуль: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


В качестве интеграционных тестов использованы тесты модуля, который работает на основе текущего модуля.

Тесты пройдены.

План разработки[править код]

См. также[править код]

  • WDBackend — модуль получения информации из Викиданных по задаваемой схеме.
  • WDSource — модуль получения информации об источнике из соответствующего элемента Викиданных.
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 .. '&nbsp;' .. 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 .. '&nbsp;'
			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 .. '&nbsp;' .. ancestorName
			ok = ok and ancestorNameOk
		end

		local familyName, familyNameOk = nameFromFieldTable(fieldTable.components.familyName)
		name = name .. '&nbsp;' .. 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 .. ',&nbsp;' .. 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 .. ',&nbsp;' .. givenName
		if fieldTable.components.ancestorName then
			local ancestorName, ancestorNameOk = namesToInitial(fieldTable.components.ancestorName)
			ok = ok and ancestorNameOk
			name = name .. '&nbsp;' .. 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&nbsp;%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 .. '&nbsp;' .. 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 .. '&nbsp;' .. givenName
		if fieldTable.components.ancestorName then
			local ancestorName, ancestorNameOk = namesToInitial(fieldTable.components.ancestorName)
			ok = ok and ancestorNameOk
			name = name .. '&nbsp;' .. 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 .. '|&#91;d&#93;]]</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 .. '|&#91;d&#93;]]</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 .. '|&#91;d&#93;]]</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