Module:FamilyTree: Difference between revisions

From KB Lexicon
No edit summary
No edit summary
Line 48: Line 48:
local function sorted(list)
local function sorted(list)
table.sort(list, function(a, b)
table.sort(list, function(a, b)
return a:lower() < b:lower()
return tostring(a):lower() < tostring(b):lower()
end)
end)
return list
return list
end
local function formatYear(dateValue)
dateValue = trim(dateValue)
if not dateValue then
return nil
end
local year = tostring(dateValue):match('^(%d%d%d%d)')
return year
end
end


Line 61: Line 70:
local rows = cargoQuery(
local rows = cargoQuery(
'Characters',
'Characters',
'Page,DisplayName,BirthDate,DeathDate',
'Page,DisplayName',
{
{
where = 'Page="' .. esc(pageName) .. '"',
where = 'Page="' .. esc(pageName) .. '"',
Line 73: Line 82:


return pageName
return pageName
end
local function formatYear(dateValue)
dateValue = trim(dateValue)
if not dateValue then
return nil
end
local year = tostring(dateValue):match('^(%d%d%d%d)')
return year
end
end


Line 92: Line 92:
local rows = cargoQuery(
local rows = cargoQuery(
'Characters',
'Characters',
'Page,DisplayName,BirthDate,DeathDate,Status',
'Page,DisplayName,BirthDate,DeathDate,Status,Gender',
{
{
where = 'Page="' .. esc(pageName) .. '"',
where = 'Page="' .. esc(pageName) .. '"',
Line 100: Line 100:


return rows[1]
return rows[1]
end
local function makeLinkedName(pageName)
local displayName = getDisplayName(pageName)
return '[[' .. pageName .. '|' .. displayName .. ']]'
end
local function linkList(list)
local out = {}
for _, pageName in ipairs(list) do
table.insert(out, makeLinkedName(pageName))
end
return table.concat(out, '<br>')
end
end


local function getParents(person)
local function getParents(person)
person = trim(person)
if not person then
return {}, nil
end
local rows = cargoQuery(
local rows = cargoQuery(
'ParentChild',
'ParentChild',
Line 124: Line 142:


local function getChildren(person)
local function getChildren(person)
person = trim(person)
if not person then
return {}
end
local rows = cargoQuery(
local rows = cargoQuery(
'ParentChild',
'ParentChild',
Line 153: Line 176:


local function getPartners(person)
local function getPartners(person)
person = trim(person)
if not person then
return {}, {}
end
local rows = cargoQuery(
local rows = cargoQuery(
'Unions',
'Unions',
'UnionID,Partner1,Partner2,UnionType,Status',
'UnionID,Partner1,Partner2,UnionType,Status,MarriageDate,DivorceDate,EngagementDate',
{
{
where = 'Partner1="' .. esc(person) .. '" OR Partner2="' .. esc(person) .. '"',
where = 'Partner1="' .. esc(person) .. '" OR Partner2="' .. esc(person) .. '"',
Line 173: Line 201:
end
end


return sorted(partners)
return sorted(partners), rows
end
 
local function getSiblings(person)
person = trim(person)
if not person then
return {}
end
 
local targetRows = cargoQuery(
'ParentChild',
'Child,Parent1,Parent2,UnionID',
{
where = 'Child="' .. esc(person) .. '"',
limit = 10
}
)
 
if not targetRows[1] then
return {}
end
 
local targetUnion = trim(targetRows[1].UnionID)
local targetP1 = trim(targetRows[1].Parent1)
local targetP2 = trim(targetRows[1].Parent2)
 
local whereParts = {}
 
if targetUnion then
table.insert(whereParts, 'UnionID="' .. esc(targetUnion) .. '"')
end
 
if targetP1 and targetP2 then
table.insert(whereParts, '(Parent1="' .. esc(targetP1) .. '" AND Parent2="' .. esc(targetP2) .. '")')
end
 
if #whereParts == 0 then
return {}
end
 
local rows = cargoQuery(
'ParentChild',
'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
{
where = table.concat(whereParts, ' OR '),
limit = 100
}
)
 
local siblings = {}
local seen = {}
 
table.sort(rows, function(a, b)
local aOrder = tonumber(a.BirthOrder) or 9999
local bOrder = tonumber(b.BirthOrder) or 9999
if aOrder == bOrder then
return tostring(a.Child):lower() < tostring(b.Child):lower()
end
return aOrder < bOrder
end)
 
for _, row in ipairs(rows) do
if trim(row.Child) ~= person then
addUnique(siblings, seen, row.Child)
end
end
 
return siblings
end
 
local function getNeighbors(person)
local neighbors = {}
 
person = trim(person)
if not person then
return neighbors
end
 
local unionRows = cargoQuery(
'Unions',
'UnionID,Partner1,Partner2',
{
where = 'Partner1="' .. esc(person) .. '" OR Partner2="' .. esc(person) .. '"',
limit = 500
}
)
 
for _, row in ipairs(unionRows) do
neighbors[trim(row.Partner1)] = true
neighbors[trim(row.Partner2)] = true
end
 
local parentChildRows = cargoQuery(
'ParentChild',
'Child,Parent1,Parent2,UnionID',
{
where = 'Child="' .. esc(person) .. '" OR Parent1="' .. esc(person) .. '" OR Parent2="' .. esc(person) .. '"',
limit = 1000
}
)
 
for _, row in ipairs(parentChildRows) do
neighbors[trim(row.Child)] = true
neighbors[trim(row.Parent1)] = true
neighbors[trim(row.Parent2)] = true
end
 
neighbors[person] = nil
neighbors[nil] = nil
 
return neighbors
end
 
local function collectConnectedComponent(root)
local visited = {}
local queue = {}
local head = 1
 
root = trim(root)
if not root then
return visited
end
 
visited[root] = true
table.insert(queue, root)
 
while head <= #queue do
local current = queue[head]
head = head + 1
 
local neighbors = getNeighbors(current)
 
for neighbor, _ in pairs(neighbors) do
if neighbor and not visited[neighbor] then
visited[neighbor] = true
table.insert(queue, neighbor)
end
end
end
 
return visited
end
end


Line 189: Line 357:
local years = ''
local years = ''
if birthYear or deathYear then
if birthYear or deathYear then
years = '<div class="ft-years">'
years = '<div class="ft-years">' .. (birthYear or '?') .. '–' .. (deathYear or '') .. '</div>'
.. (birthYear or '?')
.. '–'
.. (deathYear or '')
.. '</div>'
end
end


Line 199: Line 363:
end
end


local function makeRow(label, people)
local function makePersonRow(people)
if not people or #people == 0 then
if not people or #people == 0 then
return ''
return ''
Line 205: Line 369:


local html = {}
local html = {}
table.insert(html, '<div class="ft-row-wrap">')
table.insert(html, '<div class="ft-label">' .. label .. '</div>')
table.insert(html, '<div class="ft-row">')
table.insert(html, '<div class="ft-row">')
for _, person in ipairs(people) do
for _, person in ipairs(people) do
table.insert(html, makeCard(person))
table.insert(html, makeCard(person))
end
end
table.insert(html, '</div>')


table.insert(html, '</div>')
table.insert(html, '</div>')
return table.concat(html)
return table.concat(html)
end
end


function p.connected(frame)
local function makeCoupleCards(couples)
local args = frame.args
if not couples or #couples == 0 then
local parentArgs = frame:getParent() and frame:getParent().args or {}
return ''
local root = trim(args.root or parentArgs.root or args[1] or parentArgs[1])
end
 
local html = {}


if not root then
for _, pair in ipairs(couples) do
return 'Error: no root provided. Use root=Character Name'
table.insert(html, '<div class="ft-couple">')
for _, person in ipairs(pair) do
table.insert(html, makeCard(person))
end
table.insert(html, '</div>')
end
end


local visited = {}
return table.concat(html)
local queue = { root }
end
local head = 1
visited[root] = true


while head <= #queue do
local function makeCoupleRow(root, partners)
local current = queue[head]
local couples = {}
head = head + 1


local parents = getParents(current)
if not partners or #partners == 0 then
local children = getChildren(current)
table.insert(couples, { root })
local partners = getPartners(current)
else
for _, partner in ipairs(partners) do
table.insert(couples, { root, partner })
end
end


for _, person in ipairs(parents) do
return couples
if not visited[person] then
end
visited[person] = true
table.insert(queue, person)
end
end


for _, person in ipairs(children) do
function p.connected(frame)
if not visited[person] then
local args = frame.args
visited[person] = true
local parentArgs = frame:getParent() and frame:getParent().args or {}
table.insert(queue, person)
local root = trim(args.root or parentArgs.root or args[1] or parentArgs[1])
end
end


for _, person in ipairs(partners) do
if not root then
if not visited[person] then
return 'Error: no root provided. Use root=Character Name'
visited[person] = true
table.insert(queue, person)
end
end
end
end


local component = collectConnectedComponent(root)
local people = {}
local people = {}
for name, _ in pairs(visited) do
 
for name, _ in pairs(component) do
table.insert(people, name)
table.insert(people, name)
end
end
sorted(people)
sorted(people)
if #people == 0 then
return 'No connected people found for ' .. root
end


local lines = {}
local lines = {}
table.insert(lines, "'''Connected component for " .. getDisplayName(root) .. "'''")
table.insert(lines, "'''Connected component for " .. getDisplayName(root) .. "'''")
table.insert(lines, '* Total people found: ' .. tostring(#people))
table.insert(lines, '* Total people found: ' .. tostring(#people))
for _, person in ipairs(people) do
for _, person in ipairs(people) do
table.insert(lines, '* [[' .. person .. '|' .. getDisplayName(person) .. ']]')
table.insert(lines, '* ' .. makeLinkedName(person))
end
end


Line 288: Line 452:


local parents = getParents(root)
local parents = getParents(root)
local siblings = getSiblings(root)
local partners = getPartners(root)
local partners = getPartners(root)
local children = getChildren(root)
local children = getChildren(root)
Line 297: Line 462:
table.insert(lines, '|-')
table.insert(lines, '|-')
table.insert(lines, '! style="width:20%;" | Person')
table.insert(lines, '! style="width:20%;" | Person')
table.insert(lines, '| [[' .. root .. '|' .. getDisplayName(root) .. ']]')
table.insert(lines, '| ' .. makeLinkedName(root))
 
table.insert(lines, '|-')
table.insert(lines, '|-')
table.insert(lines, '! Parents')
table.insert(lines, '! Parents')
table.insert(lines, '| ' .. (#parents > 0 and table.concat((function()
table.insert(lines, '| ' .. (#parents > 0 and linkList(parents) or '—'))
local out = {}
 
for _, pName in ipairs(parents) do
table.insert(lines, '|-')
table.insert(out, '[[' .. pName .. '|' .. getDisplayName(pName) .. ']]')
table.insert(lines, '! Siblings')
end
table.insert(lines, '| ' .. (#siblings > 0 and linkList(siblings) or '—'))
return out
 
end)(), '<br>') or '—'))
table.insert(lines, '|-')
table.insert(lines, '|-')
table.insert(lines, '! Partners')
table.insert(lines, '! Partners')
table.insert(lines, '| ' .. (#partners > 0 and table.concat((function()
table.insert(lines, '| ' .. (#partners > 0 and linkList(partners) or '—'))
local out = {}
 
for _, pName in ipairs(partners) do
table.insert(out, '[[' .. pName .. '|' .. getDisplayName(pName) .. ']]')
end
return out
end)(), '<br>') or '—'))
table.insert(lines, '|-')
table.insert(lines, '|-')
table.insert(lines, '! Children')
table.insert(lines, '! Children')
table.insert(lines, '| ' .. (#children > 0 and table.concat((function()
table.insert(lines, '| ' .. (#children > 0 and linkList(children) or '—'))
local out = {}
 
for _, cName in ipairs(children) do
table.insert(out, '[[' .. cName .. '|' .. getDisplayName(cName) .. ']]')
end
return out
end)(), '<br>') or '—'))
table.insert(lines, '|}')
table.insert(lines, '|}')


Line 354: Line 509:
sorted(grandparents)
sorted(grandparents)


local rootRow = { root }
local rootCouples = makeCoupleRow(root, partners)
for _, partner in ipairs(partners) do
table.insert(rootRow, partner)
end


local html = {}
local html = {}
table.insert(html, '<div class="ft-tree">')
table.insert(html, '<div class="ft-tree">')
table.insert(html, '<div class="ft-title">Family tree for ' .. getDisplayName(root) .. '</div>')
table.insert(html, '<div class="ft-title">Family tree for ' .. getDisplayName(root) .. '</div>')
table.insert(html, '<div class="ft-generation ft-grandparents">')
table.insert(html, makeRow('', grandparents))
table.insert(html, '</div>')


table.insert(html, '<div class="ft-connector"></div>')
if #grandparents > 0 then
table.insert(html, '<div class="ft-generation ft-grandparents">')
table.insert(html, makePersonRow(grandparents))
table.insert(html, '</div>')
end


table.insert(html, '<div class="ft-generation ft-parents">')
if #grandparents > 0 and #parents > 0 then
table.insert(html, makeRow('', parents))
table.insert(html, '<div class="ft-connector"></div>')
table.insert(html, '</div>')
end


table.insert(html, '<div class="ft-connector"></div>')
if #parents > 0 then
table.insert(html, '<div class="ft-generation ft-parents">')
table.insert(html, makePersonRow(parents))
table.insert(html, '</div>')
end


table.insert(html, '<div class="ft-generation ft-root">')
if (#parents > 0) and (#rootCouples > 0) then
table.insert(html, makeRow('', rootRow))
table.insert(html, '<div class="ft-connector"></div>')
table.insert(html, '</div>')
end


table.insert(html, '<div class="ft-connector"></div>')
table.insert(html, '<div class="ft-generation ft-root">')
table.insert(html, '<div class="ft-row">')
table.insert(html, makeCoupleCards(rootCouples))
table.insert(html, '</div>')
table.insert(html, '</div>')


table.insert(html, '<div class="ft-generation ft-children">')
if #children > 0 then
table.insert(html, makeRow('', children))
table.insert(html, '<div class="ft-connector"></div>')
table.insert(html, '</div>')
table.insert(html, '<div class="ft-generation ft-children">')
table.insert(html, makePersonRow(children))
table.insert(html, '</div>')
end


table.insert(html, '</div>')


return table.concat(html)
return table.concat(html)

Revision as of 21:32, 27 March 2026

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

local p = {}

local cargo = mw.ext.cargo

local function esc(value)
	if not value then
		return ''
	end
	value = tostring(value)
	value = value:gsub('\\', '\\\\')
	value = value:gsub('"', '\\"')
	return value
end

local function cargoQuery(tables, fields, args)
	args = args or {}
	local ok, result = pcall(function()
		return cargo.query(tables, fields, args)
	end)

	if ok and result then
		return result
	end

	return {}
end

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

local function addUnique(list, seen, value)
	value = trim(value)
	if value and not seen[value] then
		seen[value] = true
		table.insert(list, value)
	end
end

local function sorted(list)
	table.sort(list, function(a, b)
		return tostring(a):lower() < tostring(b):lower()
	end)
	return list
end

local function formatYear(dateValue)
	dateValue = trim(dateValue)
	if not dateValue then
		return nil
	end
	local year = tostring(dateValue):match('^(%d%d%d%d)')
	return year
end

local function getDisplayName(pageName)
	pageName = trim(pageName)
	if not pageName then
		return nil
	end

	local rows = cargoQuery(
		'Characters',
		'Page,DisplayName',
		{
			where = 'Page="' .. esc(pageName) .. '"',
			limit = 1
		}
	)

	if rows[1] and trim(rows[1].DisplayName) then
		return trim(rows[1].DisplayName)
	end

	return pageName
end

local function getCharacter(pageName)
	pageName = trim(pageName)
	if not pageName then
		return nil
	end

	local rows = cargoQuery(
		'Characters',
		'Page,DisplayName,BirthDate,DeathDate,Status,Gender',
		{
			where = 'Page="' .. esc(pageName) .. '"',
			limit = 1
		}
	)

	return rows[1]
end

local function makeLinkedName(pageName)
	local displayName = getDisplayName(pageName)
	return '[[' .. pageName .. '|' .. displayName .. ']]'
end

local function linkList(list)
	local out = {}
	for _, pageName in ipairs(list) do
		table.insert(out, makeLinkedName(pageName))
	end
	return table.concat(out, '<br>')
end

local function getParents(person)
	person = trim(person)
	if not person then
		return {}, nil
	end

	local rows = cargoQuery(
		'ParentChild',
		'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
		{
			where = 'Child="' .. esc(person) .. '"',
			limit = 20
		}
	)

	local parents = {}
	local seen = {}

	for _, row in ipairs(rows) do
		addUnique(parents, seen, row.Parent1)
		addUnique(parents, seen, row.Parent2)
	end

	return sorted(parents), rows[1]
end

local function getChildren(person)
	person = trim(person)
	if not person then
		return {}
	end

	local rows = cargoQuery(
		'ParentChild',
		'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
		{
			where = 'Parent1="' .. esc(person) .. '" OR Parent2="' .. esc(person) .. '"',
			limit = 200
		}
	)

	table.sort(rows, function(a, b)
		local aOrder = tonumber(a.BirthOrder) or 9999
		local bOrder = tonumber(b.BirthOrder) or 9999
		if aOrder == bOrder then
			return tostring(a.Child):lower() < tostring(b.Child):lower()
		end
		return aOrder < bOrder
	end)

	local children = {}
	local seen = {}

	for _, row in ipairs(rows) do
		addUnique(children, seen, row.Child)
	end

	return children
end

local function getPartners(person)
	person = trim(person)
	if not person then
		return {}, {}
	end

	local rows = cargoQuery(
		'Unions',
		'UnionID,Partner1,Partner2,UnionType,Status,MarriageDate,DivorceDate,EngagementDate',
		{
			where = 'Partner1="' .. esc(person) .. '" OR Partner2="' .. esc(person) .. '"',
			limit = 50
		}
	)

	local partners = {}
	local seen = {}

	for _, row in ipairs(rows) do
		if trim(row.Partner1) == person then
			addUnique(partners, seen, row.Partner2)
		elseif trim(row.Partner2) == person then
			addUnique(partners, seen, row.Partner1)
		end
	end

	return sorted(partners), rows
end

local function getSiblings(person)
	person = trim(person)
	if not person then
		return {}
	end

	local targetRows = cargoQuery(
		'ParentChild',
		'Child,Parent1,Parent2,UnionID',
		{
			where = 'Child="' .. esc(person) .. '"',
			limit = 10
		}
	)

	if not targetRows[1] then
		return {}
	end

	local targetUnion = trim(targetRows[1].UnionID)
	local targetP1 = trim(targetRows[1].Parent1)
	local targetP2 = trim(targetRows[1].Parent2)

	local whereParts = {}

	if targetUnion then
		table.insert(whereParts, 'UnionID="' .. esc(targetUnion) .. '"')
	end

	if targetP1 and targetP2 then
		table.insert(whereParts, '(Parent1="' .. esc(targetP1) .. '" AND Parent2="' .. esc(targetP2) .. '")')
	end

	if #whereParts == 0 then
		return {}
	end

	local rows = cargoQuery(
		'ParentChild',
		'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
		{
			where = table.concat(whereParts, ' OR '),
			limit = 100
		}
	)

	local siblings = {}
	local seen = {}

	table.sort(rows, function(a, b)
		local aOrder = tonumber(a.BirthOrder) or 9999
		local bOrder = tonumber(b.BirthOrder) or 9999
		if aOrder == bOrder then
			return tostring(a.Child):lower() < tostring(b.Child):lower()
		end
		return aOrder < bOrder
	end)

	for _, row in ipairs(rows) do
		if trim(row.Child) ~= person then
			addUnique(siblings, seen, row.Child)
		end
	end

	return siblings
end

local function getNeighbors(person)
	local neighbors = {}

	person = trim(person)
	if not person then
		return neighbors
	end

	local unionRows = cargoQuery(
		'Unions',
		'UnionID,Partner1,Partner2',
		{
			where = 'Partner1="' .. esc(person) .. '" OR Partner2="' .. esc(person) .. '"',
			limit = 500
		}
	)

	for _, row in ipairs(unionRows) do
		neighbors[trim(row.Partner1)] = true
		neighbors[trim(row.Partner2)] = true
	end

	local parentChildRows = cargoQuery(
		'ParentChild',
		'Child,Parent1,Parent2,UnionID',
		{
			where = 'Child="' .. esc(person) .. '" OR Parent1="' .. esc(person) .. '" OR Parent2="' .. esc(person) .. '"',
			limit = 1000
		}
	)

	for _, row in ipairs(parentChildRows) do
		neighbors[trim(row.Child)] = true
		neighbors[trim(row.Parent1)] = true
		neighbors[trim(row.Parent2)] = true
	end

	neighbors[person] = nil
	neighbors[nil] = nil

	return neighbors
end

local function collectConnectedComponent(root)
	local visited = {}
	local queue = {}
	local head = 1

	root = trim(root)
	if not root then
		return visited
	end

	visited[root] = true
	table.insert(queue, root)

	while head <= #queue do
		local current = queue[head]
		head = head + 1

		local neighbors = getNeighbors(current)

		for neighbor, _ in pairs(neighbors) do
			if neighbor and not visited[neighbor] then
				visited[neighbor] = true
				table.insert(queue, neighbor)
			end
		end
	end

	return visited
end

local function makeCard(pageName)
	pageName = trim(pageName)
	if not pageName then
		return ''
	end

	local c = getCharacter(pageName)
	local displayName = getDisplayName(pageName)
	local birthYear = c and formatYear(c.BirthDate) or nil
	local deathYear = c and formatYear(c.DeathDate) or nil

	local years = ''
	if birthYear or deathYear then
		years = '<div class="ft-years">' .. (birthYear or '?') .. '–' .. (deathYear or '') .. '</div>'
	end

	return '<div class="ft-card">[[' .. pageName .. '|' .. displayName .. ']]' .. years .. '</div>'
end

local function makePersonRow(people)
	if not people or #people == 0 then
		return ''
	end

	local html = {}
	table.insert(html, '<div class="ft-row">')
	for _, person in ipairs(people) do
		table.insert(html, makeCard(person))
	end
	table.insert(html, '</div>')

	return table.concat(html)
end

local function makeCoupleCards(couples)
	if not couples or #couples == 0 then
		return ''
	end

	local html = {}

	for _, pair in ipairs(couples) do
		table.insert(html, '<div class="ft-couple">')
		for _, person in ipairs(pair) do
			table.insert(html, makeCard(person))
		end
		table.insert(html, '</div>')
	end

	return table.concat(html)
end

local function makeCoupleRow(root, partners)
	local couples = {}

	if not partners or #partners == 0 then
		table.insert(couples, { root })
	else
		for _, partner in ipairs(partners) do
			table.insert(couples, { root, partner })
		end
	end

	return couples
end

function p.connected(frame)
	local args = frame.args
	local parentArgs = frame:getParent() and frame:getParent().args or {}
	local root = trim(args.root or parentArgs.root or args[1] or parentArgs[1])

	if not root then
		return 'Error: no root provided. Use root=Character Name'
	end

	local component = collectConnectedComponent(root)
	local people = {}

	for name, _ in pairs(component) do
		table.insert(people, name)
	end
	sorted(people)

	if #people == 0 then
		return 'No connected people found for ' .. root
	end

	local lines = {}
	table.insert(lines, "'''Connected component for " .. getDisplayName(root) .. "'''")
	table.insert(lines, '* Total people found: ' .. tostring(#people))

	for _, person in ipairs(people) do
		table.insert(lines, '* ' .. makeLinkedName(person))
	end

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

function p.profile(frame)
	local args = frame.args
	local parentArgs = frame:getParent() and frame:getParent().args or {}
	local root = trim(args.root or parentArgs.root or args[1] or parentArgs[1])

	if not root then
		return 'Error: no root provided. Use root=Character Name'
	end

	local parents = getParents(root)
	local siblings = getSiblings(root)
	local partners = getPartners(root)
	local children = getChildren(root)

	local lines = {}
	table.insert(lines, '{| class="wikitable" style="width:100%; max-width:900px;"')
	table.insert(lines, '|-')
	table.insert(lines, '! colspan="2" | Family profile for ' .. getDisplayName(root))
	table.insert(lines, '|-')
	table.insert(lines, '! style="width:20%;" | Person')
	table.insert(lines, '| ' .. makeLinkedName(root))

	table.insert(lines, '|-')
	table.insert(lines, '! Parents')
	table.insert(lines, '| ' .. (#parents > 0 and linkList(parents) or '—'))

	table.insert(lines, '|-')
	table.insert(lines, '! Siblings')
	table.insert(lines, '| ' .. (#siblings > 0 and linkList(siblings) or '—'))

	table.insert(lines, '|-')
	table.insert(lines, '! Partners')
	table.insert(lines, '| ' .. (#partners > 0 and linkList(partners) or '—'))

	table.insert(lines, '|-')
	table.insert(lines, '! Children')
	table.insert(lines, '| ' .. (#children > 0 and linkList(children) or '—'))

	table.insert(lines, '|}')

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

function p.tree(frame)
	local args = frame.args
	local parentArgs = frame:getParent() and frame:getParent().args or {}
	local root = trim(args.root or parentArgs.root or args[1] or parentArgs[1])

	if not root then
		return 'Error: no root provided. Use root=Character Name'
	end

	local parents = getParents(root)
	local partners = getPartners(root)
	local children = getChildren(root)

	local grandparents = {}
	local gpSeen = {}

	for _, parentName in ipairs(parents) do
		local parentParents = getParents(parentName)
		for _, gp in ipairs(parentParents) do
			addUnique(grandparents, gpSeen, gp)
		end
	end
	sorted(grandparents)

	local rootCouples = makeCoupleRow(root, partners)

	local html = {}
	table.insert(html, '<div class="ft-tree">')
	table.insert(html, '<div class="ft-title">Family tree for ' .. getDisplayName(root) .. '</div>')

	if #grandparents > 0 then
		table.insert(html, '<div class="ft-generation ft-grandparents">')
		table.insert(html, makePersonRow(grandparents))
		table.insert(html, '</div>')
	end

	if #grandparents > 0 and #parents > 0 then
		table.insert(html, '<div class="ft-connector"></div>')
	end

	if #parents > 0 then
		table.insert(html, '<div class="ft-generation ft-parents">')
		table.insert(html, makePersonRow(parents))
		table.insert(html, '</div>')
	end

	if (#parents > 0) and (#rootCouples > 0) then
		table.insert(html, '<div class="ft-connector"></div>')
	end

	table.insert(html, '<div class="ft-generation ft-root">')
	table.insert(html, '<div class="ft-row">')
	table.insert(html, makeCoupleCards(rootCouples))
	table.insert(html, '</div>')
	table.insert(html, '</div>')

	if #children > 0 then
		table.insert(html, '<div class="ft-connector"></div>')
		table.insert(html, '<div class="ft-generation ft-children">')
		table.insert(html, makePersonRow(children))
		table.insert(html, '</div>')
	end

	table.insert(html, '</div>')

	return table.concat(html)
end

return p