Module:FamilyTree
From KB Lexicon
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 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,
status = nil,
birthFamily = nil,
currentFamily = nil,
father = nil,
mother = nil,
adoptiveFather = nil,
adoptiveMother = nil,
bloodStatus = nil,
title = nil,
heir = nil,
illegitimate = nil,
adopted = nil,
parents = {},
children = {},
partners = {},
unions = {}
}
end
return people[name]
end
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 yesNo(val)
if val == nil then
return nil
end
local s = mw.ustring.lower(tostring(val))
if s == '1' or s == 'true' or s == 'yes' then
return 'Yes'
end
if s == '0' or s == 'false' or s == 'no' then
return 'No'
end
return tostring(val)
end
-- =========================================
-- Data loading
-- =========================================
local function queryCharacters()
local results = cargo.query(
'Characters',
'Page,DisplayName,Gender,BirthDate,DeathDate,Status,BirthFamily,CurrentFamily,Father,Mother,AdoptiveFather,AdoptiveMother,BloodStatus,Title,Heir,Illegitimate,Adopted',
{ limit = 5000 }
)
local people = {}
for _, row in ipairs(results) do
local page = trim(row.Page)
if isRealValue(page) then
people[page] = {
name = page,
displayName = trim(row.DisplayName) or page,
gender = trim(row.Gender),
birthDate = trim(row.BirthDate),
deathDate = trim(row.DeathDate),
status = trim(row.Status),
birthFamily = trim(row.BirthFamily),
currentFamily = trim(row.CurrentFamily),
father = trim(row.Father),
mother = trim(row.Mother),
adoptiveFather = trim(row.AdoptiveFather),
adoptiveMother = trim(row.AdoptiveMother),
bloodStatus = trim(row.BloodStatus),
title = trim(row.Title),
heir = row.Heir,
illegitimate = row.Illegitimate,
adopted = row.Adopted,
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 loadCharacterParentFallbacks(people)
for _, person in pairs(people) do
if isRealValue(person.father) then
ensurePerson(people, person.father)
addUnique(person.parents, person.father)
addUnique(people[person.father].children, person.name)
end
if isRealValue(person.mother) then
ensurePerson(people, person.mother)
addUnique(person.parents, person.mother)
addUnique(people[person.mother].children, person.name)
end
if isRealValue(person.adoptiveFather) then
ensurePerson(people, person.adoptiveFather)
addUnique(person.parents, person.adoptiveFather)
addUnique(people[person.adoptiveFather].children, person.name)
end
if isRealValue(person.adoptiveMother) then
ensurePerson(people, person.adoptiveMother)
addUnique(person.parents, person.adoptiveMother)
addUnique(people[person.adoptiveMother].children, person.name)
end
end
end
local function loadUnions(people)
local results = cargo.query(
'Unions',
'UnionID,Partner1,Partner2,UnionType,Status,StartDate,EndDate,MarriageDate,DivorceDate,EngagementDate',
{ 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),
startDate = trim(row.StartDate),
endDate = trim(row.EndDate),
marriageDate = trim(row.MarriageDate),
divorceDate = trim(row.DivorceDate),
engagementDate = trim(row.EngagementDate)
})
table.insert(people[p2].unions, {
unionID = unionID,
partner = p1,
unionType = trim(row.UnionType),
status = trim(row.Status),
startDate = trim(row.StartDate),
endDate = trim(row.EndDate),
marriageDate = trim(row.MarriageDate),
divorceDate = trim(row.DivorceDate),
engagementDate = trim(row.EngagementDate)
})
end
end
end
local function finalizePeople(people)
for _, person in pairs(people) do
person.parents = uniq(person.parents)
person.children = uniq(person.children)
person.partners = uniq(person.partners)
end
end
local function loadData()
local people = queryCharacters()
loadParentChild(people)
loadCharacterParentFallbacks(people)
loadUnions(people)
finalizePeople(people)
return people
end
-- =========================================
-- Relationship helpers
-- =========================================
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 helpers
-- =========================================
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('Title', person.title)
addField('Gender', person.gender)
addField('Birth Date', person.birthDate)
addField('Death Date', person.deathDate)
addField('Status', person.status)
addField('Blood Status', person.bloodStatus)
addField('Birth Family', person.birthFamily and makeLink(person.birthFamily) or nil)
addField('Current Family', person.currentFamily and makeLink(person.currentFamily) or nil)
addField('Heir', yesNo(person.heir))
addField('Illegitimate', yesNo(person.illegitimate))
addField('Adopted', yesNo(person.adopted))
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