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 | 'Page,DisplayName', | ||
{ | { | ||
where = 'Page="' .. esc(pageName) .. '"', | where = 'Page="' .. esc(pageName) .. '"', | ||
| Line 73: | Line 82: | ||
return pageName | return pageName | ||
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>' | ||
end | end | ||
| Line 199: | Line 363: | ||
end | end | ||
local function | 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">') | 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>') | |||
return table.concat(html) | return table.concat(html) | ||
end | end | ||
function | local function makeCoupleCards(couples) | ||
if not couples or #couples == 0 then | |||
local | 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 | 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 | end | ||
local component = collectConnectedComponent(root) | |||
local people = {} | local people = {} | ||
for name, _ in pairs( | |||
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, '* | 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, '| | 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.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, '|-') | ||
table.insert(lines, '! Partners') | table.insert(lines, '! Partners') | ||
table.insert(lines, '| ' .. (#partners > 0 and | table.insert(lines, '| ' .. (#partners > 0 and linkList(partners) or '—')) | ||
table.insert(lines, '|-') | table.insert(lines, '|-') | ||
table.insert(lines, '! Children') | table.insert(lines, '! Children') | ||
table.insert(lines, '| ' .. (#children > 0 and | table.insert(lines, '| ' .. (#children > 0 and linkList(children) or '—')) | ||
table.insert(lines, '|}') | table.insert(lines, '|}') | ||
| Line 354: | Line 509: | ||
sorted(grandparents) | sorted(grandparents) | ||
local | local rootCouples = makeCoupleRow(root, partners) | ||
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- | 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- | if #grandparents > 0 and #parents > 0 then | ||
table.insert(html, '<div class="ft-connector"></div>') | |||
end | |||
table.insert(html, '<div class="ft- | 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- | if (#parents > 0) and (#rootCouples > 0) then | ||
table.insert(html, '<div class="ft-connector"></div>') | |||
end | |||
table.insert(html, '<div class="ft- | 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, | 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