Это не официальный сайт wikipedia.org 01.01.2023

Модуль:WDFormat — Википедия
Документация

Модуль предназначен для форматирования набора данных, полученных из Викиданных посредством модуля WDBackend или заданных вручную. Форматирование может осуществляться в произвольной форме, как в строчку, так и в табличном виде.

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

ИспользованиеПравить

Модуль является библиотечным и предназначен для использования в других модулях. Данный модуль не предназначен для использования в статьях или других шаблонах напрямую через вызов #invoke.

Требуемое форматирование описывается профилем, представляющим собой таблицу Lua. Профиль описывает представление отдельных полей, то есть то, как они должны отображаться. Недостающий функционал реализуется указанием в профиле собственных функций, принимающих определённый набор аргументов и возвращающих определённый результат.

Для форматирования профиля в совокупности с передаваемым набором данных необходимо использовать функцию format(). В качестве результата возвращается отформатированный викитекст.

Для примера использования см. модуль Модуль:CiteGost.

Формат профиля в общем видеПравить

{
    -- Корневой тег:
    tag = {
        name = 'Имя тега',
        classes = { 'class1', 'class2', ... },
        attr = { атрибут1='значение1', атрибут2='значение2', ... },
        tag = Вложенный тег,
    },
    -- Группы:
    groups = {
        -- Начало 1-й группы:
        {
            -- Тег группы:
            tag = {
                name = 'Имя тега',
                classes = { 'class1', 'class2', ... },
                attr = { атрибут1='значение1', атрибут2='значение2', ... },
                tag = Вложенный тег
            },
            ensureEnds = 'Символ/текст, которым должны заканчиваться поля группы',
            delimiter = 'Разделитель полей в группе',
            prefix = 'Текст до начала группы',
            -- Поля группы:
            parts = {
                -- 1-е поле группы:
                {
                    -- Тег поля:
                    tag = {
                        name = 'Имя тега',
                        classes = { 'class1', 'class2', ... },
                        attr = { атрибут1='значение1', атрибут2='значение2', ... },
                        tag = Вложенный тег
                    },
                    ensureEnds = 'Символ/текст, которым должен заканчиваться предшествующий текст',
                    delimiter = 'Разделитель, отделяющий текущий элемент от предыдущего',
                    -- Функция, разрешающая отображение поля:
                    cond = функция,
                    prefix = 'Текст до поля',
                    field = 'Название поля',
                    urlMaskProp = 'P-идентификатор свойства, отвечающего за маску ссылки',
                    -- Функции, через которые поле будет отформатировано:
                    format = { функция1, функция2, ... },
                    suffix = 'Текст после поля',
                },
                -- Остальные поля:
                ...
            },
            suffix = 'Текст в конце группы',
        },
        -- Остальные группы:
        ...
    },
    ensureEnds = 'Окончение форматированных данных, например, точка.'
}

Функции форматирования полейПравить

Форматирование поля задаётся через команду format с указанием функции форматирования, принимающей определённый набор аргументов. Доступны следующие встроенные функции:

  • numericalRanges — форматирование диапазона чисел (корректирует знак диапазона);
  • dash — оформление тире в тексте;
  • unit — получение единицы измерения у элемента Викиданных;
  • abbr — получить сокращённое обозначение (есть ограничения) у элемента Викиданных;
  • short — получить короткое название у элемента Викиданных;
  • abbrWithHint — получить сокращённое обозначение (есть ограничения) у элемента Викиданных с расшифровкой в подсказке;
  • date — форматирование даты;
  • quantity — форматирование количества с указанной в нём единицей измерения;
  • entity — получить значение идентификатора элемента Викиданных;
  • wikilink — оформить поле Викиссылкой, если это возможно;
  • wikisource — оформить поле ссылкой на Викитеку, если в элементе Викиданных указана соответствующая статья;
  • link — оформить поле внешней ссылкой, если это возможно;
  • wikidata — добавление к полю надстрочной ссылки на элемент Викиданных, если хотя бы в одном языковом разделе есть статья по теме;
  • wikidataLink — оформить поле ссылкой на элемент Викиданных, который с полем связан.

Внесение измененийПравить

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

Добавление нового функционала рекомендуется делать у себя в песочнице, скопировав в неё модуль. В правке копирования необходимо указать тот факт, что делается копирование, и сделать ссылку на оригинальный модуль в виде викитекста. При добавлении нового функционала сначала желательно добавить тест на этот функционал, затем добавить сам функционал, убедившись, что все тесты при этом проходят.

ТестыПравить

✔ Все тесты пройдены.

Название Ожидается Фактически
✔ test_format_array
✔ test_format_arrayCapitalize
✔ test_format_arrayForceCapitalize
✔ test_format_capitalize
✔ test_format_date
✔ test_format_ensureEndsAndDelimiter
✔ test_format_innerTags
✔ test_format_link
✔ test_format_linkOrder
✔ 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 — модуль получения информации об источнике из соответствующего элемента Викиданных.
local p = {}
local wikidata = require('Модуль: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 = {},
}

function Formatter:tagToContainer(tag, parentContainer)
	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(self.source)
				end
				container:attr(attr, value)
			end
		end
		
		tag = tag.tag
	end

	return container
end

function Formatter:new(profile, source)
	local obj = {}
	setmetatable(obj, self)
	self.__index = self

	self.profile = profile
	self.source = source
	self.state = {
		empty = true,
		linkedEntities = {},
	}
	self.lastAddedText = ''
	self.container = self:tagToContainer(profile.tag)
	assert(self.container ~= 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.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 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 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 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)
	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.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
		value = mw.wikibase.getLabel(nameTable.entity) .. '<sup>[[d:' .. nameTable.entity .. '|?]]</sup>'
		ok = false
	end
	return value, ok
end

local function nameToInitial(nameTable)
	local value = nameTable.value
	local ok
	if not value then
		value = mw.wikibase.getLabel(nameTable.entity) .. '<sup>[[d:' .. nameTable.entity .. '|?]]</sup>'
		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.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.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.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.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.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.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

	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.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.entity(source, processedData, result)
	local entity = processedData.fieldTable.entity
	if not entity then
		return
	end

	result.text = entity
	result.wikitext = entity
end

local function fieldValueByPath(source, path)
	if type(path) == 'table' then
		local currField = source
		for _, pathEntry in ipairs(path) do
			currField = currField[pathEntry]
		end
		if not currField.value then
			return nil
		end
		if path.sub then
			return currField.value[path.sub]
		end
		return currField.value
	end

	local fieldValue = source[path]
	if not fieldValue then
		return nil
	end
	return fieldValue.value
end

local function fieldTableByPath(source, path)
	if type(path) == 'table' then
		local currField = source
		for _, pathEntry in ipairs(path) do
			currField = currField[pathEntry]
		end
		return currField
	end

	return source[path]
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

local function forceLangByPart(part, source)
	local langCode
	if not part.forceLang then
		langCode = source.langCode
		if type(langCode) == 'table' then
			langCode = langCode.value
		end
	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

local function formatField(source, part, fieldTable, fieldValue, state)
	local processedData = {
		field = part.field,
		wikilink = wikilinkFromMaskByPart(part, source, fieldValue),
		langCode = 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 sourceLangCode
	if source.langCode then
		sourceLangCode = source.langCode.value
	end
	local linked = false
	if fieldTable.entity and (processedData.langCode ~= sourceLangCode 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

local function 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 = 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 = 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 = 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

local function 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 = formatFieldAsArray(source, part, fieldTable, state)
	else
		state.index = 1
		local result = formatField(source, part, fieldTable, fieldValue, state)
		text = result.text
		wikitext = result.wikitext
	end

	return { text = text, wikitext = wikitext }
end

function Formatter:commonFormatField(part, parentContainer)
	self:addPrefix(part)

	local result = formatFieldComponents(self.source, part, self.state)

	local container = self:tagToContainer(part.tag, parentContainer)
	container:wikitext(result.wikitext)
	self.lastAddedText = result.text

	self.state.empty = false

	self:addSuffix(part)
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)
	if self.state.empty then
		return
	end

	local delimiter = part.delimiter
	local endsText = part.ensureEnds
	if (delimiter == nil or groupEmpty) and not part.forceDelimiter then
		if group.delimiter or group.ensureEnds then
			delimiter = group.delimiter
			endsText = group.ensureEnds
		end
	end

	self:ensureEndsWith(endsText, parentContainer)

	parentContainer:wikitext(delimiter)
end

function Formatter:addPrefix(part)
	local prefix = part.prefix
	if not prefix then
		return
	end

	self.container:wikitext(prefix)
end

function Formatter:addSuffix(part)
	local suffix = part.suffix
	if not suffix then
		return
	end

	self.container:wikitext(suffix)
	self.lastAddedText = self.lastAddedText .. suffix
end

local function fieldExists(field, source)
	if type(field) == 'table' then
		local specified = false
		local currField = source
		for _, currFieldName in ipairs(field) do
			currField = currField[currFieldName]
			if not currField then
				return false
			else
				specified = true
			end
		end
		if field.sub then
			if not currField.value then
				return false
			end
			currField = currField.value[field.sub]
			if not currField then
				return false
			end
		end
		return specified
	end

	return (source[field] ~= nil)
end

local function partIsAvailable(part, source)
	if part.parts then
		for i, subpart in ipairs(part.parts) do
			if partIsAvailable(subpart, source) then
				return true
			end
		end
		return false
	end
	if not fieldExists(part.field, source) and not part.entity and not part.value then
		return false
	end

	local conflicts = part.conflicts
	if conflicts then
		if type(conflicts) == 'table' then
			for _, currField in ipairs(conflicts) do
				if fieldExists(currField, source) then
					return false
				end
			end
		else
			if fieldExists(conflicts, source) then
				return false
			end
		end
	end

	local depends = part.depends
	if depends then
		if type(depends) == 'table' then
			for _, currField in ipairs(depends) do
				if not fieldExists(currField, source) then
					return false
				end
			end
		else
			if not fieldExists(depends, source) then
				return false
			end
		end
	end

	if part.cond and not part.cond(source) then
		return false
	end

	return true
end

function Formatter:formatGroup(group, parentContainer)
	local state = self.state
	state.groupEmpty = true
	local groupContainer
	for _, part in ipairs(group.parts) do
		if partIsAvailable(part, self.source) then
			if not groupContainer then
				self:ensureEndsAndAddDelimiter(group, part, parentContainer, state.groupEmpty)
				groupContainer = self:tagToContainer(group.tag, parentContainer) or parentContainer
				local prefix = group.prefix
				if prefix then
					groupContainer:wikitext(prefix)
					self.lastAddedText = self.lastAddedText .. prefix
				end
			else
				self:ensureEndsAndAddDelimiter(group, part, groupContainer, state.groupEmpty)
			end

			if part.parts then
				self:formatGroup(part, groupContainer)
			else
				self:commonFormatField(part, groupContainer)
			end
		end
	end
	if not state.groupEmpty then
		local suffix = group.suffix
		if suffix then
			groupContainer:wikitext(suffix)
			self.lastAddedText = self.lastAddedText .. suffix
		end
	end
end

function Formatter:formatGroups(groups, parentContainer)
	for _, group in ipairs(groups) do
		self:formatGroup(group, parentContainer)
	end
end

function Formatter:format()
	local groups = self.profile.groups
	self:formatGroups(groups, self.container)

	self:ensureEndsWith(self.profile.ensureEnds, self.container)
end

function Formatter:getAsText()
	return tostring(self.container:allDone())
end

function p.format(profile, source)
	local f = Formatter:new(profile, source)
	f:format()
	return f:getAsText()
end

p.f = Formatter.processField

return p