Для документации этого модуля может быть создана страница Модуль:WDFormat/doc

require('strict')

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 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)
	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)
	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.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.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 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)

	if type(delimiter) == 'function' then
		delimiter = delimiter(self.source)
	end
	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 fieldConflicts, fieldDepends

fieldConflicts = function(conflicts, source)
	if not conflicts then
		return false
	end

	if type(conflicts) == 'table' then
		for _, currField in ipairs(conflicts) do
			if type(currField) == 'string' then
				if fieldExists(currField, source) then
					return true
				end
			else
				if fieldDepends(currField, source) then
					return true
				end
			end
		end
	else
		if fieldExists(conflicts, source) then
			return true
		end
	end
	
	return false
end

fieldDepends = function(depends, source)
	if not depends then
		return true
	end

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

	return true
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

	if fieldConflicts(part.conflicts, source) then
		return false
	end

	if not fieldDepends(part.depends, source) then
		return false
	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