Module:FamilyTree: Difference between revisions

From KB Lexicon
(Replaced content with "local p = {} function p.tree(frame) local root = frame.args.root or frame.args[1] if not root then return "Error: no root provided." end return '<div style="padding:20px; border:3px solid red; font-size:20px;">NEW TREE MODULE LOADED: ' .. root .. '</div>' end return p")
Tags: Replaced Reverted
No edit summary
Tag: Manual revert
Line 1: Line 1:
local p = {}
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 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 formatYear(dateValue)
dateValue = trim(dateValue)
if not dateValue then
return nil
end
return tostring(dateValue):match('^(%d%d%d%d)')
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 addSet(set, value)
value = trim(value)
if value then
set[value] = true
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 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 getDisplayName(pageName)
local c = getCharacter(pageName)
if c and trim(c.DisplayName) then
return trim(c.DisplayName)
end
return pageName
end
local function makeLinkedName(pageName)
return '[[' .. pageName .. '|' .. getDisplayName(pageName) .. ']]'
end
local function linkList(list)
local out = {}
for _, pageName in ipairs(list or {}) 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
local p1 = trim(row.Partner1)
local p2 = trim(row.Partner2)
if p1 == person then
addUnique(partners, seen, p2)
elseif p2 == person then
addUnique(partners, seen, p1)
end
end
return sorted(partners), rows
end
local function getUnionBetween(personA, personB)
personA = trim(personA)
personB = trim(personB)
if not personA or not personB then
return nil
end
local rows = cargoQuery(
'Unions',
'UnionID,Partner1,Partner2,UnionType,Status,MarriageDate,DivorceDate,EngagementDate',
{
where = '(Partner1="' .. esc(personA) .. '" AND Partner2="' .. esc(personB) .. '") OR (Partner1="' .. esc(personB) .. '" AND Partner2="' .. esc(personA) .. '")',
limit = 1
}
)
return rows[1]
end
local function getSiblingGeneration(person)
person = trim(person)
if not person then
return {}
end
local targetRows = cargoQuery(
'ParentChild',
'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
{
where = 'Child="' .. esc(person) .. '"',
limit = 10
}
)
if not targetRows[1] then
return { person }
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 { person }
end
local rows = cargoQuery(
'ParentChild',
'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
{
where = table.concat(whereParts, ' OR '),
limit = 100
}
)
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 people = {}
local seen = {}
for _, row in ipairs(rows) do
addUnique(people, seen, row.Child)
end
if #people == 0 then
table.insert(people, person)
end
return people
end
local function buildCoupleGroups(people)
local groups = {}
local used = {}
local working = {}
for _, person in ipairs(people or {}) do
table.insert(working, person)
end
sorted(working)
for _, person in ipairs(working) do
if not used[person] then
local partners = getPartners(person)
local matchedPartner = nil
for _, partner in ipairs(partners) do
for _, candidate in ipairs(working) do
if candidate == partner and not used[candidate] then
matchedPartner = partner
break
end
end
if matchedPartner then
break
end
end
if matchedPartner then
used[person] = true
used[matchedPartner] = true
local union = getUnionBetween(person, matchedPartner)
table.insert(groups, {
type = 'couple',
left = person,
right = matchedPartner,
marriageYear = union and formatYear(union.MarriageDate) or nil
})
else
used[person] = true
table.insert(groups, {
type = 'single',
person = person
})
end
end
end
return groups
end
local function buildFamilyUnitsForGeneration(people)
local units = {}
for _, person in ipairs(people or {}) do
local partners = getPartners(person)
local children = getChildren(person)
local partner = partners[1]
local marriageYear = nil
if partner then
local union = getUnionBetween(person, partner)
marriageYear = union and formatYear(union.MarriageDate) or nil
end
table.insert(units, {
person = person,
partner = partner,
marriageYear = marriageYear,
children = children
})
end
return units
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 makeCoupleMarkup(left, right, marriageYear)
if right then
local html = {}
table.insert(html, '<div class="ft-unit ft-unit-couple">')
table.insert(html, makeCard(left))
table.insert(html, '<div class="ft-marriage">')
if marriageYear then
table.insert(html, '<div class="ft-marriage-year">' .. marriageYear .. '</div>')
end
table.insert(html, '<div class="ft-marriage-line"></div>')
table.insert(html, '</div>')
table.insert(html, makeCard(right))
table.insert(html, '</div>')
return table.concat(html)
else
return '<div class="ft-unit ft-unit-single">' .. makeCard(left) .. '</div>'
end
end
local function makeCoupleGroupRow(groups)
if not groups or #groups == 0 then
return ''
end
local html = {}
table.insert(html, '<div class="ft-row">')
for _, group in ipairs(groups) do
if group.type == 'single' then
table.insert(html, makeCoupleMarkup(group.person, nil, nil))
else
table.insert(html, makeCoupleMarkup(group.left, group.right, group.marriageYear))
end
end
table.insert(html, '</div>')
return table.concat(html)
end
local function makeFamilyUnitsRow(units)
if not units or #units == 0 then
return ''
end
local html = {}
table.insert(html, '<div class="ft-sibling-generation">')
table.insert(html, '<div class="ft-sibling-spine"></div>')
table.insert(html, '<div class="ft-family-row">')
for _, unit in ipairs(units) do
table.insert(html, '<div class="ft-family-unit">')
table.insert(html, '<div class="ft-family-up-line"></div>')
table.insert(html, '<div class="ft-family-main">')
table.insert(html, makeCoupleMarkup(unit.person, unit.partner, unit.marriageYear))
table.insert(html, '</div>')
if unit.children and #unit.children > 0 then
table.insert(html, '<div class="ft-family-desc-line"></div>')
table.insert(html, '<div class="ft-family-children">')
for _, child in ipairs(unit.children) do
table.insert(html, makeCard(child))
end
table.insert(html, '</div>')
end
table.insert(html, '</div>')
end
table.insert(html, '</div>')
table.insert(html, '</div>')
return table.concat(html)
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 visited = {}
local queue = {}
local head = 1
visited[root] = true
table.insert(queue, root)
while head <= #queue do
local current = queue[head]
head = head + 1
local neighbors = {}
local parents = getParents(current)
local children = getChildren(current)
local partners = getPartners(current)
for _, person in ipairs(parents) do
addSet(neighbors, person)
end
for _, person in ipairs(children) do
addSet(neighbors, person)
end
for _, person in ipairs(partners) do
addSet(neighbors, person)
end
for neighbor, _ in pairs(neighbors) do
if neighbor and not visited[neighbor] then
visited[neighbor] = true
table.insert(queue, neighbor)
end
end
end
local people = {}
for name, _ in pairs(visited) do
table.insert(people, name)
end
sorted(people)
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 = getSiblingGeneration(root)
local partners = getPartners(root)
local children = getChildren(root)
local siblingList = {}
for _, person in ipairs(siblings) do
if person ~= root then
table.insert(siblingList, person)
end
end
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, '| ' .. (#siblingList > 0 and linkList(siblingList) 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)
function p.tree(frame)
    local root = frame.args.root or frame.args[1]
local args = frame.args
    if not root then
local parentArgs = frame:getParent() and frame:getParent().args or {}
        return "Error: no root provided."
local root = trim(args.root or parentArgs.root or args[1] or parentArgs[1])
    end
 
if not root then
return 'Error: no root provided. Use root=Character Name'
end
 
local parents = getParents(root)
local siblingGeneration = getSiblingGeneration(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 grandparentGroups = buildCoupleGroups(grandparents)
local parentGroups = buildCoupleGroups(parents)
local familyUnits = buildFamilyUnitsForGeneration(siblingGeneration)
 
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">')
table.insert(html, makeCoupleGroupRow(grandparentGroups))
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">')
table.insert(html, makeCoupleGroupRow(parentGroups))
table.insert(html, '</div>')
end
 
if #parents > 0 and #familyUnits > 0 then
table.insert(html, '<div class="ft-connector"></div>')
end
 
if #familyUnits > 0 then
table.insert(html, '<div class="ft-generation">')
table.insert(html, makeFamilyUnitsRow(familyUnits))
table.insert(html, '</div>')
end


    return '<div style="padding:20px; border:3px solid red; font-size:20px;">NEW TREE MODULE LOADED: ' .. root .. '</div>'
table.insert(html, '</div>')
return table.concat(html)
end
end


return p
return p

Revision as of 22:44, 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 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 formatYear(dateValue)
	dateValue = trim(dateValue)
	if not dateValue then
		return nil
	end
	return tostring(dateValue):match('^(%d%d%d%d)')
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 addSet(set, value)
	value = trim(value)
	if value then
		set[value] = true
	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 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 getDisplayName(pageName)
	local c = getCharacter(pageName)
	if c and trim(c.DisplayName) then
		return trim(c.DisplayName)
	end
	return pageName
end

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

local function linkList(list)
	local out = {}
	for _, pageName in ipairs(list or {}) 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
		local p1 = trim(row.Partner1)
		local p2 = trim(row.Partner2)

		if p1 == person then
			addUnique(partners, seen, p2)
		elseif p2 == person then
			addUnique(partners, seen, p1)
		end
	end

	return sorted(partners), rows
end

local function getUnionBetween(personA, personB)
	personA = trim(personA)
	personB = trim(personB)

	if not personA or not personB then
		return nil
	end

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

	return rows[1]
end

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

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

	if not targetRows[1] then
		return { person }
	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 { person }
	end

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

	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 people = {}
	local seen = {}
	for _, row in ipairs(rows) do
		addUnique(people, seen, row.Child)
	end

	if #people == 0 then
		table.insert(people, person)
	end

	return people
end

local function buildCoupleGroups(people)
	local groups = {}
	local used = {}
	local working = {}

	for _, person in ipairs(people or {}) do
		table.insert(working, person)
	end
	sorted(working)

	for _, person in ipairs(working) do
		if not used[person] then
			local partners = getPartners(person)
			local matchedPartner = nil

			for _, partner in ipairs(partners) do
				for _, candidate in ipairs(working) do
					if candidate == partner and not used[candidate] then
						matchedPartner = partner
						break
					end
				end
				if matchedPartner then
					break
				end
			end

			if matchedPartner then
				used[person] = true
				used[matchedPartner] = true

				local union = getUnionBetween(person, matchedPartner)
				table.insert(groups, {
					type = 'couple',
					left = person,
					right = matchedPartner,
					marriageYear = union and formatYear(union.MarriageDate) or nil
				})
			else
				used[person] = true
				table.insert(groups, {
					type = 'single',
					person = person
				})
			end
		end
	end

	return groups
end

local function buildFamilyUnitsForGeneration(people)
	local units = {}

	for _, person in ipairs(people or {}) do
		local partners = getPartners(person)
		local children = getChildren(person)

		local partner = partners[1]
		local marriageYear = nil

		if partner then
			local union = getUnionBetween(person, partner)
			marriageYear = union and formatYear(union.MarriageDate) or nil
		end

		table.insert(units, {
			person = person,
			partner = partner,
			marriageYear = marriageYear,
			children = children
		})
	end

	return units
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 makeCoupleMarkup(left, right, marriageYear)
	if right then
		local html = {}
		table.insert(html, '<div class="ft-unit ft-unit-couple">')
		table.insert(html, makeCard(left))
		table.insert(html, '<div class="ft-marriage">')
		if marriageYear then
			table.insert(html, '<div class="ft-marriage-year">' .. marriageYear .. '</div>')
		end
		table.insert(html, '<div class="ft-marriage-line"></div>')
		table.insert(html, '</div>')
		table.insert(html, makeCard(right))
		table.insert(html, '</div>')
		return table.concat(html)
	else
		return '<div class="ft-unit ft-unit-single">' .. makeCard(left) .. '</div>'
	end
end

local function makeCoupleGroupRow(groups)
	if not groups or #groups == 0 then
		return ''
	end

	local html = {}
	table.insert(html, '<div class="ft-row">')

	for _, group in ipairs(groups) do
		if group.type == 'single' then
			table.insert(html, makeCoupleMarkup(group.person, nil, nil))
		else
			table.insert(html, makeCoupleMarkup(group.left, group.right, group.marriageYear))
		end
	end

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

local function makeFamilyUnitsRow(units)
	if not units or #units == 0 then
		return ''
	end

	local html = {}
	table.insert(html, '<div class="ft-sibling-generation">')
	table.insert(html, '<div class="ft-sibling-spine"></div>')
	table.insert(html, '<div class="ft-family-row">')

	for _, unit in ipairs(units) do
		table.insert(html, '<div class="ft-family-unit">')
		table.insert(html, '<div class="ft-family-up-line"></div>')
		table.insert(html, '<div class="ft-family-main">')
		table.insert(html, makeCoupleMarkup(unit.person, unit.partner, unit.marriageYear))
		table.insert(html, '</div>')

		if unit.children and #unit.children > 0 then
			table.insert(html, '<div class="ft-family-desc-line"></div>')
			table.insert(html, '<div class="ft-family-children">')
			for _, child in ipairs(unit.children) do
				table.insert(html, makeCard(child))
			end
			table.insert(html, '</div>')
		end

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

	table.insert(html, '</div>')
	table.insert(html, '</div>')
	return table.concat(html)
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 visited = {}
	local queue = {}
	local head = 1

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

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

		local neighbors = {}
		local parents = getParents(current)
		local children = getChildren(current)
		local partners = getPartners(current)

		for _, person in ipairs(parents) do
			addSet(neighbors, person)
		end
		for _, person in ipairs(children) do
			addSet(neighbors, person)
		end
		for _, person in ipairs(partners) do
			addSet(neighbors, person)
		end

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

	local people = {}
	for name, _ in pairs(visited) do
		table.insert(people, name)
	end
	sorted(people)

	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 = getSiblingGeneration(root)
	local partners = getPartners(root)
	local children = getChildren(root)

	local siblingList = {}
	for _, person in ipairs(siblings) do
		if person ~= root then
			table.insert(siblingList, person)
		end
	end

	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, '| ' .. (#siblingList > 0 and linkList(siblingList) 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 siblingGeneration = getSiblingGeneration(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 grandparentGroups = buildCoupleGroups(grandparents)
	local parentGroups = buildCoupleGroups(parents)
	local familyUnits = buildFamilyUnitsForGeneration(siblingGeneration)

	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">')
		table.insert(html, makeCoupleGroupRow(grandparentGroups))
		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">')
		table.insert(html, makeCoupleGroupRow(parentGroups))
		table.insert(html, '</div>')
	end

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

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

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

return p