Module:FamilyTree
From KB Lexicon
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 a:lower() < b:lower()
end)
return list
end
local function getDisplayName(pageName)
pageName = trim(pageName)
if not pageName then
return nil
end
local rows = cargoQuery(
'Characters',
'Page,DisplayName,BirthDate,DeathDate',
{
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 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 getCharacter(pageName)
pageName = trim(pageName)
if not pageName then
return nil
end
local rows = cargoQuery(
'Characters',
'Page,DisplayName,BirthDate,DeathDate,Status',
{
where = 'Page="' .. esc(pageName) .. '"',
limit = 1
}
)
return rows[1]
end
local function getParents(person)
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)
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)
local rows = cargoQuery(
'Unions',
'UnionID,Partner1,Partner2,UnionType,Status',
{
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)
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 makeRow(label, people)
if not people or #people == 0 then
return ''
end
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">')
for _, person in ipairs(people) do
table.insert(html, makeCard(person))
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 = { root }
local head = 1
visited[root] = true
while head <= #queue do
local current = queue[head]
head = head + 1
local parents = getParents(current)
local children = getChildren(current)
local partners = getPartners(current)
for _, person in ipairs(parents) do
if not visited[person] then
visited[person] = true
table.insert(queue, person)
end
end
for _, person in ipairs(children) do
if not visited[person] then
visited[person] = true
table.insert(queue, person)
end
end
for _, person in ipairs(partners) do
if not visited[person] then
visited[person] = true
table.insert(queue, person)
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, '* [[' .. person .. '|' .. getDisplayName(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 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, '| [[' .. root .. '|' .. getDisplayName(root) .. ']]')
table.insert(lines, '|-')
table.insert(lines, '! Parents')
table.insert(lines, '| ' .. (#parents > 0 and table.concat((function()
local out = {}
for _, pName in ipairs(parents) do
table.insert(out, '[[' .. pName .. '|' .. getDisplayName(pName) .. ']]')
end
return out
end)(), '<br>') or '—'))
table.insert(lines, '|-')
table.insert(lines, '! Partners')
table.insert(lines, '| ' .. (#partners > 0 and table.concat((function()
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, '! Children')
table.insert(lines, '| ' .. (#children > 0 and table.concat((function()
local out = {}
for _, cName in ipairs(children) do
table.insert(out, '[[' .. cName .. '|' .. getDisplayName(cName) .. ']]')
end
return out
end)(), '<br>') 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 rootRow = { root }
for _, partner in ipairs(partners) do
table.insert(rootRow, partner)
end
local html = {}
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-generation ft-grandparents">')
table.insert(html, makeRow('', grandparents))
table.insert(html, '</div>')
table.insert(html, '<div class="ft-connector"></div>')
table.insert(html, '<div class="ft-generation ft-parents">')
table.insert(html, makeRow('', parents))
table.insert(html, '</div>')
table.insert(html, '<div class="ft-connector"></div>')
table.insert(html, '<div class="ft-generation ft-root">')
table.insert(html, makeRow('', rootRow))
table.insert(html, '</div>')
table.insert(html, '<div class="ft-connector"></div>')
table.insert(html, '<div class="ft-generation ft-children">')
table.insert(html, makeRow('', children))
table.insert(html, '</div>')
return table.concat(html)
end
return p