Module:FamilyTree: Difference between revisions

From KB Lexicon
No edit summary
No edit summary
Line 9: Line 9:


local function trim(s)
local function trim(s)
if not s then return nil end
if s == nil then
return nil
end
s = tostring(s)
s = tostring(s)
s = mw.text.trim(s)
s = mw.text.trim(s)
if s == '' then return nil end
if s == '' then
return nil
end
return s
return s
end
end
Line 18: Line 22:
local function isRealValue(v)
local function isRealValue(v)
v = trim(v)
v = trim(v)
if not v then return false end
if not v then
 
local lowered = mw.ustring.lower(v)
if lowered == 'unknown' or lowered == 'none' or lowered == 'n/a' then
return false
return false
end
end


return true
local lowered = mw.ustring.lower(v)
return lowered ~= 'unknown' and lowered ~= 'none' and lowered ~= 'n/a'
end
end


local function safeArray(v)
local function addUnique(list, value)
if type(v) == 'table' then
if not isRealValue(value) then
return v
return
end
for _, existing in ipairs(list) do
if existing == value then
return
end
end
end
return {}
table.insert(list, value)
end
end


local function uniq(list)
local function uniq(list)
local out = {}
local seen = {}
local seen = {}
local out = {}


for _, v in ipairs(list or {}) do
for _, v in ipairs(list or {}) do
Line 50: Line 57:


local function makeLink(name)
local function makeLink(name)
if not isRealValue(name) then return '' end
if not isRealValue(name) then
return ''
end
return string.format('[[%s|%s]]', name, name)
return string.format('[[%s|%s]]', name, name)
end
local function addUnique(list, value)
if not isRealValue(value) then return end
for _, existing in ipairs(list) do
if existing == value then
return
end
end
table.insert(list, value)
end
end


Line 70: Line 69:
end
end


if frame:getParent() then
local parent = frame:getParent()
v = frame:getParent().args[key]
if parent then
v = parent.args[key]
if isRealValue(v) then
if isRealValue(v) then
return trim(v)
return trim(v)
Line 80: Line 80:
end
end


-- =========================================
local function getPeopleField(row, keys)
-- Cargo loaders
for _, key in ipairs(keys) do
-- =========================================
if row[key] ~= nil then
 
return trim(row[key])
local function queryCharacters()
local results = cargo.query(
'Characters2',
'Name,DisplayName,Gender,BirthDate,DeathDate,BirthFamily,CurrentFamily,Status',
{
where = 'Name IS NOT NULL',
limit = 5000
}
)
 
local people = {}
 
for _, row in ipairs(results) do
local name = trim(row.Name)
if isRealValue(name) then
people[name] = {
name = name,
displayName = trim(row.DisplayName) or name,
gender = trim(row.Gender),
birthDate = trim(row.BirthDate),
deathDate = trim(row.DeathDate),
birthFamily = trim(row.BirthFamily),
currentFamily = trim(row.CurrentFamily),
status = trim(row.Status),
parents = {},
children = {},
partners = {},
unions = {}
}
end
end
end
end
return nil
end


return people
-- =========================================
end
-- Data loading
-- =========================================


local function ensurePerson(people, name)
local function ensurePerson(people, name)
if not isRealValue(name) then return nil end
name = trim(name)
name = trim(name)
if not isRealValue(name) then
return nil
end


if not people[name] then
if not people[name] then
Line 141: Line 117:


return people[name]
return people[name]
end
local function queryCharacters()
-- Pull all columns; we map flexibly in case your exact field names vary.
local results = cargo.query(
'Characters',
'_pageName=Page,Name,DisplayName,Gender,BirthDate,DeathDate,BirthFamily,CurrentFamily,Status',
{ limit = 5000 }
)
local people = {}
for _, row in ipairs(results) do
local name =
getPeopleField(row, { 'Name', 'Page', '_pageName', 'DisplayName' })
if isRealValue(name) then
people[name] = {
name = name,
displayName = getPeopleField(row, { 'DisplayName' }) or name,
gender = getPeopleField(row, { 'Gender' }),
birthDate = getPeopleField(row, { 'BirthDate' }),
deathDate = getPeopleField(row, { 'DeathDate' }),
birthFamily = getPeopleField(row, { 'BirthFamily' }),
currentFamily = getPeopleField(row, { 'CurrentFamily' }),
status = getPeopleField(row, { 'Status' }),
parents = {},
children = {},
partners = {},
unions = {}
}
end
end
return people
end
end


Line 147: Line 158:
'ParentChild',
'ParentChild',
'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
{
{ limit = 5000 }
where = 'Child IS NOT NULL',
limit = 5000
}
)
)


Line 178: Line 186:
local function loadUnions(people)
local function loadUnions(people)
local results = cargo.query(
local results = cargo.query(
'Unions2',
'Unions',
'UnionID,Partner1,Partner2,UnionType,Status,EngagementDate,MarriageDate,DivorceDate',
'UnionID,Partner1,Partner2,UnionType,Status,EngagementDate,MarriageDate,DivorceDate',
{
{ limit = 5000 }
where = '(Partner1 IS NOT NULL OR Partner2 IS NOT NULL)',
limit = 5000
}
)
)


Line 236: Line 241:


-- =========================================
-- =========================================
-- Legacy crash stopper
-- Relationship helpers
-- =========================================
 
-- Old code was still calling this. Keep it defined so the module doesn't explode.
local function makeFocusBranches()
return ''
end
 
-- =========================================
-- Rendering helpers
-- =========================================
-- =========================================


local function renderPersonBox(name, extraClass)
local function sortNames(people, names)
if not isRealValue(name) then return nil end
 
local box = html.create('div')
box:addClass('familytree-person')
if isRealValue(extraClass) then
box:addClass(extraClass)
end
 
box:wikitext(makeLink(name))
return box
end
 
local function renderRow(title, peopleList, rowClass)
peopleList = safeArray(peopleList)
if #peopleList == 0 then
return nil
end
 
local row = html.create('div')
row:addClass('familytree-row')
if isRealValue(rowClass) then
row:addClass(rowClass)
end
 
if isRealValue(title) then
row:tag('div')
:addClass('familytree-row-label')
:wikitext(title)
end
 
local items = row:tag('div'):addClass('familytree-row-items')
 
for _, name in ipairs(peopleList) do
local box = renderPersonBox(name)
if box then
items:node(box)
end
end
 
return row
end
 
local function sortNamesByDisplay(people, names)
table.sort(names, function(a, b)
table.sort(names, function(a, b)
local ad = (people[a] and people[a].displayName) or a
local ad = (people[a] and people[a].displayName) or a
Line 300: Line 253:


local function getGrandparents(people, personName)
local function getGrandparents(people, personName)
local grandparents = {}
local out = {}
local person = people[personName]
local person = people[personName]
if not person then return grandparents end
if not person then
return out
end


for _, parentName in ipairs(person.parents) do
for _, parentName in ipairs(person.parents) do
Line 308: Line 263:
if parent then
if parent then
for _, gp in ipairs(parent.parents) do
for _, gp in ipairs(parent.parents) do
addUnique(grandparents, gp)
addUnique(out, gp)
end
end
end
end
end
end


return uniq(grandparents)
return uniq(out)
end
end


local function getSiblings(people, personName)
local function getSiblings(people, personName)
local siblings = {}
local out = {}
local seen = {}
local person = people[personName]
local person = people[personName]
if not person then return siblings end


local siblingSet = {}
if not person then
return out
end


for _, parentName in ipairs(person.parents) do
for _, parentName in ipairs(person.parents) do
Line 327: Line 284:
if parent then
if parent then
for _, childName in ipairs(parent.children) do
for _, childName in ipairs(parent.children) do
if childName ~= personName then
if childName ~= personName and not seen[childName] then
siblingSet[childName] = true
seen[childName] = true
table.insert(out, childName)
end
end
end
end
Line 334: Line 292:
end
end


for name, _ in pairs(siblingSet) do
return uniq(out)
table.insert(siblings, name)
end
 
local function getConnectedPeople(people, personName)
local out = {}
local person = people[personName]
if not person then
return out
end
 
for _, v in ipairs(person.parents) do addUnique(out, v) end
for _, v in ipairs(person.children) do addUnique(out, v) end
for _, v in ipairs(person.partners) do addUnique(out, v) end
 
local siblings = getSiblings(people, personName)
for _, v in ipairs(siblings) do addUnique(out, v) end
 
local grandparents = getGrandparents(people, personName)
for _, v in ipairs(grandparents) do addUnique(out, v) end
 
return uniq(out)
end
 
-- =========================================
-- Rendering
-- =========================================
 
local function renderPersonBox(name, extraClass)
if not isRealValue(name) then
return nil
end
 
local box = html.create('div')
box:addClass('familytree-person')
if isRealValue(extraClass) then
box:addClass(extraClass)
end
box:wikitext(makeLink(name))
return box
end
 
local function renderRow(label, names, rowClass)
names = uniq(names)
if #names == 0 then
return nil
end
 
local row = html.create('div')
row:addClass('familytree-row')
if isRealValue(rowClass) then
row:addClass(rowClass)
end
 
row:tag('div')
:addClass('familytree-row-label')
:wikitext(label)
 
local items = row:tag('div')
:addClass('familytree-row-items')
 
for _, name in ipairs(names) do
local node = renderPersonBox(name)
if node then
items:node(node)
end
end
end


return uniq(siblings)
return row
end
end


Line 344: Line 365:
local person = people[personName]
local person = people[personName]
if not person then
if not person then
return string.format(
return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(personName) .. '".'
'<strong>FamilyTree error:</strong> No character found for "%s".',
personName or '(blank)'
)
end
end


local grandparents = getGrandparents(people, personName)
local grandparents = getGrandparents(people, personName)
local parents = uniq(person.parents)
local parents = uniq(person.parents)
local siblings = getSiblings(people, personName)
local partners = uniq(person.partners)
local partners = uniq(person.partners)
local children = uniq(person.children)
local children = uniq(person.children)
local siblings = getSiblings(people, personName)


sortNamesByDisplay(people, grandparents)
sortNames(people, grandparents)
sortNamesByDisplay(people, parents)
sortNames(people, parents)
sortNamesByDisplay(people, siblings)
sortNames(people, siblings)
sortNamesByDisplay(people, partners)
sortNames(people, partners)
sortNamesByDisplay(people, children)
sortNames(people, children)


local root = html.create('div')
local root = html.create('div')
Line 379: Line 397:
:wikitext('Focus')
:wikitext('Focus')


local selfItems = selfRow:tag('div'):addClass('familytree-row-items')
selfRow:tag('div')
selfItems:node(renderPersonBox(personName, 'familytree-focus-person'))
:addClass('familytree-row-items')
:node(renderPersonBox(personName, 'familytree-focus-person'))
 
root:node(selfRow)
root:node(selfRow)


local sibRow = renderRow('Siblings', siblings, 'familytree-siblings')
local sRow = renderRow('Siblings', siblings, 'familytree-siblings')
if sibRow then root:node(sibRow) end
if sRow then root:node(sRow) end


local partnerRow = renderRow('Partners', partners, 'familytree-partners')
local partnerRow = renderRow('Partners', partners, 'familytree-partners')
Line 391: Line 411:
local childRow = renderRow('Children', children, 'familytree-children')
local childRow = renderRow('Children', children, 'familytree-children')
if childRow then root:node(childRow) end
if childRow then root:node(childRow) end
return tostring(root)
end
local function renderConnectedForPerson(people, personName)
local person = people[personName]
if not person then
return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(personName) .. '".'
end
local connected = getConnectedPeople(people, personName)
sortNames(people, connected)
local root = html.create('div')
root:addClass('familytree-connected')
root:tag('div')
:addClass('familytree-connected-title')
:wikitext('Connected to ' .. makeLink(personName))
local items = root:tag('div')
:addClass('familytree-row-items')
for _, name in ipairs(connected) do
items:node(renderPersonBox(name))
end


return tostring(root)
return tostring(root)
Line 398: Line 444:
local person = people[personName]
local person = people[personName]
if not person then
if not person then
return string.format(
return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(personName) .. '".'
'<strong>FamilyTree error:</strong> No character found for "%s".',
personName or '(blank)'
)
end
end


Line 429: Line 472:


if #person.parents > 0 then
if #person.parents > 0 then
local parentLinks = {}
local links = {}
for _, name in ipairs(person.parents) do
for _, name in ipairs(person.parents) do
table.insert(parentLinks, makeLink(name))
table.insert(links, makeLink(name))
end
end
addField('Parents', table.concat(parentLinks, ', '))
addField('Parents', table.concat(links, ', '))
end
end


if #person.partners > 0 then
if #person.partners > 0 then
local partnerLinks = {}
local links = {}
for _, name in ipairs(person.partners) do
for _, name in ipairs(person.partners) do
table.insert(partnerLinks, makeLink(name))
table.insert(links, makeLink(name))
end
end
addField('Partners', table.concat(partnerLinks, ', '))
addField('Partners', table.concat(links, ', '))
end
end


if #person.children > 0 then
if #person.children > 0 then
local childLinks = {}
local links = {}
for _, name in ipairs(person.children) do
for _, name in ipairs(person.children) do
table.insert(childLinks, makeLink(name))
table.insert(links, makeLink(name))
end
end
addField('Children', table.concat(childLinks, ', '))
addField('Children', table.concat(links, ', '))
end
end


Line 462: Line 505:
local personName = getArg(frame, 'person') or mw.title.getCurrentTitle().text
local personName = getArg(frame, 'person') or mw.title.getCurrentTitle().text
local people = loadData()
local people = loadData()
-- harmless legacy call so any older code path won't die
makeFocusBranches()
return renderTreeForPerson(people, personName)
return renderTreeForPerson(people, personName)
end
end
Line 473: Line 512:
local people = loadData()
local people = loadData()
return renderProfileForPerson(people, personName)
return renderProfileForPerson(people, personName)
end
function p.connected(frame)
local personName = getArg(frame, 'person') or mw.title.getCurrentTitle().text
local people = loadData()
return renderConnectedForPerson(people, personName)
end
end


return p
return p

Revision as of 21:31, 29 March 2026

Documentation for this module may be created at Module:FamilyTree/doc

local p = {}

local cargo = mw.ext.cargo
local html = mw.html

-- =========================================
-- Helpers
-- =========================================

local function trim(s)
	if s == nil then
		return nil
	end
	s = tostring(s)
	s = mw.text.trim(s)
	if s == '' then
		return nil
	end
	return s
end

local function isRealValue(v)
	v = trim(v)
	if not v then
		return false
	end

	local lowered = mw.ustring.lower(v)
	return lowered ~= 'unknown' and lowered ~= 'none' and lowered ~= 'n/a'
end

local function addUnique(list, value)
	if not isRealValue(value) then
		return
	end
	for _, existing in ipairs(list) do
		if existing == value then
			return
		end
	end
	table.insert(list, value)
end

local function uniq(list)
	local out = {}
	local seen = {}

	for _, v in ipairs(list or {}) do
		if isRealValue(v) and not seen[v] then
			seen[v] = true
			table.insert(out, v)
		end
	end

	return out
end

local function makeLink(name)
	if not isRealValue(name) then
		return ''
	end
	return string.format('[[%s|%s]]', name, name)
end

local function getArg(frame, key)
	local v = frame.args[key]
	if isRealValue(v) then
		return trim(v)
	end

	local parent = frame:getParent()
	if parent then
		v = parent.args[key]
		if isRealValue(v) then
			return trim(v)
		end
	end

	return nil
end

local function getPeopleField(row, keys)
	for _, key in ipairs(keys) do
		if row[key] ~= nil then
			return trim(row[key])
		end
	end
	return nil
end

-- =========================================
-- Data loading
-- =========================================

local function ensurePerson(people, name)
	name = trim(name)
	if not isRealValue(name) then
		return nil
	end

	if not people[name] then
		people[name] = {
			name = name,
			displayName = name,
			gender = nil,
			birthDate = nil,
			deathDate = nil,
			birthFamily = nil,
			currentFamily = nil,
			status = nil,
			parents = {},
			children = {},
			partners = {},
			unions = {}
		}
	end

	return people[name]
end

local function queryCharacters()
	-- Pull all columns; we map flexibly in case your exact field names vary.
	local results = cargo.query(
		'Characters',
		'_pageName=Page,Name,DisplayName,Gender,BirthDate,DeathDate,BirthFamily,CurrentFamily,Status',
		{ limit = 5000 }
	)

	local people = {}

	for _, row in ipairs(results) do
		local name =
			getPeopleField(row, { 'Name', 'Page', '_pageName', 'DisplayName' })

		if isRealValue(name) then
			people[name] = {
				name = name,
				displayName = getPeopleField(row, { 'DisplayName' }) or name,
				gender = getPeopleField(row, { 'Gender' }),
				birthDate = getPeopleField(row, { 'BirthDate' }),
				deathDate = getPeopleField(row, { 'DeathDate' }),
				birthFamily = getPeopleField(row, { 'BirthFamily' }),
				currentFamily = getPeopleField(row, { 'CurrentFamily' }),
				status = getPeopleField(row, { 'Status' }),
				parents = {},
				children = {},
				partners = {},
				unions = {}
			}
		end
	end

	return people
end

local function loadParentChild(people)
	local results = cargo.query(
		'ParentChild',
		'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
		{ limit = 5000 }
	)

	for _, row in ipairs(results) do
		local child = trim(row.Child)
		local p1 = trim(row.Parent1)
		local p2 = trim(row.Parent2)

		if isRealValue(child) then
			ensurePerson(people, child)

			if isRealValue(p1) then
				ensurePerson(people, p1)
				addUnique(people[child].parents, p1)
				addUnique(people[p1].children, child)
			end

			if isRealValue(p2) then
				ensurePerson(people, p2)
				addUnique(people[child].parents, p2)
				addUnique(people[p2].children, child)
			end
		end
	end
end

local function loadUnions(people)
	local results = cargo.query(
		'Unions',
		'UnionID,Partner1,Partner2,UnionType,Status,EngagementDate,MarriageDate,DivorceDate',
		{ limit = 5000 }
	)

	for _, row in ipairs(results) do
		local unionID = trim(row.UnionID)
		local p1 = trim(row.Partner1)
		local p2 = trim(row.Partner2)

		if isRealValue(p1) then ensurePerson(people, p1) end
		if isRealValue(p2) then ensurePerson(people, p2) end

		if isRealValue(p1) and isRealValue(p2) then
			addUnique(people[p1].partners, p2)
			addUnique(people[p2].partners, p1)

			table.insert(people[p1].unions, {
				unionID = unionID,
				partner = p2,
				unionType = trim(row.UnionType),
				status = trim(row.Status),
				engagementDate = trim(row.EngagementDate),
				marriageDate = trim(row.MarriageDate),
				divorceDate = trim(row.DivorceDate)
			})

			table.insert(people[p2].unions, {
				unionID = unionID,
				partner = p1,
				unionType = trim(row.UnionType),
				status = trim(row.Status),
				engagementDate = trim(row.EngagementDate),
				marriageDate = trim(row.MarriageDate),
				divorceDate = trim(row.DivorceDate)
			})
		end
	end
end

local function loadData()
	local people = queryCharacters()
	loadParentChild(people)
	loadUnions(people)

	for _, person in pairs(people) do
		person.parents = uniq(person.parents)
		person.children = uniq(person.children)
		person.partners = uniq(person.partners)
	end

	return people
end

-- =========================================
-- Relationship helpers
-- =========================================

local function sortNames(people, names)
	table.sort(names, function(a, b)
		local ad = (people[a] and people[a].displayName) or a
		local bd = (people[b] and people[b].displayName) or b
		return mw.ustring.lower(ad) < mw.ustring.lower(bd)
	end)
end

local function getGrandparents(people, personName)
	local out = {}
	local person = people[personName]
	if not person then
		return out
	end

	for _, parentName in ipairs(person.parents) do
		local parent = people[parentName]
		if parent then
			for _, gp in ipairs(parent.parents) do
				addUnique(out, gp)
			end
		end
	end

	return uniq(out)
end

local function getSiblings(people, personName)
	local out = {}
	local seen = {}
	local person = people[personName]

	if not person then
		return out
	end

	for _, parentName in ipairs(person.parents) do
		local parent = people[parentName]
		if parent then
			for _, childName in ipairs(parent.children) do
				if childName ~= personName and not seen[childName] then
					seen[childName] = true
					table.insert(out, childName)
				end
			end
		end
	end

	return uniq(out)
end

local function getConnectedPeople(people, personName)
	local out = {}
	local person = people[personName]
	if not person then
		return out
	end

	for _, v in ipairs(person.parents) do addUnique(out, v) end
	for _, v in ipairs(person.children) do addUnique(out, v) end
	for _, v in ipairs(person.partners) do addUnique(out, v) end

	local siblings = getSiblings(people, personName)
	for _, v in ipairs(siblings) do addUnique(out, v) end

	local grandparents = getGrandparents(people, personName)
	for _, v in ipairs(grandparents) do addUnique(out, v) end

	return uniq(out)
end

-- =========================================
-- Rendering
-- =========================================

local function renderPersonBox(name, extraClass)
	if not isRealValue(name) then
		return nil
	end

	local box = html.create('div')
	box:addClass('familytree-person')
	if isRealValue(extraClass) then
		box:addClass(extraClass)
	end
	box:wikitext(makeLink(name))
	return box
end

local function renderRow(label, names, rowClass)
	names = uniq(names)
	if #names == 0 then
		return nil
	end

	local row = html.create('div')
	row:addClass('familytree-row')
	if isRealValue(rowClass) then
		row:addClass(rowClass)
	end

	row:tag('div')
		:addClass('familytree-row-label')
		:wikitext(label)

	local items = row:tag('div')
		:addClass('familytree-row-items')

	for _, name in ipairs(names) do
		local node = renderPersonBox(name)
		if node then
			items:node(node)
		end
	end

	return row
end

local function renderTreeForPerson(people, personName)
	local person = people[personName]
	if not person then
		return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(personName) .. '".'
	end

	local grandparents = getGrandparents(people, personName)
	local parents = uniq(person.parents)
	local siblings = getSiblings(people, personName)
	local partners = uniq(person.partners)
	local children = uniq(person.children)

	sortNames(people, grandparents)
	sortNames(people, parents)
	sortNames(people, siblings)
	sortNames(people, partners)
	sortNames(people, children)

	local root = html.create('div')
	root:addClass('familytree-wrapper')

	local gpRow = renderRow('Grandparents', grandparents, 'familytree-grandparents')
	if gpRow then root:node(gpRow) end

	local pRow = renderRow('Parents', parents, 'familytree-parents')
	if pRow then root:node(pRow) end

	local selfRow = html.create('div')
	selfRow:addClass('familytree-row')
	selfRow:addClass('familytree-focus-row')

	selfRow:tag('div')
		:addClass('familytree-row-label')
		:wikitext('Focus')

	selfRow:tag('div')
		:addClass('familytree-row-items')
		:node(renderPersonBox(personName, 'familytree-focus-person'))

	root:node(selfRow)

	local sRow = renderRow('Siblings', siblings, 'familytree-siblings')
	if sRow then root:node(sRow) end

	local partnerRow = renderRow('Partners', partners, 'familytree-partners')
	if partnerRow then root:node(partnerRow) end

	local childRow = renderRow('Children', children, 'familytree-children')
	if childRow then root:node(childRow) end

	return tostring(root)
end

local function renderConnectedForPerson(people, personName)
	local person = people[personName]
	if not person then
		return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(personName) .. '".'
	end

	local connected = getConnectedPeople(people, personName)
	sortNames(people, connected)

	local root = html.create('div')
	root:addClass('familytree-connected')

	root:tag('div')
		:addClass('familytree-connected-title')
		:wikitext('Connected to ' .. makeLink(personName))

	local items = root:tag('div')
		:addClass('familytree-row-items')

	for _, name in ipairs(connected) do
		items:node(renderPersonBox(name))
	end

	return tostring(root)
end

local function renderProfileForPerson(people, personName)
	local person = people[personName]
	if not person then
		return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(personName) .. '".'
	end

	local root = html.create('div')
	root:addClass('familytree-profile')

	root:tag('div')
		:addClass('familytree-profile-name')
		:wikitext(makeLink(person.name))

	local dl = root:tag('dl')

	local function addField(label, value)
		if isRealValue(value) then
			dl:tag('dt'):wikitext(label)
			dl:tag('dd'):wikitext(value)
		end
	end

	addField('Display Name', person.displayName ~= person.name and person.displayName or nil)
	addField('Gender', person.gender)
	addField('Birth Date', person.birthDate)
	addField('Death Date', person.deathDate)
	addField('Birth Family', person.birthFamily and makeLink(person.birthFamily) or nil)
	addField('Current Family', person.currentFamily and makeLink(person.currentFamily) or nil)
	addField('Status', person.status)

	if #person.parents > 0 then
		local links = {}
		for _, name in ipairs(person.parents) do
			table.insert(links, makeLink(name))
		end
		addField('Parents', table.concat(links, ', '))
	end

	if #person.partners > 0 then
		local links = {}
		for _, name in ipairs(person.partners) do
			table.insert(links, makeLink(name))
		end
		addField('Partners', table.concat(links, ', '))
	end

	if #person.children > 0 then
		local links = {}
		for _, name in ipairs(person.children) do
			table.insert(links, makeLink(name))
		end
		addField('Children', table.concat(links, ', '))
	end

	return tostring(root)
end

-- =========================================
-- Public functions
-- =========================================

function p.tree(frame)
	local personName = getArg(frame, 'person') or mw.title.getCurrentTitle().text
	local people = loadData()
	return renderTreeForPerson(people, personName)
end

function p.profile(frame)
	local personName = getArg(frame, 'person') or mw.title.getCurrentTitle().text
	local people = loadData()
	return renderProfileForPerson(people, personName)
end

function p.connected(frame)
	local personName = getArg(frame, 'person') or mw.title.getCurrentTitle().text
	local people = loadData()
	return renderConnectedForPerson(people, personName)
end

return p