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

Модуль:Wikidata/positions

Материал из вики «Вече»

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

local p = {}

local config = require('Module:Wikidata/positions/config')

local linkFallbackProperties = config.linkFallbackProperties
local topPriorityClasses = config.topPriorityClasses
local highPriorityClasses = config.highPriorityClasses
local lowerPrioritySubclasses = config.lowerPrioritySubclasses
local lowPrioritySubclasses = config.lowPrioritySubclasses
local ignoredPositionProperties = config.ignoredPositionProperties
local ignoredEntities = config.ignoredEntities
local maleGender = config.maleGender
local femaleGender = config.femaleGender
local actingRoleIds = config.actingRoleIds

local entityCache = {}
local sitelinkCache = {}
local priorityCache = {}

local function isUsableStatement(statement)
	return statement and statement.rank ~= 'deprecated'
end

local function getEntityId(snak)
	if not snak or snak.snaktype ~= 'value' or not snak.datavalue then
		return nil
	end

	local value = snak.datavalue.value
	return type(value) == 'table' and value.id or nil
end

local function hasIgnoredPositionProperty(entity)
	if not entity or not entity.claims then
		return false
	end

	for _, propertyId in ipairs(ignoredPositionProperties) do
		if entity.claims[propertyId] then
			return true
		end
	end

	return false
end

local function getEntity(id)
	if not id then
		return nil
	end

	if entityCache[id] ~= nil then
		return entityCache[id] or nil
	end

	local entity = mw.wikibase.getEntity(id)
	entityCache[id] = entity or false

	return entity
end

local function getSitelink(id)
	if not id then
		return nil
	end

	if sitelinkCache[id] ~= nil then
		return sitelinkCache[id] or nil
	end

	-- Current wiki is selected automatically.
	local sitelink = mw.wikibase.getSitelink(id)
	sitelinkCache[id] = sitelink or false

	return sitelink
end

local function getConfiguredEntityId(options)
	if type(options.entity) == 'string' then
		return options.entity
	end

	if type(options.entityId) == 'string' then
		return options.entityId
	end

	if type(options.from) == 'string' then
		return options.from
	end

	return nil
end

local function getSourceEntity(options)
	if type(options.entity) == 'table' then
		return options.entity
	end

	local id = getConfiguredEntityId(options)
	if id then
		return getEntity(id)
	end

	return mw.wikibase.getEntity()
end

local function getQualifierSnak(statement, property)
	local qualifiers = statement.qualifiers
	local snaks = qualifiers and qualifiers[property]

	return snaks and snaks[1] or nil
end

local function isActingStatement(statement)
	local qualifiers = statement.qualifiers or {}

	for _, property in ipairs({ 'P5102', 'P3831' }) do
		for _, snak in ipairs(qualifiers[property] or {}) do
			if actingRoleIds[getEntityId(snak)] then
				return true
			end
		end
	end

	return false
end

local function parseTimeString(time)
	if not time then
		return nil
	end

	local year, month, day = time:match('^([%+%-]?%d+)%-(%d%d)%-(%d%d)')

	if not year then
		return nil
	end

	return tonumber(year) * 10000 + tonumber(month) * 100 + tonumber(day)
end

local function getDateFromSnak(snak)
	return parseTimeString(
		snak
		and snak.datavalue
		and snak.datavalue.value
		and snak.datavalue.value.time
	)
end

local function getClaimText(claim)
	local value = claim.mainsnak
		and claim.mainsnak.datavalue
		and claim.mainsnak.datavalue.value

	if claim.rank ~= 'deprecated'
		and value
		and value.language == 'ru'
		and value.text
	then
		return value.text
	end

	return nil
end

-- Uses the name valid at the beginning of the tenure.
-- Берёт последнее название, которое пересекается со сроком должности.
local function selectDatedName(entity, property, termStart, termEnd)
	if not entity or not entity.claims then
		return nil
	end

	local selectedName = nil
	local selectedStart = -math.huge
	local undatedName = nil

	for _, claim in ipairs(entity.claims[property] or {}) do
		local text = getClaimText(claim)

		if text then
			local nameStart = getDateFromSnak(getQualifierSnak(claim, 'P580'))
			local nameEnd = getDateFromSnak(getQualifierSnak(claim, 'P582'))

			if nameStart or nameEnd then
				local overlaps =
					(not nameEnd or not termStart or nameEnd >= termStart)
					and (not nameStart or not termEnd or nameStart <= termEnd)

				if overlaps
					and (not selectedName or (nameStart or -math.huge) > selectedStart)
				then
					selectedName = text
					selectedStart = nameStart or -math.huge
				end
			elseif not undatedName then
				undatedName = text
			end
		end
	end

	return selectedName or undatedName
end

local function getSubjectGender(entity)
	if not entity or not entity.claims then
		return nil
	end

	for _, property in ipairs({ 'P21', 'P31' }) do
		for _, statement in ipairs(entity.claims[property] or {}) do
			if isUsableStatement(statement) then
				local id = getEntityId(statement.mainsnak)

				if id == maleGender or id == femaleGender then
					return id
				end
			end
		end
	end

	return nil
end

local function isSubjectDead(entity)
	if not entity or not entity.claims then
		return false
	end

	for _, statement in ipairs(entity.claims.P570 or {}) do
		if isUsableStatement(statement)
			and getDateFromSnak(statement.mainsnak)
		then
			return true
		end
	end

	return false
end

local function getFallbackSitelink(entity)
	if not entity or not entity.claims then
		return nil
	end

	for _, property in ipairs(linkFallbackProperties) do
		for _, claim in ipairs(entity.claims[property] or {}) do
			if isUsableStatement(claim) then
				local fallbackId = getEntityId(claim.mainsnak)

				if fallbackId then
					local sitelink = getSitelink(fallbackId)

					if sitelink then
						return sitelink
					end
				end
			end
		end
	end

	return nil
end

local function uppercaseFirst(text)
	if not text or text == '' then
		return text
	end

	return mw.ustring.upper(mw.ustring.sub(text, 1, 1))
		.. mw.ustring.sub(text, 2)
end

local function getPositionPriority(positionId, entity)
	if priorityCache[positionId] then
		return priorityCache[positionId]
	end

	local priority = {
		top = false,
		high = false,
		lower = false,
		low = false,
	}

	if entity and entity.claims then
		for _, statement in ipairs(entity.claims.P279 or {}) do
			if isUsableStatement(statement) then
				local classId = getEntityId(statement.mainsnak)

				if topPriorityClasses[classId] then
					priority.top = true
				end
				
				if highPriorityClasses[classId] then
					priority.high = true
				end
				
				if lowerPrioritySubclasses[classId] then
					priority.lower = true
				end
				
				if lowPrioritySubclasses[classId] then
					priority.low = true
				end
			end
		end
	end
	
	-- При пересечении списков остаётся только более высокий уровень.
	if priority.top then
		priority.high = false
		priority.lower = false
		priority.low = false
	elseif priority.high then
		priority.lower = false
		priority.low = false
	elseif priority.lower then
		priority.low = false
	end
	
	priorityCache[positionId] = priority

	return priority
end

local function newState(context, options)
	local subjectEntity = getSourceEntity(options)

	return {
		context = context,
		options = options,
		subjectEntity = subjectEntity,
		subjectId = (subjectEntity and subjectEntity.id)
			or getConfiguredEntityId(options),
		subjectGender = getSubjectGender(subjectEntity),
		subjectDead = isSubjectDead(subjectEntity),
		descriptorCache = {},
	}
end

local function getPositionDescriptor(state, statement)
	if state.descriptorCache[statement] ~= nil then
		return state.descriptorCache[statement] or nil
	end

    local positionId = getEntityId(statement.mainsnak)

    if not positionId then
        state.descriptorCache[statement] = false
        return nil
    end

	-- Если в P39 указан общий класс, а не конкретная должность, не выводим его.
	if ignoredEntities[positionId]
		or topPriorityClasses[positionId]
		or highPriorityClasses[positionId]
		or lowerPrioritySubclasses[positionId]
		or lowPrioritySubclasses[positionId]
	then
        state.descriptorCache[statement] = false
        return nil
    end

    local positionEntity = getEntity(positionId)

	if not positionEntity or hasIgnoredPositionProperty(positionEntity) then
		state.descriptorCache[statement] = false
		return nil
	end
	
	local defaultLabel = state.context.formatSnak(
		state.options,
		statement.mainsnak
	)
	local termStart = getDateFromSnak(getQualifierSnak(statement, 'P580'))
	local termEnd = getDateFromSnak(getQualifierSnak(statement, 'P582'))
	
	if not termEnd then
		termEnd = tonumber(os.date('!%Y%m%d'))
	end

	local label = selectDatedName(positionEntity, 'P1813', termStart, termEnd)

	if not label then
		label = selectDatedName(positionEntity, 'P1448', termStart, termEnd)
	end

	if not label and state.subjectGender == maleGender then
		label = selectDatedName(positionEntity, 'P3321', termStart, termEnd)
	end

	if not label and state.subjectGender == femaleGender then
		label = selectDatedName(positionEntity, 'P2521', termStart, termEnd)
	end

	-- Automatic local-wiki label selection.
	if not label then
		label = mw.wikibase.getLabel(positionId)
	end

	local formattedLabel = defaultLabel
	local groupKey = positionId .. '\30' .. positionId

	if label then
		local labelOptions = state.context.cloneOptions(state.options)
		labelOptions.text = label
		labelOptions.qualifiers = statement.qualifiers

		formattedLabel = state.context.formatSnak(
			labelOptions,
			statement.mainsnak
		)

		groupKey = positionId .. '\30' .. label

		if not getSitelink(positionId) then
			local fallbackSitelink = getFallbackSitelink(positionEntity)

			if fallbackSitelink then
				formattedLabel =
					'[['
					.. fallbackSitelink
					.. '|'
					.. uppercaseFirst(label)
					.. ']]'
			end
		end
	end

	local priority = getPositionPriority(positionId, positionEntity)

	local descriptor = {
		id = positionId,
		groupKey = groupKey,
		formattedLabel = formattedLabel,
		topPriority = priority.top,
		highPriority = priority.high,
		lowerPriority = priority.lower,
		lowPriority = priority.low,
	}

	state.descriptorCache[statement] = descriptor

	return descriptor
end

local function getAllPositionClaims(state)
	local entity = state.subjectEntity
	local statements = entity
		and entity.claims
		and entity.claims.P39
		or {}

	local claims = {}

	for _, statement in ipairs(statements) do
		if isUsableStatement(statement) then
			table.insert(claims, statement)
		end
	end

	return claims
end

local function makePositionRecord(state, statement, sourceIndex)
	local startSnak = getQualifierSnak(statement, 'P580')
	local endSnak = getQualifierSnak(statement, 'P582')

	-- Достаточно одной границы срока.
	if not startSnak and not endSnak then
		return nil
	end

	local position = getPositionDescriptor(state, statement)

	if not position then
		return nil
	end

	return {
		statement = statement,
		sourceIndex = sourceIndex,
		position = position,
		isActing = isActingStatement(statement),

		startSnak = startSnak,
		endSnak = endSnak,
		predecessorSnak = getQualifierSnak(statement, 'P1365'),
		successorSnak = getQualifierSnak(statement, 'P1366'),

		startDate = getDateFromSnak(startSnak),
		endDate = getDateFromSnak(endSnak),
	}
end

local function collectPositionRecords(state)
	local records = {}

	for sourceIndex, statement in ipairs(getAllPositionClaims(state)) do
		local record = makePositionRecord(state, statement, sourceIndex)

		if record then
			table.insert(records, record)
		end
	end

	return records
end

local function hasPreviousTerm(records, record)
	if not record.startDate then
		return false
	end

	for _, candidate in ipairs(records) do
		if candidate ~= record
			and candidate.position.groupKey == record.position.groupKey
			and candidate.isActing == record.isActing
			and candidate.endDate == record.startDate
		then
			return true
		end
	end

	return false
end

local function findNextTerm(records, record)
	if not record.endDate then
		return nil
	end

	for _, candidate in ipairs(records) do
		if candidate ~= record
			and candidate.position.groupKey == record.position.groupKey
			and candidate.isActing == record.isActing
			and candidate.startDate == record.endDate
		then
			return candidate
		end
	end

	return nil
end

local function buildMergedEntry(records, firstRecord)
	local lastRecord = firstRecord
	local used = {
		[firstRecord] = true,
	}

	while true do
		local nextRecord = findNextTerm(records, lastRecord)

		if not nextRecord or used[nextRecord] then
			break
		end

		used[nextRecord] = true
		lastRecord = nextRecord
	end

	return {
		statement = firstRecord.statement,
		sourceIndex = firstRecord.sourceIndex,
		position = firstRecord.position,
		isActing = firstRecord.isActing,

		startSnak = firstRecord.startSnak,
		endSnak = lastRecord.endSnak,

		predecessorSnak = firstRecord.predecessorSnak,
		successorSnak = lastRecord.successorSnak,

		startDate = firstRecord.startDate,
		endDate = lastRecord.endDate,
	}
end

local function buildMergedEntries(records)
	local entries = {}

	for _, record in ipairs(records) do
		if not hasPreviousTerm(records, record) then
			table.insert(entries, buildMergedEntry(records, record))
		end
	end

	return entries
end

-- Same QID + same selected title become one group.
local function buildPositionGroups(entries)
	local groupsByKey = {}
	local groups = {}

	for _, entry in ipairs(entries) do
		local key = entry.position.groupKey
		local group = groupsByKey[key]

		if not group then
			group = {
				key = key,
				title = entry.position.formattedLabel,
				firstIndex = entry.sourceIndex,
				topPriority = false,
				highPriority = false,
				lowerPriority = false,
				lowPriority = false,
				hasCurrent = false,
				latestEnd = nil,
				entries = {},
			}

			groupsByKey[key] = group
			table.insert(groups, group)
		end

		table.insert(group.entries, entry)

		group.topPriority = group.topPriority or entry.position.topPriority
		group.highPriority = group.highPriority or entry.position.highPriority
		group.lowerPriority = group.lowerPriority or entry.position.lowerPriority
		group.lowPriority = group.lowPriority or entry.position.lowPriority

		if not entry.endSnak then
			group.hasCurrent = true
		end

		if entry.endDate
			and (not group.latestEnd or entry.endDate > group.latestEnd)
		then
			group.latestEnd = entry.endDate
		end
	end

	for _, group in ipairs(groups) do
		table.sort(group.entries, function(a, b)
			-- Действующий срок всегда идёт первым.
			local aCurrent = not a.endSnak
			local bCurrent = not b.endSnak

			if aCurrent ~= bCurrent then
				return aCurrent
			end

			-- Остальные сроки — от более позднего начала к более раннему.
			if a.startDate ~= b.startDate then
				if not a.startDate then
					return false
				end

				if not b.startDate then
					return true
				end

				return a.startDate > b.startDate
			end

			-- Если даты начала совпали, выше идёт более позднее окончание.
			if a.endDate ~= b.endDate then
				if not a.endDate then
					return true
				end

				if not b.endDate then
					return false
				end

				return a.endDate > b.endDate
			end

			return a.sourceIndex < b.sourceIndex
		end)
	end

	return groups
end

-- Current → top → normal → low → newest completed position.
-- Claims inside one group are never separated.
local function orderPositionGroups(state, groups)
	table.sort(groups, function(a, b)
		local aCurrent = not state.subjectDead and a.hasCurrent
		local bCurrent = not state.subjectDead and b.hasCurrent

		if aCurrent ~= bCurrent then
			return aCurrent
		end

		if a.topPriority ~= b.topPriority then
			return a.topPriority
		end
		
		if a.highPriority ~= b.highPriority then
			return a.highPriority
		end
		
		-- «Low» сразу отправляем в конец.
		if a.lowPriority ~= b.lowPriority then
			return not a.lowPriority
		end
		
		-- Среди остальных «lower» идут после обычных.
		if a.lowerPriority ~= b.lowerPriority then
			return not a.lowerPriority
		end

		if a.latestEnd ~= b.latestEnd then
			if not a.latestEnd then
				return false
			end

			if not b.latestEnd then
				return true
			end

			return a.latestEnd > b.latestEnd
		end

		return a.firstIndex < b.firstIndex
	end)
end

local function formatOrganizations(state, statement)
	local qualifiers = statement.qualifiers
		and statement.qualifiers.P642

	if not qualifiers then
		return nil
	end

	local organizations = {}

	for _, snak in ipairs(qualifiers) do
		local formatted = state.context.formatSnak(state.options, snak)

		if formatted and formatted ~= '' then
			table.insert(organizations, formatted)
		end
	end

	if #organizations == 0 then
		return nil
	end

	return ' (' .. mw.text.listToText(organizations) .. ')'
end

local function formatSuccessionSnak(state, snak, noValueText)
	if not snak then
		return nil
	end

	if snak.snaktype == 'novalue' then
		return '<i>' .. noValueText .. '</i>'
	end

	if getEntityId(snak) == state.subjectId then
		if state.subjectGender == maleGender then
			return '<i>он сам</i>'
		end

		if state.subjectGender == femaleGender then
			return '<i>она сама</i>'
		end
	end

	return state.context.formatSnak(state.options, snak)
end

-- Форматируем дату обычным способом, затем убираем только ссылки.
local function formatPlainDate(state, snak)
	local text = state.context.formatSnak(state.options, snak)

	text = text:gsub('%[%[([^%[%]|]+)|([^%[%]]+)%]%]', '%2')
	text = text:gsub('%[%[([^%[%]]+)%]%]', '%1')

	return text
end

local function getFullDateParts(date)
	if not date then
		return nil
	end

	local plain = date:gsub('<[^>]->', '')
	plain = plain:gsub('&nbsp;', ' ')
	plain = plain:gsub('&#160;', ' ')

	return mw.ustring.match(
		mw.text.trim(plain),
		'^(%d%d?)%s+([^%s]+)%s+(%d%d%d%d)$'
	)
end

-- Сокращаем только диапазон из двух обычных полных дат.
local function formatDateRange(startDate, endDate)
	local startDay, startMonth, startYear = getFullDateParts(startDate)
	local endDay, endMonth, endYear = getFullDateParts(endDate)

	-- Неполные, старостильные и другие нестандартные даты не трогаем.
	if not startDay or not endDay or startYear ~= endYear then
		return startDate .. '&nbsp;— ' .. endDate
	end

	if startMonth == endMonth and startDay ~= endDay then
		return '<span class="nowrap">'
			.. startDay .. '—' .. endDay
			.. '&nbsp;' .. endMonth .. ' ' .. endYear
			.. '</span>'
	end

	if startMonth ~= endMonth then
		return '<span class="nowrap">'
			.. startDay .. ' ' .. startMonth
			.. '&nbsp;— ' .. endDay .. ' ' .. endMonth .. ' ' .. endYear
			.. '</span>'
	end

	return startDate .. '&nbsp;— ' .. endDate
end

local function formatPeriod(state, entry)
	local startDate = entry.startSnak
		and formatPlainDate(state, entry.startSnak)

	local endDate = entry.endSnak
		and formatPlainDate(state, entry.endSnak)

	if not startDate and endDate then
		startDate = state.context.formatSnak(
			state.options,
			{ snaktype = 'somevalue' }
		)
	end

	if not startDate then
		return nil
	end

	if endDate then
		return formatDateRange(startDate, endDate)
	end

	return 'с&nbsp;' .. startDate
end

local function formatActingNote(state, entry)
	local period = formatPeriod(state, entry)

	if not period then
		return nil
	end

	local abbreviation = state.options.frame:expandTemplate{
		title = 'и.о.',
	}

	return '<br><small>(' .. abbreviation .. ' ' .. period .. ')</small>'
end

local function getActingTitle(state)
	if state.subjectGender == femaleGender then
		return 'исполняющая обязанности'
	end

	return 'исполняющий обязанности'
end

-- Компактные сроки и. о. добавляются к обычной должности.
local function prepareActingEntries(state, group)
	local regularEntries = {}

	for _, entry in ipairs(group.entries) do
		entry.hidden = nil
		entry.actingNotes = nil

		if not entry.isActing then
			table.insert(regularEntries, entry)
		end
	end

	if #regularEntries == 0 then
		return false
	end

	for _, entry in ipairs(group.entries) do
		if entry.isActing then
			local note = formatActingNote(state, entry)
			local target = nil
			local actingBefore = false

			-- И. о. закончился в день начала обычного срока.
			if note and entry.endDate then
				for _, regular in ipairs(regularEntries) do
					if regular.startDate == entry.endDate then
						target = regular
						actingBefore = true
						break
					end
				end
			end

			-- И. о. начался в день окончания обычного срока.
			if note and not target and entry.startDate then
				for _, regular in ipairs(regularEntries) do
					if regular.endDate == entry.startDate then
						target = regular
						break
					end
				end
			end

			if target then
				target.actingNotes = (target.actingNotes or '') .. note

				-- Берём предшественника самого раннего срока.
				if actingBefore and entry.predecessorSnak then
					target.predecessorSnak = entry.predecessorSnak
				end

				-- Берём преемника самого позднего срока.
				if not actingBefore and entry.successorSnak then
					target.successorSnak = entry.successorSnak
				end

				entry.hidden = true

			-- Несвязанный срок и. о. без преемника и предшественника
			-- можно показать компактно у первой обычной должности.
			elseif not entry.predecessorSnak and not entry.successorSnak then
				target = regularEntries[1]
				target.actingNotes = (target.actingNotes or '') .. note
				entry.hidden = true
			end
		end
	end

	return true
end

local function formatPositionEntry(state, entry, heading, actingOnly)
	local args = {}

	if heading then
		heading = state.context.wrapStatement(
			heading,
			state.options.property,
			entry.statement.id
		)

		args['должность_'] =
			'<span class="ts-wikidata-positions-uppercasefirst">'
			.. heading
			.. '</span>'

		if actingOnly and entry.isActing then
			args['должность_'] = args['должность_']
				.. '<br><small>('
				.. getActingTitle(state)
				.. ')</small>'
		end

		local organizations = formatOrganizations(state, entry.statement)

		if organizations then
			args['должность*'] = organizations
		end
	else
		args['должность_'] =
			'<span class="ts-wikidata-positions-hidden-header">&#8203;</span>'
	end

	local period = formatPeriod(state, entry)

	if entry.isActing and not actingOnly and period then
		local abbreviation = state.options.frame:expandTemplate{
			title = 'и.о.',
		}

		period = abbreviation .. '&nbsp;' .. period
	end

	if period and entry.actingNotes then
		period = period .. entry.actingNotes
	end

	if period then
		args['период правления_'] = period
	end

	local predecessor = formatSuccessionSnak(
		state,
		entry.predecessorSnak,
		'должность учреждена'
	)

	if predecessor then
		args['предшественник_'] = predecessor
	end

	local successor = formatSuccessionSnak(
		state,
		entry.successorSnak,
		'должность упразднена'
	)

	if successor then
		args['преемник_'] = successor
	end

	local result = state.options.frame:expandTemplate{
		title = 'Государственный деятель/Должность',
		args = args,
	}

	if not heading then
		result = result:gsub(
			'<tr[^>]*>.-ts%-wikidata%-positions%-hidden%-header.-</tr>%s*',
			'',
			1
		)
	end

	return result
end

local function getStyles(options)
	return options.frame:extensionTag(
		'templatestyles',
		'',
		{ src = 'Шаблон:Wikidata/positions/styles.css' }
	)
end

function p.formatPositionClaims(context, options)
	if not context then error('context is not specified') end
	if not options then error('options is not specified') end

	local state = newState(context, options)
	local records = collectPositionRecords(state)
	local entries = buildMergedEntries(records)

	if #entries == 0 then
		return ''
	end

	local groups = buildPositionGroups(entries)
	orderPositionGroups(state, groups)

	local results = {
		getStyles(options),
	}

	for _, group in ipairs(groups) do
		local hasRegularEntry = prepareActingEntries(state, group)
		local headingShown = false

		for _, entry in ipairs(group.entries) do
			if not entry.hidden then
				local heading = nil

				if not headingShown then
					heading = group.title

					if heading and mw.text.trim(heading) == '' then
						heading = nil
					end
				end

				local result = formatPositionEntry(
					state,
					entry,
					heading,
					not hasRegularEntry
				)

				if result ~= '' then
					table.insert(results, result)
					headingShown = true
				end
			end
		end
	end

	return table.concat(results, '\n')
end

-- Compatibility with the old claim-function call.
function p.formatPositionClaim(context, options, statement)
	if not context then error('context is not specified') end
	if not options then error('options is not specified') end
	if not statement then error('statement is not specified') end

	if not isUsableStatement(statement) then
		return ''
	end

	local state = newState(context, options)
	local record = makePositionRecord(state, statement, 1)

	if not record then
		return ''
	end

	local entry = {
		statement = statement,
		sourceIndex = 1,
		position = record.position,
		isActing = record.isActing,

		startSnak = record.startSnak,
		endSnak = record.endSnak,
		predecessorSnak = record.predecessorSnak,
		successorSnak = record.successorSnak,

		startDate = record.startDate,
		endDate = record.endDate,
	}

	return getStyles(options)
		.. formatPositionEntry(
			state,
			entry,
			entry.position.formattedLabel,
			entry.isActing
		)
end

return p